Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
网络与协议指南阅读约 14 分钟

通过 RPC 查询 Sui 事件:suix_queryEvents、过滤器与游标分页

了解 Sui 如何发出和索引 Move 事件,如何使用 suix_queryEvents 过滤事件,以及如何使用游标分页遍历历史记录而不丢失或重复事件。

TL;DR

Sui Move 事件通过 sui::event::emit 发出,携带完全限定类型,并由全节点索引,因此可以使用 suix_queryEvents 配合 EventFilter 和游标进行查询。EventFilter 联合类型包括 All、Transaction、MoveModule、MoveEventType、MoveEventField、Sender、TimeRange 和 Package,每种都有不同的匹配语义。分页仅基于游标:响应返回 data[] 以及 nextCursor 和 hasNextPage,通过传递上一页的 nextCursor 并指定 limit 和 order 来遍历页面。可靠的索引器会存储最后处理的游标(或 txDigest 和 eventSeq),从该游标开始轮询,并在断开连接后对遗漏的事件进行对账,同时遵守全节点的保留窗口。

什么是 Sui Move 事件以及它如何存储

Sui Move 事件是在交易执行期间通过 sui::event::emit 发出的结构化记录。每个事件都携带一个完全限定类型,例如 0x2::sui::SUI 或 package::module::Struct 路径,全节点会对其进行索引,以便客户端稍后查询。关于发出和索引的权威描述见 Sui 文档的 Emitting Events 页面。

查询事件时,RPC 返回的每个事件都包含全局事件 ID、交易摘要和序列号、类型字符串、parsedJson 渲染以及 bcs(base64) 规范负载。parsedJson 便于应用程序逻辑使用,但当你需要验证或完全按照发出时的样子重新序列化事件时,应使用 bcs 字段作为规范表示。

事件由全节点索引,但不会永久存储。较旧的事件可能落在节点的保留窗口之外,这就是历史事件查询通常需要归档节点或专用索引器的原因。OnFinality 的 Sui 归档节点与历史 RPC 页面解释了归档访问如何将可查询历史扩展到标准全节点之外。

  • 在交易执行期间通过 sui::event::emit 发出。
  • 携带完全限定类型:package::module::Struct。
  • 返回时包含 id、txDigest、sequence、type、parsedJson 和 bcs(base64)。
  • 由全节点索引;保留期限有限且因节点而异。

suix_queryEvents 方法及其参数

suix_queryEvents 方法是读取已索引事件的 JSON-RPC 入口点。它接受一个查询对象,其中包含 EventFilter、limit、cursor 以及 Ascending 或 Descending 的 order。响应是一个 QueryEventsResult,包含 data 数组和 nextCursor,在当前实现中还有 hasNextPage 标志。该方法及其类型记录在 Sui API Reference 和 sui_json_rpc crate 文档中。

limit 控制每页返回多少事件。cursor 是来自上一页 nextCursor 的不透明令牌;应将其视为黑盒,切勿自行构造或解析。order 决定页面是在已索引事件流中向前还是向后遍历。

由于该方法是旧版 JSON-RPC 接口的一部分,一些提供商将其标记为已弃用,转而推荐较新的事件 API。Sui JSON-RPC 迁移文档跟踪了这一过渡。如果你的端点报告弃用,请在构建长期集成之前,验证当前针对你的用例推荐的事件 API。

  • query: { filter, limit, cursor, order }。
  • 响应:{ data[], nextCursor, hasNextPage }。
  • order 为 Ascending 或 Descending。
  • 游标是不透明的;始终复用返回的 nextCursor。

EventFilter 变体及其精确匹配语义

EventFilter 联合类型定义了如何选择事件。All 匹配所有事件。Transaction 匹配来自特定交易摘要的事件。MoveModule 匹配来自包和模块对的事件。MoveEventType 匹配完全限定的结构体类型字符串。MoveEventField 匹配解析后的字段路径和值。Sender 匹配由特定地址发出的事件。TimeRange 匹配时间戳范围内的事件。Package 匹配来自某个包的事件。

StructType 和 MoveEventType 之间的区别很重要。StructType 匹配比较事件的结构体类型,而 MoveEventType 匹配比较事件的类型标签。在实践中,这意味着你传入的字符串必须与发出时完全一致的完全限定类型,包括开头的 0x 地址以及模块和结构体名称。部分或不限定的字符串将静默返回空结果。

MoveEventField 过滤器比较解析后的值,因此数字和 ID 必须与解析后的表示匹配。如果你过滤的字段在 parsedJson 中是字符串,就传字符串;如果是数字,就传数字。类型不匹配是导致结果集为空的常见原因。

  • All:不过滤。
  • Transaction:按交易摘要。
  • MoveModule:按包和模块。
  • MoveEventType:按完全限定结构体类型字符串。
  • MoveEventField:按解析后的字段路径和值。
  • Sender:按发出地址。
  • TimeRange:按时间戳范围。
  • Package:按包地址。

游标分页:唯一正确的分页方式

Sui 事件查询不支持偏移分页。你通过将上一个响应的 nextCursor 传入下一个请求来分页。这是遍历事件流而不跳过或重复事件的唯一正确方式,因为底层索引可能在请求之间发生变化,偏移量会漂移。

降序加上游标是向后遍历历史的标准方式。如果你想要最新的事件在前,将 order 设置为 Descending 并以空游标开始。每个后续请求使用上一页的 nextCursor。当 hasNextPage 为 false 或 nextCursor 为 null 时,表示已到达该过滤器可用历史的末尾。

一个常见错误是在不同过滤器之间复用同一个游标,或者完全忽略游标并从头重新查询。两者都会导致循环或重复处理。始终将游标与产生它的过滤器一起存储,并且只在与相同过滤器和顺序一起使用时才复用它。

  • 没有偏移分页;仅使用 nextCursor。
  • 降序向后遍历历史。
  • hasNextPage 为 false 或 nextCursor 为 null 表示历史结束。
  • 将游标与其过滤器和顺序一起存储以避免循环。

一个可运行的 Node.js 示例:按 MoveEventType 过滤并使用游标分页

以下 Node.js 示例使用针对特定结构体的 MoveEventType 过滤器调用 suix_queryEvents,然后循环使用 nextCursor 收集固定数量的页面。它打印每个事件的关键字段,以便你了解响应的结构。请将端点 URL 和结构体类型替换为你自己的值。

该示例使用 fetch,它在现代 Node.js 中可用。它在迭代之间存储游标,并在没有下一页或达到页面限制时停止。这与你在生产索引器中使用的模式相同,只是额外需要持久化游标。

const ENDPOINT = 'https://your-sui-rpc-endpoint';
const STRUCT_TYPE = '0x2::sui::SUI';
const PAGE_LIMIT = 50;
const MAX_PAGES = 5;

async function queryEventsPage(cursor) {
  const body = {
    jsonrpc: '2.0',
    id: 1,
    method: 'suix_queryEvents',
    params: [
      {
        MoveEventType: STRUCT_TYPE
      },
      cursor,
      PAGE_LIMIT,
      true // descending
    ]
  };
  const res = await fetch(ENDPOINT, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body)
  });
  const json = await res.json();
  if (json.error) throw new Error(JSON.stringify(json.error));
  return json.result;
}

async function collectPages() {
  let cursor = null;
  let pages = 0;
  const all = [];
  while (pages < MAX_PAGES) {
    const result = await queryEventsPage(cursor);
    for (const event of result.data) {
      all.push(event);
      console.log({
        id: event.id,
        txDigest: event.id.txDigest,
        sequence: event.id.eventSeq,
        type: event.type,
        parsedJson: event.parsedJson,
        bcs: event.bcs
      });
    }
    pages += 1;
    if (!result.hasNextPage || !result.nextCursor) break;
    cursor = result.nextCursor;
  }
  console.log('collected', all.length, 'events across', pages, 'pages');
}

collectPages().catch(console.error);

按发送者和交易摘要过滤

当你想要特定地址发出的所有事件(无论类型)时,按发送者过滤很有用。过滤器是 Sender 加上地址字符串。这常用于钱包活动流以及审计特定账户与多个包的交互。

当你已经知道交易并想检查其事件时,按交易摘要过滤很有用。过滤器是 Transaction 加上摘要字符串。这是最精确的过滤器,仅按序列顺序返回该单笔交易的事件。

两种过滤器都支持相同的游标分页。如果某个发送者有非常庞大的事件历史,你将像使用任何其他过滤器一样通过 nextCursor 分页遍历。保留窗口仍然适用,因此非常旧的发送者事件在标准全节点上可能不可用。

  • Sender:来自某个地址的所有事件。
  • Transaction:来自一个交易摘要的所有事件。
  • 相同的游标分页和保留规则适用。

连续索引器的轮询模式

连续索引器应保留最后处理的游标,或最后处理的 (txDigest, eventSeq) 对,并按间隔从该游标轮询 queryEvents。这确保每次轮询都恰好从上次停止的地方继续,没有间隙或重复。如果你偏好实时交付,Sui WebSocket 事件订阅 中描述的 WebSocket 订阅可以在事件发出时推送它们,但你仍应存储游标,以便在断开连接后进行对账。

断开连接后,从存储的游标重新查询,以获取你离线期间发出的任何事件。这个对账步骤是使索引器可靠的关键。没有它,断开的 WebSocket 连接会静默丢失事件。

对于高容量索引,考虑批量查询并使用专用端点。OnFinality 的 API 服务RPC 定价 页面描述了可用的计划以及如何为持续事件轮询确定规模。

  • 存储最后的游标或 (txDigest, eventSeq)。
  • 按间隔从该游标轮询。
  • 使用 WebSocket 实现实时,但保留游标用于对账。
  • 断开连接后从存储的游标重新查询。

结果表:测量你的端点的事件查询限制

事件查询行为因提供商和节点配置而异。使用下表记录你自己的端点报告的内容。通过运行查询并观察响应,或查看提供商的文档来填写每一行。不要假设其他提供商的值适用于你的。

对于每页最大限制,尝试一个较大的 limit,看看端点是限制它还是返回错误。对于保留窗口,查询一个你知道很旧的事件,看看它是否被返回。对于弃用,检查端点是否返回弃用通知,或者提供商是否记录了替代 API。

  • 每页最大限制:[你的值]
  • 保留窗口:[你的值]
  • 已弃用,转而使用新事件 API:[是/否]
  • 跨请求的游标稳定性:[你的观察]
  • 事件查询的速率限制:[你的值]

常见故障及其诊断方法

空结果是最常见的故障。首先要检查的是过滤器类型是否为完全限定的结构体类型。缺少 0x 前缀、模块名错误或类型字符串不完整都会返回空。从已知交易的 type 字段验证确切的类型字符串。

早于节点保留窗口的事件也会返回空。如果你在查询历史并得到空页面,请检查事件是否在保留窗口内。如果不在,你需要归档节点或专用索引器。Sui 归档节点与历史 RPC 页面涵盖了这一点。

游标误用会导致循环或重复。如果你将游标与不同的过滤器复用,或者忽略游标并从头重新查询,你将反复处理相同的事件。始终将游标与其过滤器和顺序一起存储,并且只在与相同查询一起使用时才复用它。

依赖已弃用的 JSON-RPC 路径而不是当前事件 API,如果端点已移除或限制该方法,可能会导致失败。在构建长期集成之前,请检查提供商的文档和 Sui JSON-RPC 迁移说明。

  • 空结果:验证完全限定的结构体类型。
  • 空结果:检查保留窗口。
  • 循环或重复:正确存储并复用游标。
  • 弃用:验证你的端点当前的事件 API。

限制与权衡

事件查询受全节点保留窗口的限制。标准全节点不会存储所有历史,因此非常旧的事件需要归档节点或外部索引器。这是存储成本与可查询性之间的根本权衡。

游标分页可靠但不是随机访问。你无法跳转到任意偏移量;必须从已知游标开始遍历。对于庞大的历史,这意味着你的第一次查询可能需要很多页才能到达你想要的事件。

parsedJson 渲染方便但不是规范的。如果你需要精确验证或重新序列化事件,请使用 bcs 字段。parsedJson 的格式可能随版本变化,因此不要依赖其确切结构进行长期存储。

事件查询速率限制和页面大小限制因提供商而异。OnFinality 不发布具体的事件查询延迟或吞吐量数字,因为它们取决于端点和负载。使用上面的结果表针对你自己的端点进行测量。

  • 保留窗口限制历史查询。
  • 游标分页不是随机访问。
  • parsedJson 不是规范的;使用 bcs 进行验证。
  • 速率和页面限制因提供商而异。

故障排除清单

当事件查询未按预期运行时,使用此清单。按顺序逐项检查,因为最常见的原因也是最容易检查的。

如果你对 Sui RPC 不熟悉,Sui RPC 指南(RPC Assistant) 提供了更广泛的入门介绍。关于对象读取和动态字段,请参阅 读取 Sui 对象:getObject、动态字段和分页。关于交易模拟,请参阅 使用 devInspectTransaction 模拟 Sui 交易

  • 过滤器类型是否带有 0x 前缀且完全限定?
  • 事件是否在节点的保留窗口内?
  • 你是否传递了上一页的 nextCursor?
  • 你是否将相同的过滤器和顺序与游标一起使用?
  • 该方法在你的端点上是否已弃用?
  • 你是否触发了速率限制或页面大小限制?
  • 你是否在本应读取 bcs 时读取了 parsedJson?

后续步骤与进一步阅读

要深入了解,请从 OnFinality Learn 中心 开始查看相关 Sui 指南,并查看 Sui RPC 指南(RPC Assistant) 获取逐方法的入门介绍。如果你需要超出全节点保留窗口的历史事件,请查看 Sui 归档节点与历史 RPC。关于实时交付,请参阅 Sui WebSocket 事件订阅

当你准备在生产环境中运行事件查询时,请在 RPC 定价 页面比较计划,并查看 API 服务 了解托管端点。关于特定网络的详细信息,请参阅 Sui 网络页面

本文中主张的权威一手来源是 Sui 文档的 Emitting Events 页面以及 suix_queryEvents 的 Sui API Reference。始终针对你自己的端点验证方法行为,因为提供商实现和保留窗口各不相同。

  • 查看 OnFinality Learn 中心获取相关指南。
  • 检查归档节点以获取历史事件。
  • 使用 WebSocket 订阅实现实时交付。
  • 针对你自己的端点验证行为。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始