因业务需求,需要实现交易所自动确认用户的转账。

与btc的不同

交易所实现自动确认比特币到账
上一篇文章有说到确认btc到账的方法,eth与其大致相同,都是获取一段时间内区块上的所有交易,存入数据库。然后历遍新存入的交易数据,如果交易数据中的to能与本站用户的钱包地址匹配,那这比交易就是用户充值的。
与btc不同的是获取交易数据的方式不同。btc必须借助第三方api才能实现,eth只需要调用自己钱包服务器的json-rpc就可以了。

业务逻辑

目前的逻辑是,通过eth_newBlockFilter这个钱包json-rpc,在eth服务器上添加一个BlockFilter。之后每次调用eth_getFilterChanges,都会返回当前到上次调用之间的所有交易。
获取到交易数据后就是上述所说的,存入数据库。然后历遍新存入的交易数据,如果交易数据中的to能与本站用户的钱包地址匹配,即交易接收人是本站的用户,那这比交易就是用户充值的。

数据库

与btc整体差不多。

block表

存储币种和最新区块高度

block.png

eth表

存储btc交易数据

eth.png

代码实现

业务逻辑使用NodeJs实现,其他语言也是可行的,因为要保证代码一直运行,所以要尽可能地catch各种错误,不要让报错停掉代码。
接口文档:blockchain

引入的文件

var Db = require('../Db.js');    //网站数据库
var BDb = require('../BDb.js');    //存储区块交易数据的数据库
var req = require('../request.js');    //http请求
var log = require('../log');    //log日志输出
var COIN = "btc";

checkBlock

该方法确认是否有新区块生产。

BlockFilter在运行久了之后会出现失效的情况,即eth_getFilterChanges返回错误代码-32000,所以在eth_getFilterChanges加了判断。其他的与btc大致一样。
这里有一个bestBlockNumber > block的判断操作,bestBlockNumber是获取到的最新区块,block是数据库中保存的最新区块。这个操作是为了保证能记录到所有的区块,不排除一些异常情况比如抛出错误代码停止运行,或是其他网络异常,导致一定时间内代码没有工作,就会漏掉许多区块。blockLog()用于记录指定区块的交易,为了保证服务器压力,这里i <= block + 30限制了最多写入30个区块的限制。当更新的区块达到了最新区块或者达到了30个的限制,就会把区块高度写进数据库里。下次再从这个高度开始。

var BlockFilter = '';

/**
 * 判断区块是否更新
 * @param block 保存的最新区块
 * @param lock 锁
 */
function checkBlock(block, lock) {
    block = parseInt(block);
    if (lock)
        return;

    clearLog(block);
    if (BlockFilter == '') {
        rpc.eth("eth_newBlockFilter", [], res => {
            BlockFilter = res;
            log.success("Add eth blocks to monitor successfully!");
        })
    } else {
        rpc.eth("eth_getFilterChanges", [
            BlockFilter
        ], res => {
            if (!res)
                return;
            if (res == -32000) {
                rpc.eth("eth_newBlockFilter", [], res => {
                    BlockFilter = res;
                    log.success("Add eth blocks to monitor successfully!");
                })
            } else if (res.length > 0) {
                rpc.eth("eth_getBlockByHash", [
                    res[res.length - 1],
                    true
                ], r => {
                    if (!r)
                        return;
                    let bestBlockNumber = hextoDecimal(r.number);
                    if (bestBlockNumber > block) {
                        for (let i = block + 1; i <= bestBlockNumber && i <= block + 30; i++) {
                            blockLog(i);
                            if (i == bestBlockNumber || i == block + 30) {
                                BDb.query(`UPDATE block SET block=${i} WHERE coinname='${COIN}'`, () => {
                                    log.success(`The best block of ${COIN} : ${i}`);
                                })
                            }
                        }
                    }
                })

            } else {
                log.success(`${COIN} block is not updated.`);
            }
        });
    }
}

blockLog

该方法调用eth_getBlockByNumber获取每个区块详细的交易记录。

当区块交易数据成功插入数据库后,调用userRechange()userReceivables()两个方法,用于确认用户的转入和转出。
需要注意的是,eth可能会出现空区块的现象(btc暂时没遇见),如果是空区块(没有任何交易),我们手动添加一条‘空交易’进这个区块里。因为如果这个区块没有交易数据,会影响到后面的recoveryBlock操作。

/**
 * 根据区块获取交易信息并写入数据库
 * @param blockNumber 区块高度
 */
function blockLog(blockNumber) {
    rpc.eth("eth_getBlockByNumber", [
        Decimaltohex(blockNumber),
        true
    ], res => {
        if (!res)
            return;

        let sql = `INSERT INTO ${COIN} (\`block\`,\`hash\`,\`addr\`,\`value\`,\`time\`) VALUES `;
        let sql_value = '';

        let time = hextoDecimal(res.timestamp);
        let transactions = res.transactions;


        if (transactions !== 'undefined' && transactions.length > 0) {
            transactions.forEach(e => {
                let sql_value_s = `(${blockNumber},'${e.hash}','${e.to}','${hextoDecimal(e.value) / 1000000000000000000}',${time})`;
                if (sql_value != '')
                    sql_value_s = ',' + sql_value_s;
                sql_value += sql_value_s;
            });
            sql += sql_value;
            BDb.query(sql, () => {
                log.success(`The ${COIN} block has been updated and the updated block is: ${blockNumber}`);
                userRechange(blockNumber);
                userReceivables(blockNumber);
            });
        }

        //区块无交易的操作
        if (transactions.length == 0) {
            let sql = `INSERT INTO ${COIN} (\`block\`,\`hash\`,\`addr\`,\`value\`,\`time\`) VALUES (${blockNumber},'0x0','0x0','0',${time})`;
            BDb.query(sql, () => {
                log.success(`The ${COIN} block has been restored and the restoration block is: ${blockNumber}`);
            });
        }
    })
}

userRechange

该方法历遍网站数据库中所有用户的地址,并把每个地址与对应高度的区块所有交易匹配,匹配成功则进行对应操作,添加转账记录,更改余额之类的。

这里还可以加一个判断,因为btc交易在确认6次之后才被认定为不可更改,即最新区块 - 交易发生区块 > 12。

/**
 * 查询一个区块内是否有用户转入
 * @param blockNumber 区块高度
 */
function userRechange(block_height) {
    Db.query(`SELECT ethb,userid FROM user_coin WHERE ethb!='' AND LEFT(ethb,1)!='{'`, res => {
        addrlist = res;
        if (addrlist !== 'undefined' && addrlist.length > 0) {
            addrlist.forEach(e => {
                let userid = e.userid;
                let addr = e.ethb;
                BDb.query(`SELECT * FROM ${COIN} WHERE addr='${addr}' AND block=${block_height}`, r => {
                    if (r.length > 0) {
                        let sql = "INSERT INTO qq3479015851_myzr (`userid`,`username`,`coinname`,`txid`,`num`,`mum`,`fee`,`sort`,`addtime`,`endtime`,`status`,`confirmations`,`hash`) VALUES ";
                        let sql_value = '';
                        let num = 0;
                        r.forEach(e => {
                            value = parseFloat(e.value);
                            num += value
                            sql_value_s = `(${userid},'','${COIN}',0,${value},${value},0,0,${e.time},0,1,0,'${e.hash}')`;
                            if (sql_value != '')
                                sql_value_s = ',' + sql_value_s;
                            sql_value += sql_value_s;
                            log.success(`User ${userid} rechanged ${value} ${COIN} in ${sDate(e.time)}`);
                        });
                        sql += sql_value;
                        Db.query(sql, () => {
                            Db.query(`UPDATE user_coin SET ${COIN}=${COIN}+${num} WHERE userid=${userid}`, () => {});
                        });
                    } else {}
                });
            });
        }
    });
}

userReceivables

该方法历遍网站数据库中所有转出记录,转出是用户先发起存入库中再调用钱包发起转账的。将转出记录中的转出地址与对应高度的区块所有交易匹配,匹配成功则进行对应操作,更改转出记录状态等。

同样可以加入一个判断确认次数的操作,另外这里还有一个${e.addtime - Date.parse(new Date()) / 1000 >= 10800 ? '' : block=${blockNumber}`的操作。如果该转出记录超过6小时了,就把该交易的转出地址与整个库的交易匹配。因为一笔交易很少会出现6个小时没有被处理,很有可能是出现了遗漏。

/**
 * 查询一个区块内是否有用户转出
 * @param blockNumber 区块高度
 */
function userReceivables(block_height) {
    Db.query(`SELECT id,hash,addtime FROM myzc WHERE coinname='${COIN}' AND \`status\`=0`, res => {
        myzc = res;
        if (myzc !== 'undefined' && myzc.length > 0) {
            myzc.forEach(e => {
                let id = e.id;
                let hash = e.hash;
                BDb.query(`SELECT * FROM ${COIN} WHERE ${Date.parse(new Date()) / 1000 - e.addtime >= 10800 ? '' : `block=${block_height} AND `}\`hash\`='${hash}'`, r => {
                    if (r !== 'undefined' && r.length > 0) {
                        r = r[0];
                        Db.query(`UPDATE myzc SET \`status\`=1 WHERE \`hash\`='${hash}'`, () => {
                            log.success(`${COIN} turn out ID ${id} ${sDate(r.time)} already to account`);
                        });
                    } else {}
                });
            });

        }
    });
}

clearLog

该方法在checkBlock()一开始就被调用过,用于清理8小时前的区块交易,存储区块交易还是很占用空间的。

/**
 * 清理旧数据
 * @param block 区块高度
 */
function clearLog(block) {
    var timestamp = Date.parse(new Date()) / 1000;
    var time = timestamp - 8 * 3600;
    BDb.query(`DELETE FROM ${COIN} WHERE time<=${time}`, () => {
        log.success(`clear ${COIN} before ${time} block`)
        recoveryBlock(block);
    });
}

recoveryBlock

该方法在clearLog()清理完交易数据后调用,目的在于检查已存入的区块是否连贯,即所有区块交易都被记录上。发现缺失的区块就调用blockLog补上。

同样,为了保证服务器压力加了30次的次数限制。

/**
 * 恢复指定区块之前遗漏的区块
 * @param blockNumber 区块高度
 */
function recoveryBlock(blockNumber) {
    BDb.query(`SELECT block FROM \`${COIN}\` WHERE block<=${blockNumber} GROUP BY block`, res => {
        let defect = Array();
        let blockList = Array();
        if (res.length > 0) {
            res.forEach((e, i) => {
                blockList[i] = e.block;
            })
            blockList.push(blockNumber + 1);
            for (let i = 1; i < blockList.length; i++) {
                if (blockList[i] - blockList[i - 1] > 1) {
                    for (let j = 1; j < (blockList[i] - blockList[i - 1]); j++) {
                        defect.push(blockList[i - 1] + j);
                    }
                }
            }
        }
        log.success(`A total of ${defect.length} ${COIN} blocks will be restored.`);
        if (defect.length > 0) {
            for (let i = 0; i < defect.length && i < 30; i++) {
                blockLog(defect[i])
            }
        }
    })

}

sDate

/**
 * 时间戳转时间
 * @param ts 时间戳
 */
function sDate(ts) {
    return new Date(parseInt(ts) * 1000).toLocaleString();
}

app.js

入口文件,每60秒调用一次btc.checkBlock,eth的业务逻辑之后更新。

var Db = require('./Db.js');
var BDb = require('./BDb.js');
var req = require('./request.js');
var rpc = require('./rpc.js');
var btc = require('./coin/btc.js');
var eth = require('./coin/eth.js');
var log = require('./log');

var walvar = [
    'btc',
    'eth'
];

main();

function main(){
    walvar.forEach(e => {
        checkBlock(e);
    })

    setInterval(() => {
    log.success("--------------------------------------------------")
        walvar.forEach(e => {
            checkBlock(e);
        })
    }, 60000);

}

function checkBlock(coin) {
    BDb.query(`SELECT * FROM block WHERE coinname='${coin}'`, r => {
        block = r[0].block;
        lock = r[0].locked;
        switch (coin){
            case 'btc': btc.checkBlock(block, lock);break;
            case 'eth': eth.checkBlock(block, lock);break;
        }
    })
}

总结

目前阶段能想到的不更改钱包应用程序即二次开发的基础上,最好的办法。(〃^∇^)ぇ∧∧∧っ