本文介绍了如何使用QuickNode SDK和JavaScript对EVM兼容链(如Ethereum和Avalanche)进行钱包地址的彻底审计。提供了详细的步骤和代码示例,包括如何提取ERC20、ERC721和ERC1155代币的交易活动,涵盖了项目设置、库依赖、功能实现及结果检查。
在区块链技术的动态环境中,审计公司面临着高效跟踪和分析区块链网络上交易的关键任务。这一需求在税务评估或处理金融监管机构的询问时尤为突出。传统的访问区块链数据的方法可能复杂且耗时。为了解决这个问题,我们开发了一份全面的指南,利用 QuickNode SDK 与 JavaScript,特别为 EVM 兼容链设计,包括以太坊、Polygon 和 Avalanche。虽然这个过程变得更加高效,但信息的来源仍然是区块链本身。QuickNode SDK 只是一个接口,用于对我们的端点进行 JSON-RPC 调用。本指南简化了对钱包地址进行深入审计的过程。
我们的逐步指南旨在优化审计公司访问和解释区块链交易的方式。对于那些开始进行区块链数据分析的审计公司,无论是为了日常合规检查还是回应监管要求,这份指南都是一项重要资源。QuickNode 致力于提高区块链的可访问性,在我们的持续支持和定制解决方案中体现了这一承诺。
如果在遵循本指南时遇到任何挑战,或需要针对区块链数据检索的个性化帮助,QuickNode 团队随时为你提供服务。请通过 结论 部分中的反馈表与我们联系以获得专家指导和支持。
在本指南中,你将获取与钱包关联的所有 ERC20、ERC721 和 ERC1155 交易活动,包括:
在开始之前,具备以下信息会有所帮助,但如果没有也没关系。我们将展示项目的安装步骤以及在 QuickNode 上获取端点的阶段。
依赖 | 版本 |
---|---|
node.js | ^16 |
@quicknode/sdk | ^1.1.4 |
fs-extra | ^11.1.1 |
cli-progress | ^3.12.0 |
你需要 API 端点以与以太坊网络交互,从而查询区块链上的数据。在 这里 创建一个免费的 QuickNode 账户,然后登录并点击 创建端点 按钮,根据你的偏好选择链和网络。
在本指南中,我们将逐步演示如何从 Avalanche C-chain 获取代币活动。然而,本指南中的代码适用于所有 EVM 兼容链,如以太坊、Polygon 和 Arbitrum。无论你想要使用哪个网络,只需在创建端点后使用该 HTTP Provider 链接即可。不需要额外更改。
创建端点后,复制 HTTP Provider 链接并妥善保管。
在深入编码之前,是时候了解一下本项目中使用的库及其功能了。
@quicknode/sdk:提供易于使用的函数,以便无需手动与区块链或智能合约交互即可获取代币元数据、余额和转移历史
fs-extra:用于文件操作的文件系统模块,例如写入文件
cli-progress:用于控制台可视化进度反馈的 CLI 进度条模块
现在,让我们开始构建项目。
创建一个项目目录并切换到该目录:
mkdir tokenActivities
cd tokenActivities
然后,用默认选项初始化一个 npm 项目:
npm init --y
要安装这些库,我们将使用 Node.js 包管理器 npm。
npm install @quicknode/sdk cli-progress fs-extra
注意:完成此步骤后,请在新创建的 package.json 文件中插入 "type": "module"
。这将启用 ES 模块语法。
所以,你的 package.json 文件应类似于以下内容。
{
"name": "tokenActivities",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@quicknode/sdk": "^1.1.4",
"cli-progress": "^3.12.0",
"fs-extra": "^11.1.1"
}
}
现在,我们应该在我们的项目目录中安装了库。
创建一个名为 index.js 的 JavaScript 文件,使用你喜欢的代码编辑器(例如 VS Code)打开它,并将以下代码粘贴到其中。将代码片段中高亮显示的 QUICKNODE_ENDPOINT 占位符替换为你的端点的 HTTP Provider 链接。
// 导入必要的模块和库
import { Core, viem } from "@quicknode/sdk"; // 从 QuickNode SDK 导入 Core 和 viem
import fs from "fs-extra"; // 导入文件系统模块以进行文件操作
import * as cli from "cli-progress"; // 导入 CLI 进度条模块以在控制台中提供可视进度反馈
// 创建 QuickNode SDK 的 Core 实例
const core = new Core({
endpointUrl: 'QUICKNODE_ENDPOINT', // 你的 QuickNode 的端点 URL。将 "QUICKNODE_ENDPOINT" 替换为你的实际 QuickNode 端点 URL。
})
// 获取特定地址在给定区块内的 ERC20 代币转移的函数
async function getERC20TokenTransfers(address, blockNum) {
// 第 1 步
}
// 获取特定地址在给定区块内的 ERC721 代币转移的函数
async function getERC721TokenTransfers(address, blockNum) {
// 第 2 步
}
// 获取特定地址在给定区块内的 ERC1155 代币转移的函数
async function getERC1155TokenTransfers(address, blockNum) {
// 第 3 步
}
// 将转移事件日志解析为更易读格式的函数
function parseTransferEvents(events) {
// 第 4 步
}
// 获取特定交易哈希的内部交易的函数
async function getInternalTransactions(txHash) {
// 第 5 步
}
// 获取特定地址在区块范围内和给定代币类型的交易的函数
async function getTransactionsForAddresses(
addresses,
fromBlock,
toBlock,
tokenTypes
) {
// 第 6 步
}
// 检查并验证输入变量:地址、区块编号和代币类型的函数
function checkVariables(addresses, fromBlock, toBlock, tokenTypes) {
// 第 7 步
}
// 运行交易获取过程的主函数
async function run(addresses, fromBlock, toBlock, tokenTypes) {
// 第 8 步
}
为避免混淆,我们将首先讨论函数的逻辑。然后,我们将逐步填充函数。
getERC20TokenTransfers
- 获取 ERC20 转移:获取指定地址在特定区块内的所有 ERC20 代币转移。
getERC721TokenTransfers
- 获取 ERC721 转移:获取指定地址在特定区块内的所有 ERC721 代币转移。
getERC1155TokenTransfers
- 获取 ERC1155 转移:获取指定地址在特定区块内的所有 ERC1155 代币转移。
parseTransferEvents
- 解析转移事件:解析这些转移事件以将区块链数据转换为更易读和可解读的格式,从而增强审计结果的清晰度。
getInternalTransactions
- 提取内部交易:获取与特定交易哈希关联的内部交易,揭示区块链内发生交互的深度。
getTransactionsForAddresses
- 收集地址的交易:核心功能是收集指定地址在一系列区块和代币类型上的交易,确保全面和详细的审计。上述提到的所有功能都在此函数中执行。
checkVariables
- 验证输入:在数据提取之前,验证步骤确认输入变量如地址、区块编号和代币类型的完整性和格式,确保审计过程的准确性和可靠性。
run
- 运行审计过程:最后,主函数是运行整个交易获取操作。检查变量、获取交易并将所有结果写入文件的操作都在 run
函数中执行。
此函数 getERC20TokenTransfers
设计用于从区块链中检索特定地址在指定区块内的 ERC20 代币转移事件。它分别查询从该地址发送的转移和收到的转移,然后解析这些事件日志以提取详细的转移信息。 parseTransferEvents
函数(此处未显示)负责解释日志数据并将其转换为更用户友好的格式。
用以下代码替换 getERC20TokenTransfers
函数。检查注释以获取更详细的信息。
// 获取特定地址在给定区块内的 ERC20 代币转移的函数
async function getERC20TokenTransfers(address, blockNum) {
const transfers = []; // 用于存储转移的数组
// 将区块编号转换为十六进制格式
const blockHex = viem.toHex(blockNum);
// 获取从指定地址发送的代币转移日志
const sentTransfers = await core.client.getLogs({
fromBlock: blockHex,
toBlock: blockHex,
event: viem.parseAbiItem(
"event Transfer(address indexed from, address indexed to, uint256 value)"
),
args: { from: address },
strict: true,
});
// 解析已发送代币转移的事件
let parsedEvents = parseTransferEvents(sentTransfers);
// 将已解析的已发送转移添加到 transfers 数组
transfers.push(...parsedEvents);
// 获取指定地址接收的代币转移日志
const receivedTransfers = await core.client.getLogs({
fromBlock: blockHex,
toBlock: blockHex,
event: viem.parseAbiItem(
"event Transfer(address indexed from, address indexed to, uint256 value)"
),
args: { to: address },
strict: true,
});
// 解析已接收代币转移的事件
parsedEvents = parseTransferEvents(receivedTransfers);
// 将已解析的已接收转移添加到 transfers 数组
transfers.push(...parsedEvents);
return transfers; // 返回已发送和已接收转移的综合列表
}
在此函数 getERC721TokenTransfers
中,流程与获取 ERC20 代币转移类似,但针对 ERC721(NFT)转移进行了调整。它查询从 ERC721 合约发出的 Transfer 事件,指示 NFT 的转移。该函数处理从指定地址发送和接收的转移。它使用 parseTransferEvents
(此处未提供)来解释日志数据为更易读的格式,考虑到 ERC721 转移包括 tokenId 以表示单个 NFT。
用以下代码替换 getERC721TokenTransfers
函数。检查注释以获取更详细的信息。
// 获取特定地址在给定区块内的 ERC721 代币转移的函数
async function getERC721TokenTransfers(address, blockNum) {
const transfers = []; // 用于存储转移的数组
// 将区块编号转换为十六进制格式
const blockHex = viem.toHex(blockNum);
// 获取指定地址发送的 ERC721 代币转移日志
const sentTransfers = await core.client.getLogs({
fromBlock: blockHex,
toBlock: blockHex,
event: viem.parseAbiItem(
"event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)"
),
args: { from: address },
strict: true,
});
// 解析已发送的转移事件
let parsedEvents = parseTransferEvents(sentTransfers);
// 将已解析的已发送转移添加到 transfers 数组
transfers.push(...parsedEvents);
// 获取指定地址接收的 ERC721 代币转移日志
const receivedTransfers = await core.client.getLogs({
fromBlock: blockHex,
toBlock: blockHex,
event: viem.parseAbiItem(
"event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)"
),
args: { to: address },
strict: true,
});
// 解析已接收的转移事件
parsedEvents = parseTransferEvents(receivedTransfers);
// 将已解析的已接收转移添加到 transfers 数组
transfers.push(...parsedEvents);
return transfers; // 返回已发送和已接收转移的综合列表
}
在此函数 getERC1155TokenTransfers
中,流程专门针对 ERC1155 代币转移进行了调整,这可以包括单次和批量转移。它查询 ERC1155 合约发出的 TransferSingle 和 TransferBatch 事件。该函数处理从指定地址发送和接收的转移。它使用 parseTransferEvents
(未提供此函数)来解释日志数据,考虑到 ERC1155 转移的独特特性,包括涉及多个代币 ID 和数量的批量转移。
用以下代码替换 getERC1155TokenTransfers
函数。检查注释以获取更详细的信息。
// 获取特定地址在给定区块内的 ERC1155 代币转移的函数
async function getERC1155TokenTransfers(address, blockNum) {
const transfers = []; // 用于存储转移的数组
// 将区块编号转换为十六进制格式
const blockHex = viem.toHex(blockNum);
// 获取从指定地址发送的 ERC1155 代币转移日志(单次转移)
let sentTransfers = await core.client.getLogs({
fromBlock: blockHex,
toBlock: blockHex,
event: viem.parseAbiItem(
"event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value)"
),
args: { _from: address },
strict: true,
});
// 解析已发送的单次代币转移事件
let parsedEvents = parseTransferEvents(sentTransfers);
transfers.push(...parsedEvents);
// 获取从指定地址发送的 ERC1155 代币转移日志(批量转移)
sentTransfers = await core.client.getLogs({
fromBlock: blockHex,
toBlock: blockHex,
event: viem.parseAbiItem(
"event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values)"
),
args: { _from: address },
strict: true,
});
// 解析已发送的批量代币转移事件
parsedEvents = parseTransferEvents(sentTransfers);
transfers.push(...parsedEvents);
// 获取指定地址接收的 ERC1155 代币转移日志(单次转移)
let receivedTransfers = await core.client.getLogs({
fromBlock: blockHex,
toBlock: blockHex,
event: viem.parseAbiItem(
"event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value)"
),
args: { _to: address },
strict: true,
});
// 解析已接收的单次代币转移事件
parsedEvents = parseTransferEvents(receivedTransfers);
transfers.push(...parsedEvents);
// 获取指定地址接收的 ERC1155 代币转移日志(批量转移)
receivedTransfers = await core.client.getLogs({
fromBlock: blockHex,
toBlock: blockHex,
event: viem.parseAbiItem(
"event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values)"
),
args: { _to: address },
strict: true,
});
// 解析已接收的批量代币转移事件
parsedEvents = parseTransferEvents(receivedTransfers);
transfers.push(...parsedEvents);
return transfers; // 返回已发送和已接收转移的综合列表
}
此函数接收事件日志的数组(events)并返回一个新数组,其中每个事件日志被转换为包含更可读格式的对象。每个对象包含有关事件的详细信息,例如合约地址、转移值、主题、交易细节等。 viem.fromHex 函数用于将十六进制值转换为数字,使其更容易解读。此函数对理解区块链事件日志中的数据至关重要。
用以下代码替换 parseTransferEvents
函数。检查注释以获取更详细的信息。
// 将转移事件日志解析为更易读格式的函数
function parseTransferEvents(events) {
// 将每个事件映射到格式化对象
return events.map((event) => ({
contractAddress: event.address, // 发出事件的合约地址
value: event.data === "0x" ? "0x" : viem.fromHex(event.data, "number"), // 转移的值,如果不是零则从十六进制转换为数字
topics: event.topics.map((topic) => topic), // 索引事件参数
data: event.data, // 事件日志的数据字段
args: event.args, // 日志的参数(解码后的参数)
blockNumber: event.blockNumber, // 记录事件的区块编号
logIndex: event.logIndex, // 该日志在区块内的索引
transactionIndex: event.transactionIndex, // 该交易在区块中的索引
transactionHash: event.transactionHash, // 交易的哈希
blockHash: event.blockHash, // 包含交易的区块哈希
removed: event.removed, // 一个位,表示该日志是否因链重组而被移除
}));
}
在此函数 getInternalTransactions
中,使用特定交易的哈希(txHash)向端点发起请求以跟踪该交易。该追踪通过 debug_traceTransaction
方法获取,提供有关交易执行的详细信息,包括内部交易。这些内部交易从调用追踪中提取并由该函数返回。包括错误处理以捕获并记录在追踪请求过程中发生的任何问题,如果遇到错误则返回空数组。
用以下代码替换 getInternalTransactions
函数。检查注释以获取更详细的信息。
// 获取特定交易哈希的内部交易的函数
async function getInternalTransactions(txHash) {
try {
// 使用 debug_traceTransaction 方法请求该交易的追踪
const traceResponse = await core.client.request({
method: "debug_traceTransaction",
params: [txHash, { tracer: "callTracer" }], // 使用调用追踪器以获取详细的交易执行信息
});
const internalTxs = [];
// 检查 traceResponse 对象是否具有 'calls' 属性
if (Object.prototype.hasOwnProperty.call(traceResponse, "calls")) {
const result = traceResponse.calls; // 从响应中提取调用追踪
// 将调用追踪结果添加到内部交易数组
// 调用追踪的结构确定如何提取内部交易
internalTxs.push(...result);
}
return internalTxs; // 返回解析后的内部交易
} catch (error) {
// 记录并处理在请求中发生的任何错误
console.error("发生错误:", error);
return []; // 在出现错误的情况下返回空数组
}
}
此函数 getTransactionsForAddresses
在特定区块范围内处理给定地址和代币类型的交易。它编译有关每个交易的详细信息,包括 ERC20、ERC721 和 ERC1155 代币的转移以及任何内部交易。使用进度条提供处理进度的可视指示。
用以下代码替换 getTransactionsForAddresses
函数。检查注释以获取更详细的信息。
// 获取特定地址在区块范围内和给定代币类型的交易的函数
async function getTransactionsForAddresses(
addresses,
fromBlock,
toBlock,
tokenTypes
) {
// 初始化进度条
const bar1 = new cli.SingleBar({}, cli.Presets.shades_classic);
// 开始进度条
bar1.start(toBlock - fromBlock, 0);
const transactions = []; // 用于存储交易的数组
// 循环遍历指定范围内的每个区块
for (let blockNum = fromBlock; blockNum <= toBlock; blockNum++) {
// 如果是最后一个区块停止进度条,否则递增
blockNum === toBlock ? bar1.stop() : bar1.increment();
// 获取区块及其交易
const block = await core.client.getBlock({
blockNumber: blockNum,
includeTransactions: true,
});
// 处理区块内的每个交易
for (const tx of block.transactions) {
// 检查交易是否涉及任何指定地址
if (
(tx.from && addresses.includes(viem.getAddress(tx.from))) ||
(tx.to && addresses.includes(viem.getAddress(tx.to)))
) {
// 初始化交易细节对象
const txDetails = {
block: blockNum,
hash: tx.hash,
from: viem.getAddress(tx.from),
to: viem.getAddress(tx.to),
value: tx.value,
gas: tx.gas,
gasPrice: tx.gasPrice,
input: tx.input,
internalTransactions: [],
};
let typeTransfers = []; // 用于存储代币转移的数组
// 检查发送地址是否是指定地址之一
const isSender = addresses.includes(viem.getAddress(tx.from));
// 处理每种代币类型
for (const tokenType of tokenTypes) {
// 根据代币类型及地址类型(发送方或接收方)获取代币转移
if (isSender) {
// 处理基于发送地址的特定代币类型的转移
switch (tokenType) {
case "ERC20":
typeTransfers = await getERC20TokenTransfers(tx.from, blockNum);
break;
case "ERC721":
typeTransfers = await getERC721TokenTransfers(
tx.from,
blockNum
);
break;
case "ERC1155":
typeTransfers = await getERC1155TokenTransfers(
tx.from,
blockNum
);
break;
default:
throw new Error("没有支持的代币类型。");
}
} else {
// 处理基于接收地址的特定代币类型的转移
switch (tokenType) {
case "ERC20":
typeTransfers = await getERC20TokenTransfers(tx.to, blockNum);
break;
case "ERC721":
typeTransfers = await getERC721TokenTransfers(tx.to, blockNum);
break;
case "ERC1155":
typeTransfers = await getERC1155TokenTransfers(tx.to, blockNum);
break;
default:
throw new Error("没有支持的代币类型。");
}
}
// 将获取的代币转移添加到交易细节中
if (typeTransfers.length) {
// 如果代币类型在 txDetails 中尚不存在,则初始化
if (!Object.prototype.hasOwnProperty.call(txDetails, tokenType)) {
txDetails[tokenType] = { tokenTransfers: [] };
}
// 将转移添加到 txDetails 中对应的代币类型中
txDetails[tokenType].tokenTransfers.push(...typeTransfers);
}
}
// 如果适用,则获取并添加内部交易
const bytecode = await core.client.getBytecode({
address: tx.to,
});
if (tx.to && bytecode !== "0x") {
txDetails.internalTransactions.push(
...(await getInternalTransactions(tx.hash))
);
}
// 将详细交易添加到交易数组中
transactions.push(txDetails);
}
}
}
return transactions; // 返回收集到的交易
}
在此函数中:
这些验证确保输入函数或过程格式正确且逻辑一致,以便在进一步处理或查询区块链之前。
用以下代码替换 checkVariables
函数。检查注释以获取更详细的信息。
// 检查并验证输入变量:地址、区块编号和代币类型的函数
function checkVariables(addresses, fromBlock, toBlock, tokenTypes) {
//遍历每个地址并检查其是否为有效的 EVM 兼容地址
addresses.forEach((address) => {
if (!viem.isAddress(address)) {
throw new Error(
`地址 (${address}) 不是 EVM 兼容的。请检查地址。`
);
}
});
// 检查 'fromBlock' 和 'toBlock' 是否为整数
if (!Number.isInteger(fromBlock) || !Number.isInteger(toBlock)) {
throw new Error("区块编号必须为整数。");
}
// 检查 'fromBlock' 不大于 'toBlock'
if (fromBlock > toBlock) {
throw new Error("最后一个区块必须大于第一个区块。");
}
// 定义有效的代币类型
const validTokenTypes = ["ERC20", "ERC721", "ERC1155"];
// 检查 'tokenTypes' 中的所有元素是否为有效代币类型
if (!tokenTypes.every((tokenType) => validTokenTypes.includes(tokenType))) {
throw new Error(
`无效的代币类型: ${tokenTypes}。必须为 ${validTokenTypes.join(
", "
)} 之一。`
);
}
}
在此 run
函数中:
用以下代码替换 run
函数。检查注释以获取更详细的信息。
// 运行交易获取过程的主函数
async function run(addresses, fromBlock, toBlock, tokenTypes) {
try {
// 检查输入变量是否有效
checkVariables(addresses, fromBlock, toBlock, tokenTypes);
// 将所有地址转换为安全格式以确保兼容性
const checksummedAddresses = addresses.map((address) =>
viem.getAddress(address)
);
// 为给定地址、区块范围和代币类型获取交易
const transactions = await getTransactionsForAddresses(
checksummedAddresses,
fromBlock,
toBlock,
tokenTypes
);
// 定义一个用于 JSON.stringify 的替换器函数以处理大整数
const replacer = (key, value) =>
typeof value === "bigint" ? Number(value) : value;
// 输出文件路径
const outputFilePath = "wallet_audit_data.json";
// 将交易对象转换为带缩进的 JSON 字符串以提高可读性
const stringified = JSON.stringify(transactions, replacer, 4);
// 将 JSON 字符串写入指定文件
fs.writeFileSync(outputFilePath, stringified);
console.log("数据已保存到 " + outputFilePath);
} catch (error) {
console.error("发生错误:", error);
}
}
在所有函数定义后,剩下的就是调用 run
函数,传入审计的钱包地址、感兴趣的区块范围和代币标准类型。
将以下代码添加到文件末尾。我们选择一个随机地址,其中有一个包括 ERC20 和 ERC721 转移的交易,位于 Avalanche C-chain 上。
// 使用示例
// run(["address1", "address2"], fromBlock, toBlock, ["tokenStandard1", "tokenStandard2"]);
run(['0xe2233D97f30745fa5f15761B81B281BE5959dB5C'], 38063215, 38063220, [\
'ERC20',\
'ERC721',\
])
现在,保存 index.js 文件,并在终端中运行以下命令。
node index.js
执行后,你将看到类似如下的控制台输出,并在同一文件夹中找到一个 wallet_audit_data.json
文件,包含钱包的整个活动记录,包括交易、代币转移和内部交易。
> node indexNfts.js
████████████████████████████████████████ 100% | ETA: 0s | 5/5
数据已保存到 wallet_audit_data.json
以下是该程序中找到的交易的详细信息(在左侧)和结果文件的视图(在右侧)。看起来我们的程序已成功检查和找到交易。
如果你想查看我们完整的代码,请访问我们的 GitHub 页面 这里。
恭喜你!你现在能够在任何 EVM 兼容链上对任何钱包地址进行彻底审计,针对 ERC20、ERC721 和 ERC1155 代币转移!如前所述,如果你想使用不同的网络,只需更改端点即可。
如需了解更多关于 QuickNode 如何帮助审计公司以保证数据完整性和准确性从区块链中提取这种数据,请随时通过以下反馈表与我们联系。
让我们知道 如果你有任何反馈或新主题请求。我们非常希望听到你的意见。
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!