本文介绍了如何使用 JavaScript 和 WebSocket 连接来从以太坊的 Mempool 中检索并过滤交易,特别是 Uniswap V3 协议的交易。文章详细讲解了连接设置、项目结构、代码逻辑及如何筛选特定函数的交易,并提供了执行示例和相关链接,适合开发者学习和实践。
在以太坊的交易被处理之前,它们会经过一个称为 Mempool 的缓冲区。Mempool 是一个有价值的信息源,对于希望实时检测特定交易的用户来说尤其重要。本指南将教你如何使用 WebSocket 连接和 JavaScript 检索和过滤以太坊的 Mempool 交易。具体来说,我们将过滤 Mempool,以找到希望与 Uniswap V3 协议互动的传入交易。
建立一个与以太坊 RPC 端点的 WebSocket (WSS) 连接
创建并设置项目目录
检索和过滤 Mempool 交易
要了解有关 Mempool 交易的一些背景信息,请查看我们的 如何访问以太坊 Mempool 指南,由 Sahil Sen 编写!
为了检索 Mempool 交易,我们需要一个连接到以太坊主网的全节点。你可以通过查阅 Geth 的文档 上的 入门指南 来运行自己的节点。然而,这有时难以管理,而且可能不如我们希望的那样优化。相反,你可以轻松地在 QuickNode 上启动一个端点,并访问 20 多条区块链。QuickNode 的基础设施经过优化,具有低延迟和冗余,使其速度比竞争对手快多达 8 倍。你可以使用 QuickNode 比较工具 来对比不同 RPC 和 QuickNode 的端点。
点击 创建端点 按钮并选择以太坊主网网络。然后一旦你的端点准备好,请随时保存 WSS 提供者 URL,因为你将在下一节中需要它。
你可以查看端点页面上的 指标 选项卡,以获得有关你调用方法的可贵见解,以及调用的频率。请注意,开放 WebSocket 连接接收到的响应按 每个响应 计数(了解更多信息 这里)。
现在我们通过 WSS 提供者 URL 获得了访问以太坊网络的权限,我们可以创建保存脚本和其他依赖项的项目目录。打开终端并运行以下命令集以创建项目目录,安装依赖项并创建必要的文件:
mkdir filtering-mempool-txns
cd filtering-mempool-txns
npm init -y
npm install ethers
echo > index.js && echo > abi.json
确保你的 Ethers.js 安装版本为 6 >=
在你刚创建的 filtering-mempool-txns 目录中打开你选择的代码编辑器。我们将使用 VSCode。导航到 abi.json 文件,并将以下 ABI 粘贴到文件中:
[{"inputs":[{"internalType":"address","name":"_factory","type":"address"},{"internalType":"address","name":"_WETH9","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"WETH9","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"}],"internalType":"struct ISwapRouter.ExactInputParams","name":"params","type":"tuple"}],"name":"exactInput","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountIn","type":"uint256"},{"internalType":"uint256","name":"amountOutMinimum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct ISwapRouter.ExactInputSingleParams","name":"params","type":"tuple"}],"name":"exactInputSingle","outputs":[{"internalType":"uint256","name":"amountOut","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"path","type":"bytes"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"}],"internalType":"struct ISwapRouter.ExactOutputParams","name":"params","type":"tuple"}],"name":"exactOutput","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"tokenIn","type":"address"},{"internalType":"address","name":"tokenOut","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint256","name":"amountOut","type":"uint256"},{"internalType":"uint256","name":"amountInMaximum","type":"uint256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"}],"internalType":"struct ISwapRouter.ExactOutputSingleParams","name":"params","type":"tuple"}],"name":"exactOutputSingle","outputs":[{"internalType":"uint256","name":"amountIn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"refundETH","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowed","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"expiry","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitAllowedIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"selfPermitIfNecessary","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"sweepToken","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"sweepTokenWithFee","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"int256","name":"amount0Delta","type":"int256"},{"internalType":"int256","name":"amount1Delta","type":"int256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"uniswapV3SwapCallback","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"}],"name":"unwrapWETH9","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountMinimum","type":"uint256"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"feeBips","type":"uint256"},{"internalType":"address","name":"feeRecipient","type":"address"}],"name":"unwrapWETH9WithFee","outputs":[],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}]
ABI 代表合约的接口,并帮助我们进行数据的编解码。关于 ABI 的复习,请查看我们的 什么是 ABI? 指南。
现在,项目目录设置好了,我们可以创建检索和过滤 Mempool 交易的逻辑。
在本指南中,我们将过滤 Mempool,以查找希望与 Uniswap 的 SwapRouter 进行交互的传入交易(这是一种 "用于无状态执行 Uniswap V3 交换的路由器")。一个示例交换交易将如下所示 这一条。
WebSocket URL 允许与以太坊端点之间保持持续和实时的连接,而无需多次发出 HTTP 请求。你在使用 WebSocket 时可能想要实施的良好实践包括添加逻辑以在服务器连接断开时自动重新连接,并监控你应用运行主机的网络健康(更多信息可以在 这里 找到)。
现在,让我们快速介绍一下我们将获取的 Mempool 交易的外观以及我们将如何过滤它们。
一个典型的 Mempool 交易如下所示:
{
ha: '0x7deacece762e83d20e767dcafa6bfa0cee8e41414465df1e20866b800a3a5d6b',
type: 2,
accessList: [],
blockHa: null,
blockNumber: null,
transactionIndex: null,
confirmations: 0,
from: '0x97a88D526232D228f15621B3bacce9C56137d789',
gasPrice: BigNumber { _hex: '0x09c7d88b48', _isBigNumber: true },
maxPriorityFeePerGas: BigNumber { _hex: '0x59682f00', _isBigNumber: true },
maxFeePerGas: BigNumber { _hex: '0x09c7d88b48', _isBigNumber: true },
gasLimit: BigNumber { _hex: '0x013e33', _isBigNumber: true },
to: '0x37613D64258c0FE09d5E53EeCb091DA5b8fA8707',
value: BigNumber { _hex: '0x00', _isBigNumber: true },
nonce: 190,
data: '0x095ea7b3000000000000000000000000152ec68908f44cf1b3b000b451cf092648efd1bcffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
r: '0xa12916d49d6b2983bd9fc991ca0efc3e2c7b7670797f5c69969397df61893069',
s: '0x04b080ff7242d026801978ff25de874741a879665e50efd8c2fb8e3285dfc69c',
v: 1,
creates: null,
chainId: 1,
wait: [Function (anonymous)]
}
智能合约中的每个函数都有一个函数签名。你可以通过查看交易的 data 字段中的前 4 个字节来识别函数签名。一个字节被视为两个十六进制字符(例如,7d)。
如上例,前 4 个字节为 095ea7b3(不包括前导 0x),它表示在 To 字段中的智能合约地址上调用了一个 批准 函数(即 0x37613D64258c0FE09d5E53EeCb091DA5b8fA8707)。字符串中剩余的十六进制字符表示函数参数和输入。 152ec68908f44cf1b3b000b451cf092648efd1bc 表示支出地址,而 ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 解码为一个大型 uint256(即 115792089237316...)。
在我们的代码逻辑中,我们将检查每个传入交易的 To 字段,以查看它是否与 Uniswap 路由器地址匹配(例如,0xE592427A0AEce92De3Edee1F18E0157C05861564)。我们还将过滤 data 字段以仅返回调用特定函数的交易。我们正在过滤的函数是 exactInputSingle,其函数签名为 0x414bf389。该函数可以在 ABI.json 文件中找到,用于以最大数量置换一种代币。
现在,让我们打开 index.js 文件,并输入以下代码:
const ethers = require("ethers");
const abi = require('./abi.json');
const wssUrl = "QUICKNODE_WSS_URL_PROVIDER";
const router = "0xE592427A0AEce92De3Edee1F18E0157C05861564"; //Uniswap v3 SwapRouter
const interface = new ethers.Interface(abi);
async function main() {
const provider = new ethers.WebSocketProvider(wssUrl);
provider.on('pending', async (tx) => {
const txnData = await provider.getTransaction(tx);
if (txnData) {
let gas = txnData['gasPrice'];
if (txnData.to == router && txnData['data'].includes("0x414bf389")) {
console.log("hash: ", txnData['hash']);
let decoded = interface.decodeFunctionData("exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))", txnData['data']);
logTxn(decoded, gas);
}
}
})
}
async function logTxn(data, gas) {
console.log("tokenIn: ", data['params']['tokenIn']);
console.log("tokenOut: ", data['params']['tokenOut']);
console.log("amount: ", data['params']['amountOutMinimum'].toString());
console.log("gasPrice: ", gas.toString());
console.log("-------------");
}
main();
让我们回顾一下代码:
第 1-2 行:导入依赖项,如 Ethers.js 和合约 ABI(来自 Uniswap 的 SwapRouter)。
第 5-6 行:声明将保存 WSS 提供者 URL、SwapRouter 合约地址的变量,以及一个空字典,用于记录交易。
第 8 行:使用 Ethers.js 定义一个接口对象,该对象保存与 SwapRouter 合约交互所需的编码和解码。
第 10-22 行:声明一个异步函数,通过我们的 WSS URL 初始化一个 WebSocketProvider 对象。在该函数内,我们使用 provider.on('pending') 方法仅检索待处理的交易(即 Mempool 交易)。接下来,我们创建 if 语句以确保交易对象不为空,并过滤合约地址(即路由器)和函数签名(即 0x414bf389)以满足条件。最后,我们使用 Ethers.js 接口类解码符合我们条件的交易数据。
第 24-32 行:声明一个异步函数,接受两个输入:交易对象 data 和Gas量 gas。该函数使用这些值输出 tokenIn(正在交换的代币)、tokenOut(被交换的代币)、amountOutMinimum(来自交换的输出代币数量)以及 gasPrice。
要执行此脚本,请在终端中运行以下命令:
node index.js
找到符合我们条件的交易可能需要几分钟的时间,但当检测到交易时,你将看到以下输出:
干得好!你现在知道如何检索和过滤以太坊的 Mempool 交易。尝试过滤不同的智能合约地址和函数,以巩固你的理解。你还可以继续改进当前脚本,通过检测多个函数或尝试创建套利或抢先交易机器人。
如果你有任何问题,请加入我们的 Discord,或者通过 Twitter 与我们联系。
我们 ❤️ 反馈!
如果你对本指南有任何反馈或问题,请 告知我们。我们期待你的声音!
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!