白天搞智能合约,晚上撸前端代码,只要咖啡还续着,凌晨照样在线!会说 Solidity、PHP、Node.js,还有 Vue 和 HTML 的“方言”,代码不怕我不写,就怕写完跑太快。Bug?那都是小场面,一出手就搞定。我的宗旨是——交付准时,调试无敌,客户满意才是真理!

19. 监听Mempool

这一讲,我们将介绍如何读取mempool(交易内存池)中的交易。

MEV

MEV(Maximal Extractable Value,最大可提取价值)是个令人着迷的话题。大部分人对它很陌生,因为在支持智能合约的区块链被发明之前它并不存在。它是科学家的盛宴,矿场的友人,散户的噩梦。

在区块链中,矿工可以通过打包、排除或重新排序他们产生的区块中的交易来获得一定的利润,而MEV是衡量这种利润的指标。

Mempool

在用户的交易被矿工打包进以太坊区块链之前,所有交易会汇集到Mempool(交易内存池)中。矿工也是在这里寻找费用高的交易优先打包,实现利益最大化。通常来说,gas price越高的交易,越容易被打包。

同时,一些MEV机器人也会搜索mempool中有利可图的交易。比如,一笔滑点设置过高的swap交易可能会被三明治攻击:通过调整gas,机器人会在这笔交易之前插一个买单,之后发送一个卖单,等效于把把代币以高价卖给用户(抢跑)。

Mempool

监听mempool

你可以利用ethers.jsProvider类提供的方法,监听mempool中的pending(未决,待打包)交易:

provider.on("pending", listener)

监听mempool脚本

下面,我们写一个监听mempool脚本。

  1. 创建providerwallet。这次我们用的provider是WebSocket Provider,更持久的监听交易。因此,我们需要将url换成wss的。

    console.log("\n1. 连接 wss RPC")
    
    const ALCHEMY_MAINNET_WSSURL = 'wss://eth-mainnet.g.alchemy.com/v2/oKmOQKbneVkxgHZfibs-iFhIlIAl6HDN';
    const provider = new ethers.WebSocketProvider(ALCHEMY_MAINNET_WSSURL);
    
  2. 因为mempool中的未决交易很多,每秒上百个,很容易达到免费rpc节点的请求上限,因此我们需要用throttle限制请求频率。

    function throttle(fn, delay) {
        let timer;
        return function(){
            if(!timer) {
                fn.apply(this, arguments)
                timer = setTimeout(()=>{
                    clearTimeout(timer)
                    timer = null
                },delay)
            }
        }
    }
    
  3. 监听mempool的未决交易,并打印交易哈希。

    let i = 0
    provider.on("pending", async (txHash) => {
        if (txHash && i < 100) {
            // 打印txHash
            console.log(`[${(new Date).toLocaleTimeString()}] 监听Pending交易 ${i}: ${txHash} \r`);
            i++
            }
    });
    

    获取pending交易哈希

  4. 通过未决交易的哈希,获取交易详情。我们看到交易还未上链,它的blockHashblockNumber,和transactionIndex都为空。但是我们可以获取到交易的发送者地址from,燃料费gasPrice,目标地址to,发送的以太数额value,发送数据data等等信息。机器人就是利用这些信息进行MEV挖掘的。

    let j = 0
    provider.on("pending", throttle(async (txHash) => {
        if (txHash && j <= 100) {
            // 获取tx详情
            let tx = await provider.getTransaction(txHash);
            console.log(`\n[${(new Date).toLocaleTimeString()}] 监听Pending交易 ${j}: ${txHash} \r`);
            console.log(tx);
            j++
            }
    }, 1000));
    

    获取交易详情

完整代码


// provider.on("pending", listener)
import { ethers } from "ethers";

// 1. 创建provider和wallet,监听事件时候推荐用wss连接而不是http
console.log("\n1. 连接 wss RPC")

const ALCHEMY_MAINNET_WSSURL = 'wss://eth-mainnet.g.alchemy.com/v2/oKmOQKbneVkxgHZfibs-iFhIlIAl6HDN';
const provider = new ethers.WebSocketProvider(ALCHEMY_MAINNET_WSSURL);
let network = provider.getNetwork()
// network.then(res => console.log(`[${(new Date).toLocaleTimeString()}] 连接到 chain ID ${res.chainId}`));

console.log("\n2. 限制调用rpc接口速率")
// 2. 限制访问rpc速率,不然调用频率会超出限制,报错。
function throttle(fn, delay) {
    let timer;
    return function(){
        if(!timer) {
            fn.apply(this, arguments)
            timer = setTimeout(()=>{
                clearTimeout(timer)
                timer = null
            },delay)
        }
    }
}

const main = async () => {
    let i = 0;
    // 3. 监听pending交易,获取txHash
    console.log("\n3. 监听pending交易,打印txHash。")
    provider.on("pending", async (txHash) => {
        if (txHash && i < 100) {
            // 打印txHash
            console.log(`[${(new Date).toLocaleTimeString()}] 监听Pending交易 ${i}: ${txHash} \r`);
            i++
            }
    });

    // 4. 监听pending交易,并获取交易详情
    console.log("\n4. 监听pending交易,获取txHash,并输出交易详情。")
    let j = 0
    provider.on("pending", throttle(async (txHash) => {
        if (txHash && j <= 100) {
            // 获取tx详情
            let tx = await provider.getTransaction(txHash);
            console.log(`\n[${(new Date).toLocaleTimeString()}] 监听Pending交易 ${j}: ${txHash} \r`);
            console.log(tx);
            j++
            }
    }, 1000));
};

main()