解码交易详情


这一讲,我们以未决交易(Pending Transaction)为例,介绍如何解码交易详情。

未决交易

未决交易是用户发出但没被矿工打包上链的交易,在mempool(交易内存池)中出现。对于mempool的更多介绍可以看第19讲:监听Mempool

下面是一个利用Uniswap V3 Router合约交易代币的未决交易,你可以在etherscan上查看交易详情:

未决交易

红框中是这个交易的input data,看似杂乱无章的十六进制数据,实际上编码了这笔交易的内容:包括调用的函数,以及输入的参数。我们在etherscan点击Decode Input Data按钮,就可以解码这段数据:

Decode Input Data

解码之后,我们可以看到这笔交易调用了合约中的exactInputSingle()函数(Uniswap V3路由合约中的一个交易函数)以及输入的参数。

Interface类

ethers.js提供了Interface类方便解码交易数据。声明Interface类型和声明abi的方法差不多,例如:

const iface = ethers.utils.Interface([
    "function balanceOf(address) public view returns(uint)",
    "function transfer(address, uint) public returns (bool)",
    "function approve(address, uint256) public returns (bool)"
]);

如果输入参数是soliditystruct结构体类型时,我们要做特殊处理,在javascript中改为tuple元组类型。上面的exactInputSingle()函数的参数为ExactInputSingleParams结构体:

    struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 deadline;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;
    }

js中我们要写为:

const iface = new utils.Interface([
    "function exactInputSingle(tuple(address tokenIn, address tokenOut, uint24 fee, address recipient, uint deadline, uint amountIn, uint amountOutMinimum, uint160 sqrtPriceLimitX96) calldata) external payable returns (uint amountOut)",
])

解码交易数据

下面我们写一个解码未决交易数据的脚本。

  1. 创建providerwallet,监听交易时候推荐用wss连接而不是http

    // 准备 alchemy API 可以参考/solidity/p/Alchemy/ 
    const ALCHEMY_MAINNET_WSSURL = 'wss://eth-mainnet.g.alchemy.com/v2/oKmOQKbneVkxgHZfibs-iFhIlIAl6HDN';
    const provider = new ethers.providers.WebSocketProvider(ALCHEMY_MAINNET_WSSURL);
    let network = provider.getNetwork()
    network.then(res => console.log(`[${(new Date).toLocaleTimeString()}] 连接到 chain ID ${res.chainId}`));
  2. 创建Interface对象,用于解码交易详情。

    const iface = new utils.Interface([
        "function exactInputSingle(tuple(address tokenIn, address tokenOut, uint24 fee, address recipient, uint deadline, uint amountIn, uint amountOutMinimum, uint160 sqrtPriceLimitX96) calldata) external payable returns (uint amountOut)",
    ])
  3. 限制访问rpc速率,不然调用频率会超出限制,报错。

    function throttle(fn, delay) {
        let timer;
        return function(){
            if(!timer) {
                fn.apply(this, arguments)
                timer = setTimeout(()=>{
                    clearTimeout(timer)
                    timer = null
                },delay)
            }
        }
    }
  4. 监听pendinguniswapV3交易,获取交易详情并解码。这里只解码了通过Uniswap V3路由合约的exactInputSingle()函数的交易。网络不活跃的时候,可能需要等待几分钟才能监听到一笔。

    provider.on("pending", throttle(async (txHash) => {
        if (txHash) {
            // 获取tx详情
            let tx = await provider.getTransaction(txHash);
            if (tx) {
                // filter pendingTx.data
                if (tx.data.indexOf(iface.getSighash("exactInputSingle")) !== -1) {
                    // 打印txHash
                    console.log(`\n[${(new Date).toLocaleTimeString()}] 监听Pending交易: ${txHash} \r`);
    
                    // 打印解码的交易详情
                    let parsedTx = iface.parseTransaction(tx)
                    console.log("pending交易详情解码:")
                    console.log(parsedTx);
                    // Input data解码
                    console.log("Input Data解码:")
                    console.log(parsedTx.args);
                }
            }
        }
    }, 100));

    监听并解码交易

  5. 交易参数解码:

    交易参数解码

总结

这一讲,我们介绍了ethers.jsInterface类,并利用它解码了mempool中的Uniswap交易。