交易所实现自动确认比特币到账
因业务需求,需要实现交易所自动确认用户的转账。
用户转账地址
用户的转账地址现阶段是直接json-rpc调用getnewaddress
接口生成。之后将改为统一批量生成多个地址存入数据库,新用户注册时自动分配,确保每个人的地址唯一。
业务逻辑
目前的逻辑是,通过每分钟轮询一次blockchain.info/latestblock
这个接口获取最新的区块高度。
如果区块高度更新,就通过/block-height/${blockNumber}?format=json
这个接口获取该区块下的所有交易数据,存入自己的数据库。
然后把所有用户的btc地址历遍,把每个用户的btc地址与最新更新的交易数据匹配,如果发现交易数据中的out
等于用户地址,即交易接收人为本站用户,那这比交易就是用户转入btc的交易。
数据库
block表
存储币种和最新区块高度
btc表
存储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
该方法确认是否有新区块生产。
这里有一个bestBlockNumber > block
的判断操作,bestBlockNumber
是获取到的最新区块,block
是数据库中保存的最新区块。这个操作是为了保证能记录到所有的区块,不排除一些异常情况比如抛出错误代码停止运行,或是其他网络异常,导致一定时间内代码没有工作,就会漏掉许多区块。blockLog()
用于记录指定区块的交易,为了保证服务器压力,这里i <= block + 30
限制了最多写入30个区块的限制。当更新的区块达到了最新区块或者达到了30个的限制,就会把区块高度写进数据库里。下次再从这个高度开始。
/**
* 判断区块是否更新
* @param block 保存的最新区块
* @param lock 锁
*/
function checkBlock(block, lock) {
block = parseInt(block);
if (lock)
return;
clearLog(block);
req.sget({
hostname: 'blockchain.info',
port: 443,
path: '/latestblock'
}, data => {
if (!data) {
log.success(`Failed to detect ${COIN} update!`);
return;
}
bestBlockNumber = data;
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
该方法调用blockchain.info/block-height/${blockNumber}?format=json
获取每个区块详细的交易记录。
bitcoin钱包提供的rpc方法中,没有一次获得一个区块中所有交易的详细信息如inputs
和out
,仅能获取一个区块中的所有交易hash,要获取详细信息还得再通过交易hash调用rpc,显然是不可取的。因此用到了第三方的api。另外这里要注意的是,/latestblock
这个接口获取到的最新区块高度比rpc调用自己钱包获取的区块高度通常会慢一会。
当区块交易数据成功插入数据库后,调用userRechange()
和userReceivables()
两个方法,用于确认用户的转入和转出。
/**
* 根据区块获取交易信息并写入数据库
* @param blockNumber 区块高度
*/
function blockLog(blockNumber) {
req.sget({
hostname: 'blockchain.info',
port: 443,
path: `/block-height/${blockNumber}?format=json`
}, res => {
if (!res) {
log.success(`Get the latest block of ${COIN} failed!`);
return;
}
let sql_value = "";
let tx = res.blocks[0].tx;
let time = res.blocks[0].time;
tx.forEach(e => {
let out = e.out;
let hash = e.hash;
out.forEach(o => {
let sql_value_s = `(${blockNumber},'${hash}','${o.addr}','${o.value}',${time})`;
if (sql_value != '')
sql_value_s = ',' + sql_value_s;
sql_value += sql_value_s;
})
});
let sql = `INSERT INTO ${COIN} (\`block\`,\`hash\`,\`addr\`,\`value\`,\`time\`) VALUES `;
sql += sql_value;
if (sql_value) {
BDb.query(sql, () => {
log.success(`The ${COIN} block has been updated and the updated block is: ${blockNumber}`);
userRechange(blockNumber);
userReceivables(blockNumber);
});
} else {
log.success(`There is no data in ${COIN} block ${blockNumber}`);
}
});
}
userRechange
该方法历遍网站数据库中所有用户的地址,并把每个地址与对应高度的区块所有交易匹配,匹配成功则进行对应操作,添加转账记录,更改余额之类的。
这里还可以加一个判断,因为btc交易在确认6次之后才被认定为不可更改,即最新区块 - 交易发生区块 > 6。
/**
* 查询一个区块内是否有用户转入
* @param blockNumber 区块高度
*/
function userRechange(blockNumber) {
Db.query(`SELECT btcb,userid FROM user_coin WHERE btcb!='' AND LEFT(btcb,1)!='{'`, r => {
let addrlist = r;
if (addrlist !== 'undefined' && addrlist.length > 0) {
addrlist.forEach(e => {
let userid = e.userid;
let addr = e.btcb;
BDb.query(`SELECT * FROM ${COIN} WHERE addr='${addr}' AND block=${blockNumber}`, r => {
if (r.length > 0) {
let sql = "INSERT INTO myzr (`userid`,`username`,`coinname`,`txid`,`num`,`mum`,`fee`,`sort`,`addtime`,`endtime`,`status`,`confirmations`,`hash`) VALUES ";
let sql_value = '';
let num = 0;
r.forEach(e => {
let value = e.value / 100000000;
num += value
let 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(blockNumber) {
Db.query(`SELECT id,hash FROM myzc WHERE coinname='${COIN}' AND \`status\`=0`, res => {
let 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 ${e.addtime - Date.parse(new Date()) / 1000 >= 10800 ? '' : `block=${blockNumber} 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