交易日志事件解码:使钱包活动易于理解
介绍
在本文中,我将解释如何将与加密钱包相关的原始交易数据转换为有关钱包活动的清晰、人类可读的信息。
我们将研究流行的 web3 产品(钱包、投资组合追踪器等)如何可视化复杂的钱包活动信息的最佳示例,然后我们将在 typescript 上实现负责为此类可视化准备数据的逻辑(带有代码的 github 存储库)。
但首先,我们需要熟悉 EVM 区块链中的事件和日志理论。
事务事件和日志 101
我不会深入探讨这个理论;关于事件和日志的理论,有一篇很棒的文章(“关于以太坊事件和日志,你想知道的一切”)。我只会对这个理论做一个简短的总结。
活动
事件是 EVM 智能合约在交易执行期间可以发出的日志实体。
事件非常善于指示某个操作已在链上发生。
应用程序可以订阅并监听事件以触发某些链下逻辑,或者将事件索引、转换并存储在某些链下存储中(例如The Graph 协议或以太坊 ETL)。
让我们看看Open Zeppelin 的 ERC20 代币合约的事件:
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}
使用 emit 关键字打开 Zeppelin 的 ERC20 合约调用/创建事件:
abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
/**
* @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from`
* (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
* this function.
*
* Emits a {Transfer} event.
*/
function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
// Overflow check required: The rest of the code assumes that totalSupply never overflows
_totalSupply += value;
} else {
uint256 fromBalance = _balances[from];
if (fromBalance < value) {
revert ERC20InsufficientBalance(from, fromBalance, value);
}
unchecked {
// Overflow not possible: value <= fromBalance <= totalSupply.
_balances[from] = fromBalance - value;
}
}
if (to == address(0)) {
unchecked {
// Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply.
_totalSupply -= value;
}
} else {
unchecked {
// Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256.
_balances[to] += value;
}
}
emit Transfer(from, to, value);
}
}
日志
当智能合约发出事件时,事件名称及其参数会存储在交易的日志实体中。您
可以使用区块链节点的 JSON-RPC 或自定义区块链数据提供程序读取日志。
带有事件日志的交易收据(例如 ERC-20 转账交易)将如下所示
{
"jsonrpc":"2.0",
"id":1,
"result":{
"transactionHash":"0xa81b0b764ea32179b29c1098378992bed1b9a53b04f180393f0438d02da1687e",
"blockHash":"0x93dbccc61ff09a4ba65dacf26c35d2035373aca9453c65f65523ce9cb8700b41",
"blockNumber":"0x115f5a9",
"logs":[
{
"transactionHash":"0xa81b0b764ea32179b29c1098378992bed1b9a53b04f180393f0438d02da1687e",
"address":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"blockHash":"0x93dbccc61ff09a4ba65dacf26c35d2035373aca9453c65f65523ce9cb8700b41",
"blockNumber":"0x115f5a9",
"data":"0x0000000000000000000000000000000000000000000000000000000ba43b7400",
"logIndex":"0x178",
"removed":false,
"topics":[
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x0000000000000000000000004a7c6899cdcb379e284fbfd045462e751da4c7ce",
"0x000000000000000000000000f1ddf1fc0310cb11f0ca87508207012f4a9cb336"
],
"transactionIndex":"0x67"
}
],
"contractAddress":null,
"effectiveGasPrice":"0x1e6d855cc",
"cumulativeGasUsed":"0x9717a6",
"from":"0x4a7c6899cdcb379e284fbfd045462e751da4c7ce",
"gasUsed":"0x10059",
"logsBloom":"0x00000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000008000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000010000000000008000000000000000000000020000000000000010000000000000000000000000000000000200000000000000000000000000000000000000000000000000020000002000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000",
"status":"0x1",
"to":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"transactionIndex":"0x67",
"type":"0x2"
}
}
interface Log {
blockNumber: number;
blockHash: string;
transactionIndex: number;
removed: boolean;
address: string;
data: string;
topics: Array<string>;
transactionHash: string;
logIndex: number;
}
名为“logs”的字段包含事务执行期间发出的每个事件的日志对象。
事件如何存储在日志中(ABI、事件签名、主题、数据)
合约编译过程中会生成一个特殊的制品,称为合约的 ABI。ABI
是该合约接口(构造函数、方法和事件的描述)的底层表示。
我们来看看ERC20标准中的Transfer事件,该事件在ABI中是这样的。
{
"anonymous":false,
"inputs":[
{
"indexed":true,
"internalType":"address",
"name":"from",
"type":"address"
},
{
"indexed":true,
"internalType":"address",
"name":"to",
"type":"address"
},
{
"indexed":false,
"internalType":"uint256",
"name":"value",
"type":"uint256"
}
],
"name":"Transfer",
"type":"event"
}
为了在日志中识别和存储特定事件,它们会被散列成“事件签名”。例如,Transfer 事件的签名是Transfer(address,address,uint256)
(事件名称,然后将其与输入类型配对)。
此签名经过哈希运算 (keccak256) 后的结果0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
,可在日志的“主题”(始终为第一个主题)中找到。
带索引的参数(例如 from 和 to)也存储在主题中,而未带索引的参数(例如 value)则存储在数据中。
如果我们查看示例 ERC-20 传输交易的日志对象,我们会发现散列事件是第一个主题。
{
"transactionHash":"0xa81b0b764ea32179b29c1098378992bed1b9a53b04f180393f0438d02da1687e",
"address":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"blockHash":"0x93dbccc61ff09a4ba65dacf26c35d2035373aca9453c65f65523ce9cb8700b41",
"blockNumber":"0x115f5a9",
// encoded value amount
"data":"0x0000000000000000000000000000000000000000000000000000000ba43b7400",
"logIndex":"0x178",
"removed":false,
"topics":[
// hashed event signature
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
//encoded from/sender address
"0x0000000000000000000000004a7c6899cdcb379e284fbfd045462e751da4c7ce",
//encoded to/recipient address
"0x000000000000000000000000f1ddf1fc0310cb11f0ca87508207012f4a9cb336"
],
"transactionIndex":"0x67"
}
使用合约 ABI 和Ethers.js之类的库可以解码这些数据,从而简化了这个过程。
如果您想深入了解,可以阅读“关于以太坊事件和日志的一切”文章中的“如何从日志中解码事件”段落,了解以太坊是如何做到这一点的。
交易可视化示例
事件解码最流行的用例是以人类可读的格式可视化钱包活动。除此之外,还有一些有趣的用例,例如交易结果模拟和通知,但我可以在另一篇文章中介绍这些用例。
大多数流行的钱包都能很好地处理钱包活动的可视化,但我决定重点介绍最好的例子。
在文章后面的所有示例中,我将使用 Nansen CEO 的钱包0x4a7c6899cdcb379e284fbfd045462e751da4c7ce
(etherscan 上的钱包页面)来研究不同的 web3 产品如何可视化钱包的交易历史(钱包活动)并用于我们自己的实现。
Etherscan 为技术娴熟的用户提供可视化功能
让我们从第一个处理区块链数据的界面开始,即 Etherscan。
当你需要从区块链读取一些信息时,Etherscan 是首选界面。但它是为技术熟练的用户设计的,并不适合普通用户。
例如,假设有一笔从钱包发送 5 万 USDC 代币的交易。当您访问交易页面时,在“交易操作”和“已转移 ERC-20 代币”部分下,您可以看到转账的可视化效果(从哪里、到哪里、转账金额以及转账内容)。
此信息基于此交易中发生的日志/事件生成。这正是对日志/事件进行解码的地方,这些日志/事件由 USDC 代币的 ERC-20 智能合约生成。
在下面的截图中,我们可以看到记录的事件“Transfer”。正确地从事件中解码此日志,使我们能够为诸如“交易操作”和“ERC-20 代币转移”之类的可视化区块准备数据。
Etherscan 处理与标准 ERC-20/ERC-721/ERC-1155 合约交互时调用的基本交易的可视化。在代币转账/NFT 转账列表中,此类交易将被识别并清晰地呈现。
然而,对于与不同协议交互而产生的更复杂的交易(例如,DEX 协议上的代币交易,以及 Marketplace 协议上的 NFT 交易),Etherscan 无法以用户友好的方式将其可视化。如果相应的合约在 ethersscan 上具有可验证的代码,它只会将日志解码为事件。
当然,例如,如果你访问基于去中心化交易所 (DEX) 协议的 Uniswap 钱包的交易页面,你会看到实际发生的操作(例如兑换和转账),但你也会看到许多与进行交易的钱包无直接关系的无关信息。这些信息(各种转账)与执行交易的底层操作有关。
Zerion 为普通用户提供易于理解的可视化效果
让我们看看Zerion如何处理这项任务。
在下面的屏幕截图中,我们看到一个以人类可读格式显示交易历史记录的页面,没有不必要的细节,并且具有良好的可视化效果。
这里有发送和接收代币的交易、Uniswap 上的代币交换交易,以及允许从钱包余额中为特定协议/应用程序花费代币的交易。
Zapper、Interface app、Rainbow wallet 等其他人类可读的可视化示例
Zapper 应用程序
界面应用程序
彩虹钱包
我们看到,所有这些产品都以非常相似的方式可视化交易历史。只有最终的用户体验 (UX/UI) 和术语有所不同。
但所有这些产品都采用了一种通用的可视化数据准备方法。
该方法基于解码智能合约事件日志,丰富事件信息(代币/NFT 元数据(标识、名称、代码)、钱包元数据(ENS、Lens 配置文件)),并准备最终活动可视化的数据结构。
编码交易解码和数据准备逻辑(受 Zerion 启发的方法)
现在,让我们尝试实现类似于 Zerion 的可视化交易数据准备逻辑。
将交易接口分解成各个组件
让我们分解发送/接收 ERC-20 代币和 ERC-20 代币交换/兑换的情况,然后尝试了解我们需要从原始交易数据中提取哪些信息。
需要注意的是,所有应用程序中的 UI 都是从交易所属钱包的角度呈现的。
一笔交易通常有两个参与者(发送方/接收方账户或智能合约),并且同一笔交易的外观会因我们查看的账户不同而有所不同。
ERC-20 代币发送/接收交易
这里的交易 UI 可以分为 3 个主要部分:
- 交易类型- 发送或接收类型,因为 USDC 或 USDT 代币在钱包之间转移,没有其他操作。
- 交易操作- 代币转移操作,如 USDC 或 USDT 代币被提取或存入钱包。
- 交易参与者- 发送至/接收方或来自/发送方的参与者,如发送交易操作的情况下是发送至/接收方,如接收的情况下是来自/发送方的参与者。
代币交易/掉期交易
- 交易类型- 交易/交换类型,因为 MEF 代币根据 Uniswap DEX 应用程序上的某些市场价格兑换为 USDC 代币。
- 交易操作- 代币转移操作,例如从余额中扣除 MEF 代币并添加 USDC 代币。MEF 为 OUT 代币,USDC 为 IN 代币。
- 交易参与者——应用程序、钱包与负责代币交换/互换操作的 Uniswap DEX 应用程序/协议交互。
设计数据结构
根据我们对 Zerion UX 的研究,让我们定义交易的 3 个主要 UI 元素。
- 交易类型- 定义交易的性质或目的,指定交易的预期用途。(发送、接收、交换、批准、任意合约执行)
- 交易操作- 描述交易中涉及的具体步骤或操作,这些操作取决于交易类型。(代币转移操作、代币兑换操作、代币批准操作、NFT 铸造操作)
- 交易参与者- 识别参与交易的实体或账户,此参与者取决于交易类型。(发送方账户、接收方账户、合约/应用程序/协议)
因此,我们的基本实体/类型将如下所示。完整的集合可在文件中找到transactions.types.ts
。
交易方向
这是一个枚举值,用于定义交易的可能方向。它可以是“IN”、“OUT”或“SELF”。
enum TransactionDirection {
'IN' = 'IN',
'OUT' = 'OUT',
'SELF' = 'SELF'
}
交易行为
- TransactionTransferAction 定义了转账动作的结构,包括被转账的 token(类型为
Token
)、被转账的金额、转账的发送者和接收者,以及转账的方向(类型为TransactionDirection
)。 - TransactionSwapAction 定义了交换动作的结构。
interface TransactionActionBase {
type: 'TRANSFER' | 'SWAP'
}
interface TransactionTransferAction extends TransactionActionBase {
type: 'TRANSFER'
token: Token;
value: string;
from: Account;
to: Account;
direction: TransactionDirection;
}
interface TransactionSwapAction extends TransactionActionBase {
type: 'SWAP'
trader: Account;
application: Account;
}
type TransactionAction = TransactionTransferAction | TransactionSwapAction
交易类型
TransactionType
是一个枚举,定义了交易的可能类型。它可以是“SEND_TOKEN”、“RECEIVE”、“RECEIVE_TOKEN”、“EXECUTION”、“SEND_NFT”、“RECEIVE_NFT”或“SWAP”。
enum TransactionType {
'SEND' = 'SEND',
'RECEIVE' = 'RECEIVE',
'SELL' = 'SELL',
'EXECUTION' = 'EXECUTION',
'SEND_NFT' = 'SEND_NFT',
'RECEIVE_NFT' = 'RECEIVE_NFT'
}
交易
Transaction 是一个定义交易结构的接口。它包含交易的哈希值、链 ID、交易的类型和状态(分别为TransactionType
和类型TransactionStatus
)、执行时间、费用、发送方和接收方地址、交易金额、交易方向(类型TransactionDirection
)、交易涉及的操作(类型TransactionAction[]
),以及钱包地址、名称和 ID。
interface Transaction {
hash: string;
chainId: number | null;
type: TransactionType;
status: TransactionStatus;
executed: string;
gasFee: string;
fromAddress: string | null;
toAddress: string | null;
value: string | null;
direction: TransactionDirection;
transactionActions: TransactionAction[] | null;
walletAddress: string | null;
walletName?: string | null;
walletId?: string | null;
}
获取原始交易数据
要形成这样的交易对象,我们首先需要获取交易的基本属性,以及相应的交易收据,该收据在交易被挖矿(添加到区块中)后出现。
为此,我们将使用 JSON-RPC 节点提供程序Alchemy(也可以是 Infura、Quicknode 或其他)。
此外,我们需要Ethers库来利用提供商的 JSON-RPC 方法:eth_getTransactionByHash 和 eth_getTransactionReceipt。
功能decodeWalletTransactions
该decodeWalletTransactions
函数是一项服务,用于获取并解码特定钱包地址的以太坊交易。它用于提供更易于理解的交易格式,以便于在用户界面中显示交易数据或进行进一步分析。
该函数在文件scripts/decodeWalletTransactions.tsdecodeWalletTransactions
中可用。
async function decodeWalletTransactions() {
const provider = new ethers.providers.AlchemyProvider(ETHEREUM_CHAIN_ID, ALCHEMY_KEY)
const transactionsRawRequests = testTransactions.map(async (txHash) => {
const transaction = await provider.getTransaction(txHash)
const transactionReceipt = await provider.getTransactionReceipt(txHash)
return {
transaction,
transactionReceipt
}
});
const transactionsResponse = await Promise.all(transactionsRawRequests)
transactionsResponse.forEach(transactionResponse => {
const transactionContext: TransactionContext = {
chainId: ETHEREUM_CHAIN_ID,
walletAddress: getAddress(testWalletAddress) || '0x'
};
const rawTransaction = {
...transactionResponse.transaction,
receipt: transactionResponse.transactionReceipt
};
const decodedTransaction = decodeTransaction(rawTransaction, transactionContext)
console.log(`Raw transaction ${rawTransaction.hash}`, JSON.stringify(rawTransaction, undefined, 1));
console.log(`Decoded transaction ${rawTransaction.hash}`, JSON.stringify(decodedTransaction, undefined, 1));
console.log(' ');
})
}
让我们检查一下 USDC 代币转移交易的对象(etherscan 上的交易页面)及其相应的收据。
如果我们从以太坊的 rpc(eth_getTransactionByHash)请求此交易,我们将获得以下对象。
{
"jsonrpc":"2.0",
"id":1,
"result":{
"blockHash":"0x93dbccc61ff09a4ba65dacf26c35d2035373aca9453c65f65523ce9cb8700b41",
"blockNumber":"0x115f5a9",
"hash":"0xa81b0b764ea32179b29c1098378992bed1b9a53b04f180393f0438d02da1687e",
"accessList":[
],
"chainId":"0x1",
"from":"0x4a7c6899cdcb379e284fbfd045462e751da4c7ce",
"gas":"0x183ac",
"gasPrice":"0x1e6d855cc",
"input":"0xa9059cbb000000000000000000000000f1ddf1fc0310cb11f0ca87508207012f4a9cb3360000000000000000000000000000000000000000000000000000000ba43b7400",
"maxFeePerGas":"0x299ba7bbf",
"maxPriorityFeePerGas":"0x5f5e100",
"nonce":"0x318",
"r":"0x9531ceb792b8a76f4e9851b73979d6633ccdd4379635af8129a7a9a3bd830164",
"s":"0x2ada43d1f25287bba8a57939112868f271c89ed30bdd8af0956ac2906b6db000",
"to":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"transactionIndex":"0x67",
"type":"0x2",
"v":"0x1",
"value":"0x0"
}
}
如果我们从以太坊的 rpc( eth_getTransactionReceipt)请求此交易的收据
{
"jsonrpc":"2.0",
"id":1,
"result":{
"transactionHash":"0xa81b0b764ea32179b29c1098378992bed1b9a53b04f180393f0438d02da1687e",
"blockHash":"0x93dbccc61ff09a4ba65dacf26c35d2035373aca9453c65f65523ce9cb8700b41",
"blockNumber":"0x115f5a9",
"logs":[
{
"transactionHash":"0xa81b0b764ea32179b29c1098378992bed1b9a53b04f180393f0438d02da1687e",
"address":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"blockHash":"0x93dbccc61ff09a4ba65dacf26c35d2035373aca9453c65f65523ce9cb8700b41",
"blockNumber":"0x115f5a9",
"data":"0x0000000000000000000000000000000000000000000000000000000ba43b7400",
"logIndex":"0x178",
"removed":false,
"topics":[
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x0000000000000000000000004a7c6899cdcb379e284fbfd045462e751da4c7ce",
"0x000000000000000000000000f1ddf1fc0310cb11f0ca87508207012f4a9cb336"
],
"transactionIndex":"0x67"
}
],
"contractAddress":null,
"effectiveGasPrice":"0x1e6d855cc",
"cumulativeGasUsed":"0x9717a6",
"from":"0x4a7c6899cdcb379e284fbfd045462e751da4c7ce",
"gasUsed":"0x10059",
"logsBloom":"0x00000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000008000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000010000000000008000000000000000000000020000000000000010000000000000000000000000000000000200000000000000000000000000000000000000000000000000020000002000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000",
"status":"0x1",
"to":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"transactionIndex":"0x67",
"type":"0x2"
}
}
在解码逻辑中,我们将使用交易中的属性“来自”、“到”、“值”以及交易收据中的“日志”。
值得注意的是,对于生产解决方案,发送单独的eth_getTransactionReceipt
请求来检索每个事务的日志的选项并不合适。
幸运的是,市场上有强大的区块链数据提供商,提供便捷的 API 方法,让您可以一次性获取钱包交易列表以及日志(例如,Covalent)。
由于原始交易数据中看不到任何关于交易类型、交易操作和交易参与者的信息,原始数据甚至不包含 10 进制形式的数字,所有数据都经过了编码。
从原始交易日志中解码事件
我们已经设计了基本类型/实体,并找到了获取基本交易对象及其对应收据的方法,并附带日志。接下来,需要解码事件日志。
功能decodeTransactionLogs
该decodeTransactionLogs
函数用于解码交易日志。这些日志包含交易执行期间发出的事件。解码日志可以提供有关交易执行期间发生的事情的宝贵信息。
该函数在文件services/transactionDecoder/events.tsdecodeTransactionLogs
中可用。
const contractInterfaces = [
ERC20TokenEventsABIInterface,
ERC721TokenEventsABIInterface,
UniswapABIInterface
]
const decodeTransactionLogs = (logs: TransactionLogRaw[]): TransactionLogWithDecodedEvent[] => {
try {
return logs
.map(log => {
const decodedEvent = decodeLogWithInterface(log);
return {
...log,
decodedEvent
};
})
.filter((log) => !!log.decodedEvent);
} catch (error) {
return [];
}
};
const decodeLogWithInterface = (log: TransactionLogRaw): utils.LogDescription | undefined => {
for (const contractInterface of contractInterfaces) {
try {
const decodedEvent = contractInterface.parseLog(log);
if (decodedEvent) {
return decodedEvent;
}
} catch (err) {
}
}
return undefined;
};
以下是该功能工作原理的详细分解decodeTransactionLogs
:
- 输入:该函数接受一个参数:logs,它是原始事务日志的数组。
- 解码:该函数映射到日志数组上,并尝试使用该
decodeLogWithInterface
函数解码每个日志。此函数使用 ethers.js 库和一系列合约接口 (ABI) 来尝试解析日志。如果日志可以通过合约接口解析,则返回已解析的日志(解码事件)。如果日志无法通过任何合约接口解析,则返回 undefined。 - 过滤:在尝试解码所有日志后,该函数会过滤掉所有无法解码的日志(即,decodedEvent 未定义的日志)。这样我们就得到了一个已成功解码的日志数组。
- 输出:函数返回已解码日志的数组。每个解码日志都是一个对象,包含原始日志和解码事件。
- 错误处理:如果在函数执行期间发生错误,则会捕获该错误并且函数返回一个空数组。
该decodeTransactionLogs
函数用于交易解码服务,在应用交易类型规则之前解码交易日志。通过先解码日志,规则函数可以处理解码后的事件,而不是原始日志,从而使规则更简单易懂。
然后,我们将确定获取带有日志的原始交易的代码,解码日志,并使用一组规则尝试将交易转换为人类可读的形式,并将其与具有类型、方向、交易操作等属性的交易接口对齐。
功能decodeTransaction
该decodeTransaction
函数是交易解码服务的核心部分。其目的是将原始的以太坊交易解码为更易于理解的格式。
该函数在文件services/transactionDecoder/transactions.tsdecodeTransaction
中可用。
const transactionTypeRules: TransactionTypeRule[] = [
erc20DirectTransactionRule,
nativeTransactionRule,
erc721TransactionRule,
executionTransactionRule
];
export const decodeTransaction = (tx: TransactionRaw, transactionContext: TransactionContext): Transaction => {
const { chainId, walletAddress } = transactionContext;
const decodedLogs = decodeTransactionLogs(tx.receipt.logs);
const transactionWithDecodedLogs: TransactionWithDecodedLogs = {
...tx,
decodedLogs: decodedLogs
};
try {
for (const transactionTypeRule of transactionTypeRules) {
const formattedTransaction = transactionTypeRule(transactionWithDecodedLogs, transactionContext);
if (formattedTransaction) {
return formattedTransaction;
}
}
} catch (err) {}
const defaultTx: Transaction = {
chainId: chainId,
fromAddress: tx.from,
toAddress: tx.to || null,
value: String(tx.value),
hash: tx.hash,
type: TransactionType.EXECUTION,
status: !!tx.blockNumber ? TransactionStatus.SUCCESS : TransactionStatus.FAILED,
executed: tx.timestamp?.toString() || '',
gasFee: tx.gasLimit.toString(),
direction: tx.from === walletAddress ? TransactionDirection.OUT : TransactionDirection.IN,
transactionActions: [],
walletAddress
};
if (BigNumber.from(tx.value).gt(0)) {
defaultTx.transactionActions = [getNativeTransactionTransferAction (tx, transactionContext)];
}
return defaultTx;
};
以下是其工作原理的详细分解:
- 输入:该函数接受两个参数:rawTransaction 和 transactionContext。rawTransaction 是从以太坊区块链获取的交易数据。transactionContext 包含上下文信息,例如钱包地址和链 ID。
- 解码:该函数使用 ethers.js 库解析原始交易并解码其日志。这将为我们提供一个包含解码日志和收据的交易对象 (tx)。
- 交易类型规则:该函数随后会遍历一系列交易类型规则。这些规则函数将解码后的交易和交易上下文作为输入,如果规则适用,则返回解码后的交易;如果不适用,则返回 false。这些规则会按特定顺序进行检查,函数会在第一个适用的规则处停止。例如,其中一条规则是
erc20DirectTransferTransactionRule
,它会检查交易是否是 ERC-20 代币的直接(一步式)转移。如果是,规则函数会相应地解码交易并返回解码后的交易。 - 输出:如果规则适用,函数将返回规则函数返回的解码交易。如果没有规则适用,函数将返回一个回退/默认交易类型。
解码交易函数 (decodeTransaction) 的设计是可扩展的。您可以根据需要添加新的交易类型规则,以支持更多交易类型。每个规则函数负责解码特定类型的交易,这使得该函数保持模块化和可维护性。
功能erc20DirectTransferTransactionRule
该函数在文件services/transactionDecoder/transactionTypeRules/erc20DirectTransferTransactionRule.tserc20DirectTransferTransactionRule
中可用。
让我们考虑其中之一TransactionTypeRule
,它使用规则中定义的启发式方法,尝试确定它是否是 ERC-20 代币转移交易,如果是,则创建我们需要的交易对象。
ATransactionTypeRule
是一个函数,它以原始交易和交易上下文作为输入,如果规则适用,则返回已识别的交易;如果不适用,则返回 false。这些规则在 decoderTransaction 函数中用于确定交易的类型并提取相关信息。
export const erc20DirectTransferTransactionRule: TransactionTypeRule = (tx, transactionContext) => {
try {
const { hash, to } = tx;
const { walletAddress } = transactionContext;
const erc20TokenTransferEvents = tx.decodedLogs.filter((log) => isErc20TokenTransferEvent(log));
if (erc20TokenTransferEvents.length !== 1) return false;
const erc20TokenTransferEvent = erc20TokenTransferEvents[0];
if (!erc20TokenTransferEvent) return false;
const transactionTransferAction = mapErc20TokenTransferLogToTransactionTransferAction (erc20TokenTransferEvent, transactionContext);
if (!transactionTransferAction) {
return false;
}
const condition = erc20TokenTransferEvent && transactionTransferAction;
if (!condition) {
return false;
}
const fromAddress = getAddress(transactionTransferAction.from.address) || '';
return {
chainId: transactionContext.chainId,
hash: hash,
fromAddress: tx.from,
toAddress: tx.to || null,
value: tx.value.toString(),
type: fromAddress === walletAddress ? TransactionType.SEND_TOKEN : TransactionType.RECEIVE_TOKEN,
status: !!tx.blockNumber ? TransactionStatus.SUCCESS : TransactionStatus.FAILED,
executed: tx.timestamp?.toString() || '',
fee: tx.gasPrice ? tx.receipt.gasUsed.mul(tx.gasPrice).toString(): '0',
direction: tx.from === transactionContext.walletAddress ? TransactionDirection.OUT : TransactionDirection.IN,
transactionActions: [transactionTransferAction].filter(action => !!action),
walletAddress
};
} catch (error) {
console.error('[erc20DirectTransferTransactionRule]', error);
return false;
}
};
这erc20DirectTransferTransactionRule
是一个专门用于处理 ERC-20 代币直接(一步式)转账的特定 TransactionTypeRule。以下是其工作原理的详细说明:
- 输入:该函数接受两个参数:tx(要解码的交易)和transactionContext(包含钱包地址等上下文信息)。
- 过滤 ERC-20 代币转移事件:该函数过滤交易的解码日志,以查找 ERC-20 代币转移事件。此操作使用 isErc20TokenTransferEvent 函数完成。
- 检查单步转账:该函数检查是否只有一个 ERC-20 代币转账事件。如果没有,则返回 false,表示该规则不适用于此交易。
- 将 ERC-20 代币转移日志映射到交易操作:如果只有一个 ERC-20 代币转移事件,则该函数使用该函数将此事件映射到交易操作
mapErc20TokenTransferLogToTransactionTransferAction
。该函数从事件中提取相关信息,并将其格式化为交易操作。 - 检查有效的交易操作:该函数检查是否创建了有效的交易操作。如果没有,则返回 false。
- 创建并返回交易对象:如果创建了有效的交易操作,该函数将创建一个交易对象,其中包含链 ID、交易哈希、发送方和接收方地址、交易金额、交易类型、交易状态、执行时间、交易费用、交易方向和交易操作。然后返回此对象。
在decodeTransaction函数中,用于erc20DirectTransferTransactionRule
处理ERC-20代币的直接转账。如果交易是ERC-20代币的直接转账,则此规则将对其进行相应的解码并返回解码后的交易。如果交易不是ERC-20代币的直接转账,则规则将返回false,decodeTransaction函数将转到下一个规则。
探索数据解码和准备逻辑的结果
ERC-20转账交易
我们的目标是开发
我们为UI准备的交易对象
{
"chainId":1,
"hash":"0xa81b0b764ea32179b29c1098378992bed1b9a53b04f180393f0438d02da1687e",
"fromAddress":"0x4a7C6899cdcB379e284fBFD045462e751DA4C7cE",
"toAddress":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"value":"0",
"type":"SEND_TOKEN",
"status":"SUCCESS",
"executed":"",
"fee":"536018746987500",
"direction":"OUT",
"transactionActions":[
{
"type":"TRANSFER",
"token":{
"chainId":1,
"type":"ERC-20",
"name":"USDC",
"iconUrl":"",
"address":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"symbol":"USDC",
"decimals":6
},
"value":"50000000000",
"from":{
"type":"UNKNOWN",
"address":"0x4a7C6899cdcB379e284fBFD045462e751DA4C7cE"
},
"to":{
"type":"UNKNOWN",
"address":"0xf1DdF1fc0310Cb11F0Ca87508207012F4a9CB336"
},
"direction":"OUT"
}
],
"walletAddress":"0x4a7C6899cdcB379e284fBFD045462e751DA4C7cE"
}
我们可以看到我们的交易对象拥有实现这种 UI 所需的一切。
ERC-20 代币兑换/掉期交易
我们的目标是开发
我们为UI准备的交易对象
{
"chainId":1,
"hash":"0x49dc2be71db900f5699656f536fbcd606353015bcb4420985aa55985fa18e0d5",
"fromAddress":"0x4a7C6899cdcB379e284fBFD045462e751DA4C7cE",
"toAddress":"0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD",
"value":"0",
"type":"SWAP",
"status":"SUCCESS",
"executed":"",
"fee":"2378243995433416",
"direction":"OUT",
"transactionActions":[
{
"type":"SWAP",
"trader":{
"type":"EXTERNAL",
"address":"0x4a7C6899cdcB379e284fBFD045462e751DA4C7cE"
},
"application":{
"type":"CONTRACT",
"address":"0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
}
},
{
"type":"TRANSFER",
"token":{
"chainId":1,
"type":"ERC-20",
"name":"FRENBOT",
"iconUrl":"",
"address":"0xCA5001bC5134302Dbe0F798a2d0b95Ef3cF0803F",
"symbol":"MEF",
"decimals":18
},
"value":"403597500532145231683",
"from":{
"type":"UNKNOWN",
"address":"0x4a7C6899cdcB379e284fBFD045462e751DA4C7cE"
},
"to":{
"type":"UNKNOWN",
"address":"0xCA5001bC5134302Dbe0F798a2d0b95Ef3cF0803F"
},
"direction":"OUT"
},
{
"type":"TRANSFER",
"token":{
"chainId":1,
"type":"ERC-20",
"name":"FRENBOT",
"iconUrl":"",
"address":"0xCA5001bC5134302Dbe0F798a2d0b95Ef3cF0803F",
"symbol":"MEF",
"decimals":18
},
"value":"7668352510110759401990",
"from":{
"type":"UNKNOWN",
"address":"0x4a7C6899cdcB379e284fBFD045462e751DA4C7cE"
},
"to":{
"type":"UNKNOWN",
"address":"0xb181f381773d7C732B8Af4e39B39dCdb8380196a"
},
"direction":"OUT"
},
{
"type":"TRANSFER",
"token":{
"chainId":1,
"type":"ERC-20",
"name":"USDC",
"iconUrl":"",
"address":"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"symbol":"USDC",
"decimals":6
},
"value":"2520347185",
"from":{
"type":"UNKNOWN",
"address":"0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
},
"to":{
"type":"UNKNOWN",
"address":"0x4a7C6899cdcB379e284fBFD045462e751DA4C7cE"
},
"direction":"IN"
}
],
"walletAddress":"0x4a7C6899cdcB379e284fBFD045462e751DA4C7cE"
}
我们可以看到我们的交易对象拥有实现这种 UI 所需的一切。
文件decoder_transactions_results.md中提供了更多解码交易的示例。
概括
在本文中,我深入探讨了加密货币钱包交易的解码和可视化的奇妙世界。我们首先了解了事件日志在区块链交易中的关键作用,并理解了它们如何作为操作的指标和有价值的链上数据来源。
本文引导我们了解了事件在日志中存储的内部机制,强调了合约 ABI 的重要性以及 Ethers.js 等库在解析事件数据方面的实用性。这些基础概念为我们探索交易可视化奠定了基础。
我们探索了现实世界中的一些案例,了解领先的 web3 产品(例如 Etherscan、Zerion、Zapper、Interface app 和 Rainbow wallet)如何有效地可视化钱包活动。这些平台采用共同的数据准备策略,即通过补充信息来增强事件,并构建井然有序的交易历史记录。
本文的核心部分深入探讨了交易解码和数据准备逻辑编码的技术复杂性,其灵感源自 Zerion 的方法。我们将交易分解成各个组成部分,设计了数据结构,并演示了如何获取原始交易数据。此外,我们还讨论了从原始交易日志中解码事件的关键流程,包括处理 ERC-20 代币转账交易的具体规则。
为了证明我们数据准备逻辑的实用性,我们提供了解码交易的具体示例,涵盖 ERC-20 代币转账和代币兑换/交换操作。这些解码交易与文章中呈现的用户界面非常相似,展现了我们数据准备方法的有效性和完整性。
鏂囩珷鏉ユ簮锛�https://dev.to/nazhmudin/transactions-data-decoding-and- human-read-visualization-elj