1. 使用web3.js查询
在以太坊中,想要查询交易详情,只能使用交易hash,调用web3.js
的方法web3.eth.getTransaction(transactionHash [, callback])
来查询。
官方示例:
web3.eth.getTransaction('0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b§234')
.then(console.log);
> {
"hash": "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b",
"nonce": 2,
"blockHash": "0xef95f2f1ed3ca60b048b4bf67cde2195961e0bba6f70bcbea9a2c4e133e34b46",
"blockNumber": 3,
"transactionIndex": 0,
"from": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b",
"to": "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f",
"value": '123450000000000000',
"gas": 314159,
"gasPrice": '2000000000000',
"input": "0x57cb2fc4"
}
2. 使用etherscan.io的API
而很多时候,我们希望根据某个账户地址或者合约地址,来查询相关交易详情。
最简单的,我们可以使用https://etherscan.io的相关API完成操作:
https://etherscan.io/apis#accounts
http://api.etherscan.io/api?module=account&action=txlist&address=0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae&startblock=0&endblock=99999999&sort=asc&apikey=YourApiKeyToken
但是这个接口有个限制:
The Etherscan Ethereum Developer APIs are provided as a community service and without warranty, so please just use what you need and no more. They support both GET/POST requests and a rate limit of 5 requests/sec (exceed and you will be blocked).
每秒最多仅支持5次请求。所以如果你的应用请求量很大,那这个方法就不适合你。
3. 自己同步区块数据写入数据库,通过数据库查询
由于我对Node.js和MongoDB都不熟,所以代码质量可能较差,主要是思路的分享
这篇文章主要就是介绍第三种方法,分为以下几个内容:
安装
MongoDB
编写同步区块的方法
编写查询交易的方法
3.1 安装MongoDB
下载+安装+配置环境,可以查看这篇文章在Mac上安装MongoDB
下载安装可视化数据库工具NoSQLBooster for MongoDB
3.2 编写同步区块的方法
同步区块内的交易,根据web3.js
的API,需要采取如下顺序:
获取当前的最新区块编号
web3.eth.getBlockNumber()
根据当前数据库区块编号和当前最新的区块编号,确定要获取的区块编号。例如数据库同步到了
Block500
,当前最新为Block600
,现在需要从Block501
开始同步,且Block
不能大于Block600
根据区块编号获取该区块内共有多少笔交易
web3.eth.getBlockTransactionCount(blockNum)
根据区块编号和交易的index获取交易详情
getTransactionFromBlock(hashStringOrNumber, indexNumber [, callback])
详细流程:
初始化数据库,插入一条数据,记录本地数据库的区块号和交易index,默认设为0
检查上一次本地最新区块下的交易是否同步完成,若未完成,先同步完该区块交易。
2.1 如果该区块之前已经同步完成,跳过。
2.2 如果该区块交易数为0,跳过。
检查主网最新区块号是否大于本地数据库最新区块号,如果大于,同步。
由于主网最新区块的交易可能会持续增加,只同步主网最新区块的前一个区块。
说明
不同步创世区块,同步时从第一个区块开始。
由于前面约120万区块没有交易,如果检测到某个区块没有交易,只更新数据库区块号。
请求方法设置全局变量,避免重复请求导致的重复同步。
每插入一条交易数据,同步更新本地数据库区块号和交易index。
关键代码
// 引入所需模块和配置
var koa = require('koa'),
app = new koa(),
Web3 = require('web3'),
MongoClient = require('mongodb').MongoClient,
moment = require('moment');
// 连接WEB3
web3 = new Web3(new Web3(config.url))
// 数据库url
databaseUrl: 'mongodb://localhost:27017/',
// 数据库名称
databaseName: 'blockTransationsDB',
// 数据表表名称
tableName: 'transations',
/ 获取当前最新区块的前一个区块数
async function getPrevBlockNum() {
// 获取当前最新的区块号
var currentBlockNum = await web3.eth.getBlockNumber();
// 获取前一个区块号
var prevBlockNum = currentBlockNum - 1;
console.log('===================================');
console.log('currentBlockNum: ' + currentBlockNum);
console.log('prevBlockNum: ' + prevBlockNum);
return prevBlockNum;
}
// 获取指定区块下所有交易的数量
async function getBlockTransactionCounts(blockNum) {
// 获取当前区块的前一个区块下的所有交易的数量
var transationCountByBlockNum = await
web3.eth.getBlockTransactionCount(blockNum);
// 如果返回的是null,提示错误中止执行后面的方法
if (transationCountByBlockNum == null) throw 'Error: The transationCountByBlockNum is null, please try again later.';
console.log('blockNum: ' + blockNum + ' transationCountByBlockNum: ' + transationCountByBlockNum);
return transationCountByBlockNum;
}
// 向数据库插入交易记录
function insertTransationData(response, blockNum, index) {
return new Promise((resolve, reject) => {
MongoClient.connect(config.databaseUrl, function(err, db) {
if (err) reject(err);
var dbo = db.db(config.databaseName);
// // 插入单条数据
dbo.collection(config.tableName).insertOne(response, function(err, res) {
if (err) reject(err);
console.log("Insert success! BlockNum: " + blockNum + ' index: ' + index);
db.close();
resolve();
});
});
});
}
// 初始化数据库,插入第一条数据,将dataBaselocalBlockNumber和dataBaselocalTransationIndex设为0
function initDatabaseData() {
return new Promise((resolve, reject) => {
MongoClient.connect(config.databaseUrl, function(err, db) {
if (err) reject(err);
var dbo = db.db(config.databaseName);
var initData = { dataBaselocalBlockNumber: 0, dataBaselocalTransationIndex: 0 };
dbo.collection(config.tableName).insertOne(initData, function(err, res) {
if (err) reject(err);
console.log("Init database data success.");
db.close();
resolve();
});
});
});
}
// 更新数据库的dataBaselocalBlockNumber和dataBaselocalTransationIndex
function updateDataBaselocalBlockNumber(localBlockNumber, transationIndex) {
return new Promise((resolve, reject) => {
MongoClient.connect(config.databaseUrl, function(err, db) {
if (err) reject(err);
var dbo = db.db(config.databaseName);
var whereStr = {
dataBaselocalBlockNumber: {
$exists: true
}
}
var updateStr = {
$set: {
dataBaselocalBlockNumber: localBlockNumber,
dataBaselocalTransationIndex: transationIndex
}
};
dbo.collection(config.tableName).updateOne(whereStr, updateStr, function(err, res) {
if (err) reject(err);
console.log("Update dataBaselocalBlockNumber: " + localBlockNumber +
" dataBaselocalTransationIndex: " + transationIndex);
db.close();
resolve();
});
});
})
}
// 获取数据库记录的已同步的区块号
function getLatestLocalBlockNum() {
return new Promise((resolve, reject) => {
MongoClient.connect(config.databaseUrl, function(err, db) {
if (err) reject(err);
var dbo = db.db(config.databaseName);
dbo.collection(config.tableName).find({
dataBaselocalBlockNumber: {
$exists: true
}
}).toArray(async function(err, result) {
if (err) reject(err);
var localBlockNumber;
var localTransationIndex;
// 先判断数据库的blockNum字段和transationIndex字段是否有值,没有的话都设为0
if (result.length === 0) {
// 不同步创世区块(block[0])
localBlockNumber = 0;
localTransationIndex = 0;
} else {
// 如果有,取值
localBlockNumber = result[0].dataBaselocalBlockNumber;
localTransationIndex = result[0].dataBaselocalTransationIndex;
}
console.log('Latest localBlockNumber: ' + localBlockNumber +
' & localTransationIndex ' + localTransationIndex);
db.close();
resolve(localBlockNumber);
});
});
})
}
// 获取数据库记录的已同步的区块号的交易index
function getLatestLocalTransationIndex() {
return new Promise((resolve, reject) => {
MongoClient.connect(config.databaseUrl, function(err, db) {
if (err) reject(err);
var dbo = db.db(config.databaseName);
dbo.collection(config.tableName).find({
dataBaselocalTransationIndex: {
$exists: true
}
}).toArray(async function(err, result) {
if (err) reject(err);
var localBlockNumber;
var localTransationIndex;
// 先判断数据库的blockNum字段和transationIndex字段是否有值,没有的话都设为0
if (result.length === 0) {
// 不同步创世区块(block[0])
localBlockNumber = 0;
localTransationIndex = 0;
} else {
// 如果有,取值
localBlockNumber = result[0].dataBaselocalBlockNumber;
localTransationIndex = result[0].dataBaselocalTransationIndex;
}
console.log('localTransationIndex ' + localTransationIndex);
db.close();
resolve(localTransationIndex);
});
});
})
}
router.get('/transation/syncTransations', async(ctx, next) => {
// 防止重复请求重复同步
if (tools.isSync) {
console.log('============================================================');
console.log('The Sync is in progress... ...');
console.log('============================================================');
return;
}
// 如果数据库中不存在本地区块号,先初始化数据库区块号数据
console.log('============================================================');
console.log('=====> Checking database data has init?');
var localBlockNumber = await getLatestLocalBlockNum();
if (localBlockNumber == 0) {
console.log('The local blockNum in the database does not exist.');
await initDatabaseData();
} else {
console.log('The database data has already init before.');
}
console.log('============================================================');
// 判断上一次本地最新区块下的交易是否同步完成,若未完成,先同步完该区块交易
console.log('============================================================');
console.log('=====> Checking last local block transations has complete sync?');
var transactionCount = await getBlockTransactionCounts(localBlockNumber);
console.log('localBlockNumber[' + localBlockNumber + ']' + ' transactionCount on mainNet is ' + transactionCount);
// 先查看该区块中是否有交易
if (transactionCount == 0) {
console.log('Block ' + localBlockNumber + ' has no transation data.');
} else {
// 如果有,判断是否需要继续同步
if ((transactionCount - 1) > await getLatestLocalTransationIndex()) {
console.log('Not all of transations, Continue sync block transations... ...');
tools.isSync = true;
// 从最后一条交易记录的下一条开始获取并插入
for (i = (await getLatestLocalTransationIndex() + 1); i < transactionCount; i++) {
await web3.eth.getTransactionFromBlock(localBlockNumber, i).then(async response => {
// 插入交易数据
await insertTransationData(response, localBlockNumber, i);
// 更新数据库的dataBaselocalBlockNumber和dataBaselocalTransationIndex
await updateDataBaselocalBlockNumber(localBlockNumber, i);
console.log(moment().format('LLLL'));
})
}
console.log('Complete sync block ' +
localBlockNumber + ' of ' + transactionCount + ' transations.');
tools.isSync = false;
} else {
console.log('No need sync.Local block has full of ' + transactionCount + ' transations.');
}
}
console.log('============================================================');
// 比较主网和本地的区块号并决定是否进行同步
console.log('============================================================');
console.log('=====> Checking local block is the latested?');
var nextLocalBlockNumber;
var blockTransactionCount;
while (await getPrevBlockNum() > await getLatestLocalBlockNum()) {
console.log('Not the latested, Prepare Sync......');
tools.isSync = true;
// 获取数据库最新一条交易记录的区块号的下一个区块号
nextLocalBlockNumber = (await getLatestLocalBlockNum()) + 1;
console.log('nextLocalBlockNumber: ' + nextLocalBlockNumber);
// 获取该区块下的交易数量
blockTransactionCount = await getBlockTransactionCounts(nextLocalBlockNumber);
// 判断区块内是否包含交易,如果为0,只需要更新下数据库的区块号
if (blockTransactionCount == 0) {
console.log('Block ' + nextLocalBlockNumber + ' has no transation data.');
await updateDataBaselocalBlockNumber(nextLocalBlockNumber, 0);
} else {
console.log('Block ' + nextLocalBlockNumber + ' isSyncing......');
// 获取该区块下的交易数据
for (var i = 0; i < blockTransactionCount; i++) {
await web3.eth.getTransactionFromBlock(nextLocalBlockNumber, i).then(async response => {
// 插入交易数据
await insertTransationData(response, nextLocalBlockNumber, i);
// 更新数据库的dataBaselocalBlockNumber和dataBaselocalTransationIndex
await updateDataBaselocalBlockNumber(nextLocalBlockNumber, i);
console.log(moment().format('LLLL'));
})
}
}
}
tools.isSync == false;
console.log('The current local block is the latest.');
console.log('Finish Sync.');
console.log('============================================================');
})
3.3 编写查询交易的方法
// 在数据库中查询指定账号的交易
function queryAccountTransationData(account) {
return new Promise((resolve, reject) => {
MongoClient.connect(config.databaseUrl, function(err, db) {
if (err) reject(err);
var dbo = db.db(config.databaseName);
var whereStr = {
$or: [{
from: account
}, {
to: account
}]
};
dbo.collection(config.tableName).find(whereStr).toArray(function(err, result) {
if (err) reject(err);
console.log('Transations by account ' + account + ':');
console.log(result);
db.close();
resolve(result);
});
});
})
}
// 根据账号地址从数据库中查询交易:from or to
// http://0.0.0.0:8084/transation/getTransationsByAccount?account=0xcE85247b032f7528bA97396F7B17C76D5D034D2F
router.get('/transation/getTransationsByAccount', async(ctx, next) => {
if (!ctx.request.query.account) {
ctx.body = await Promise.resolve({
code: 20002,
data: {},
message: 'account 必须是一个钱包地址',
})
}
var transations = await queryAccountTransationData(ctx.request.query.account);
ctx.body = await Promise.resolve({
code: 10000,
transations: transations,
message: 'ok',
})
})