交易所实现自动确认以太坊到账
因业务需求,需要实现交易所自动确认用户的转账。
与btc的不同
交易所实现自动确认比特币到账
上一篇文章有说到确认btc到账的方法,eth与其大致相同,都是获取一段时间内区块上的所有交易,存入数据库。然后历遍新存入的交易数据,如果交易数据中的to
能与本站用户的钱包地址匹配,那这比交易就是用户充值的。
与btc不同的是获取交易数据的方式不同。btc必须借助第三方api才能实现,eth只需要调用自己钱包服务器的json-rpc就可以了。
业务逻辑
目前的逻辑是,通过eth_newBlockFilter
这个钱包json-rpc,在eth服务器上添加一个BlockFilter
。之后每次调用eth_getFilterChanges
,都会返回当前到上次调用之间的所有交易。
获取到交易数据后就是上述所说的,存入数据库。然后历遍新存入的交易数据,如果交易数据中的to
能与本站用户的钱包地址匹配,即交易接收人是本站的用户,那这比交易就是用户充值的。
数据库
与btc整体差不多。
block表
存储币种和最新区块高度
eth表
存储btc交易数据
代码实现
业务逻辑使用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=$`的操作。如果该转出记录超过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;
}
})
}
总结
目前阶段能想到的不更改钱包应用程序即二次开发的基础上,最好的办法。(〃^∇^)ぇ∧∧∧っ
Or you can contact me by Email