欢迎光临
我们一直在努力

在 ethers.js v6 中深入理解事件过滤器(Event Filter)

在 ethers.js 中,事件过滤器(Event Filter) 是用于精准筛选合约事件的工具,它能从海量链上事件中,按照开发者定义的条件(如索引参数、区块范围)提取目标事件,避免无差别监听或查询所有事件导致的资源浪费与效率低下。无论是实时监听新事件,还是查询历史事件,事件过滤器都是实现“按需筛选”的核心。

一、先搞懂:事件过滤器的核心背景

要理解过滤器,需先回顾 Solidity 合约事件的一个关键特性——索引参数(indexed):

  • Solidity 事件中,最多可将 3 个参数标记为 indexed(索引参数),这类参数会被存储到区块日志的“索引字段”中,支持快速查询;
  • 未标记 indexed 的参数(非索引参数),仅会作为日志的“数据字段”存储,无法通过过滤器直接筛选,需在获取事件后手动过滤。

例如 ERC20 代币的 Transfer 事件:

// from 和 to 是索引参数,value 是非索引参数
event Transfer(address indexed from, address indexed to, uint256 value);

事件过滤器的本质,就是通过指定“索引参数的匹配规则”和“区块范围”,告诉区块链节点:我只需要符合这些条件的事件。

二、事件过滤器的核心作用

事件过滤器主要解决两类问题,覆盖“实时监听”和“历史查询”两大场景:

应用场景核心作用
实时监听(contract.on()) 过滤掉无关事件,只接收符合条件的新事件(如“只监听从地址 A 转出的 Transfer 事件”),减少不必要的回调触发
历史事件查询(contract.queryFilter()) 从指定区块范围内,快速提取符合条件的历史事件(如“查询 2024 年 1 月 1 日至今,转入地址 B 的所有事件”),避免遍历所有事件

简单来说:没有过滤器时,你会收到“所有事件”;有过滤器时,你只会收到“你想要的事件”。

三、ethers.js v6 中事件过滤器的 3 种创建方式

ethers.js v6 提供了灵活的过滤器创建方式,核心通过 BaseContract.filters 对象实现,不同方式对应不同筛选需求:

方式 1:无筛选条件(获取所有该类型事件)

如果不需要筛选,仅需指定事件名称,生成“无差别过滤器”,会匹配该事件的所有实例。

适用场景
  • 监听某合约的所有 Transfer 事件;
  • 查询某区块范围内某合约的所有 Mint 事件。
示例代码(以 ERC20 合约为例)

import { ethers } from "ethers";

// 1. 初始化 Provider 和 Contract 实例
const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_API_KEY");
const ERC20_ABI = ["event Transfer(address indexed from, address indexed to, uint256 value)"];
const usdtContract = new ethers.Contract(
"0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT 主网地址
ERC20_ABI,
provider
);

// 2. 创建“无筛选条件”的 Transfer 事件过滤器
// 语法:contract.filters.事件名称()
const allTransferFilter = usdtContract.filters.Transfer();

// 3. 用过滤器查询历史事件(查询最近 100 个区块的所有 Transfer 事件)
async function queryAllTransfers() {
const currentBlock = await provider.getBlockNumber();
const startBlock = currentBlock 100; // 起始区块:当前区块前 100 块
const endBlock = currentBlock; // 结束区块:当前区块

// 调用 queryFilter,传入过滤器和区块范围
const allTransferEvents = await usdtContract.queryFilter(
allTransferFilter,
startBlock,
endBlock
);

console.log(`最近 100 区块内共 ${allTransferEvents.length} 条 Transfer 事件`);
allTransferEvents.forEach((event, i) => {
const { from, to, value } = event.args;
const formattedValue = ethers.formatUnits(value, 6); // USDT 是 6 位小数
console.log(`事件 ${i+1}${from}${to},金额:${formattedValue} USDT`);
});
}

queryAllTransfers();

方式 2:按单个索引参数筛选

指定事件的某一个 indexed 参数值,仅匹配该参数符合条件的事件。

适用场景
  • 监听“从地址 A 转出”的 Transfer 事件(筛选 from 参数);
  • 查询“转入地址 B”的所有 Transfer 事件(筛选 to 参数)。
示例代码(筛选“转入指定地址”的 Transfer 事件)

// 目标收款地址(筛选 to 为该地址的事件)
const targetToAddress = "0x1234567890123456789012345678901234567890";

// 1. 创建“筛选 to = targetToAddress”的 Transfer 事件过滤器
// 语法:contract.filters.事件名称(索引参数1值, 索引参数2值, …)
// 注意:参数顺序需与 Solidity 事件定义一致(Transfer(from, to, value),这里筛选 to,所以第一个参数传 null,第二个传目标地址)
const toTransferFilter = usdtContract.filters.Transfer(null, targetToAddress);

// 2. 用过滤器实时监听新事件
function listenToTransfers() {
console.log(`开始监听转入 ${targetToAddress} 的 USDT 事件…`);

// 传入过滤器,仅接收符合条件的事件
usdtContract.on(toTransferFilter, (from, to, value, event) => {
const formattedValue = ethers.formatUnits(value, 6);
console.log(`=== 新转入事件 ===`);
console.log(`转出地址:${from}`);
console.log(`转入地址:${to}(目标地址)`);
console.log(`金额:${formattedValue} USDT`);
console.log(`交易哈希:${event.transactionHash}\\n`);
});

// 监听错误,避免中断
usdtContract.on("error", (err) => {
console.error("监听错误:", err.message);
});
}

listenToTransfers();

方式 3:按多个索引参数筛选

同时指定事件的多个 indexed 参数值,仅匹配所有参数均符合条件的事件(多条件“与”逻辑)。

适用场景
  • 监听“从地址 A 转出且转入地址 B”的 Transfer 事件;
  • 查询“某 NFT 合约中,从地址 C 转移到地址 D”的 Transfer 事件(NFT 合约的 Transfer 事件通常有 from、to、tokenId 三个索引参数)。
示例代码(筛选“从地址 A 转到地址 B”的 Transfer 事件)

// 目标转出地址和转入地址
const targetFromAddress = "0x0987654321098765432109876543210987654321";
const targetToAddress = "0x1234567890123456789012345678901234567890";

// 1. 创建“筛选 from = targetFromAddress 且 to = targetToAddress”的 Transfer 过滤器
// 语法:按事件参数顺序传入多个索引参数值
const fromToTransferFilter = usdtContract.filters.Transfer(
targetFromAddress, // 第一个索引参数:from
targetToAddress // 第二个索引参数:to
);

// 2. 用过滤器查询历史事件(查询最近 500 区块内的符合事件)
async function queryFromToTransfers() {
const currentBlock = await provider.getBlockNumber();
const startBlock = currentBlock 500;
const endBlock = currentBlock;

const matchedEvents = await usdtContract.queryFilter(
fromToTransferFilter,
startBlock,
endBlock
);

if (matchedEvents.length === 0) {
console.log(`最近 500 区块内,没有从 ${targetFromAddress} 转到 ${targetToAddress} 的 USDT 事件`);
return;
}

console.log(`最近 500 区块内共 ${matchedEvents.length} 条匹配事件:`);
matchedEvents.forEach((event) => {
const { from, to, value } = event.args;
const formattedValue = ethers.formatUnits(value, 6);
console.log(`${from}${to}${formattedValue} USDT,区块:${event.blockNumber}`);
});
}

queryFromToTransfers();

四、事件过滤器的高级用法:区块范围筛选

无论是实时监听还是历史查询,都可以结合“区块范围”进一步缩小筛选范围,提升效率。

1. 历史查询时的区块范围

queryFilter(filter, fromBlock, toBlock) 方法的后两个参数,就是区块范围:

  • fromBlock:起始区块号(可选,默认 0,即创世区块);
  • toBlock:结束区块号(可选,默认 'latest',即当前最新区块)。
示例:查询 2024 年 1 月 1 日至今的事件

// 2024 年 1 月 1 日对应的以太坊主网区块号(需提前查询,此处为示例值)
const jan12024Block = 19000000;

async function queryEventsSinceJan1() {
const currentBlock = await provider.getBlockNumber();
const events = await usdtContract.queryFilter(
usdtContract.filters.Transfer(null, targetToAddress), // 筛选转入目标地址
jan12024Block, // 从 2024 年 1 月 1 日的区块开始
currentBlock // 到当前区块结束
);
console.log(`2024 年 1 月至今,共 ${events.length} 条转入事件`);
}

2. 实时监听时的“从指定区块开始”

默认情况下,contract.on(filter, callback) 会从“当前最新区块”开始监听新事件;若需从“历史某一区块”开始监听(补全该区块之后的所有事件,包括已发生的和未来的),可使用 contract.queryFilter 先查历史,再用 contract.on 监听未来。

示例:补全历史 + 监听未来

// 从区块 19000000 开始,补全历史并监听未来
async function catchUpAndListen() {
const currentBlock = await provider.getBlockNumber();
const filter = usdtContract.filters.Transfer(null, targetToAddress);

// 1. 补全 19000000 到当前区块的历史事件
const historicalEvents = await usdtContract.queryFilter(filter, 19000000, currentBlock);
console.log(`补全历史:19000000 ~ ${currentBlock} 区块,共 ${historicalEvents.length} 条事件`);

// 2. 监听当前区块之后的新事件
usdtContract.on(filter, (from, to, value, event) => {
if (event.blockNumber > currentBlock) { // 确保是新事件
const formattedValue = ethers.formatUnits(value, 6);
console.log(`新事件:${from}${to}${formattedValue} USDT`);
}
});
}

catchUpAndListen();

五、注意事项与避坑指南

  • 非索引参数无法通过过滤器筛选
    只有 indexed 标记的参数(最多 3 个)才能用过滤器筛选,非索引参数(如 Transfer 的 value)需在获取事件后,通过回调函数手动过滤(例如“只保留 value > 10 USDT 的事件”)。

    示例:手动过滤非索引参数

    const minValue = 10000000n; // 10 USDT(6 位小数)
    usdtContract.on(usdtContract.filters.Transfer(), (from, to, value, event) => {
    if (value > minValue) { // 手动过滤 value 大于 10 USDT 的事件
    const formattedValue = ethers.formatUnits(value, 6);
    console.log(`大额事件:${formattedValue} USDT`);
    }
    });

  • 过滤器的参数顺序必须与事件定义一致
    例如 Transfer(from, to, value) 中,from 是第一个参数,to 是第二个参数;若想筛选 to,必须将第一个参数传 null(表示不筛选该参数),第二个参数传目标地址,不能颠倒顺序。

  • 避免过度筛选导致漏事件
    若筛选条件过于严格(如同时筛选 3 个索引参数,但链上无符合条件的事件),会导致无法获取任何事件,需确保筛选条件符合实际业务场景。

  • 免费 RPC 节点的查询限制
    部分免费 RPC 节点对 queryFilter 的查询范围有限制(如单次最多查询 1000 个区块),若需查询大范围历史事件,建议:

    • 使用付费节点(如 Infura Pro、Alchemy);
    • 分批次查询(例如每次查询 1000 个区块,循环直到查完目标范围)。
  • 六、总结

    事件过滤器是 ethers.js 中高效处理合约事件的核心工具,其核心价值在于“精准筛选”:

    • 创建方式:通过 contract.filters.事件名称(参数) 生成,支持无筛选、单索引参数筛选、多索引参数筛选;
    • 核心场景:实时监听时减少无效回调,历史查询时提升效率;
    • 关键注意:仅索引参数可筛选,非索引参数需手动过滤,参数顺序需与事件定义一致。

    掌握事件过滤器的使用,能让你在处理链上事件时更高效、更灵活,避免不必要的资源浪费,是以太坊 DApp 开发中的必备技能。

    原文地址:https://mp.weixin.qq.com/s/8qOV29_ZGX8wekkffyS5kQ
    gzh:iEfoam
    CSDN账号只更新markdom类型的文章哦

    赞(0)
    未经允许不得转载:171主机测评 » 在 ethers.js v6 中深入理解事件过滤器(Event Filter)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址