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

Sui RPC 交易效果:解析 objectChanges 与 balanceChanges

一份实用指南,教你通过 JSON-RPC 读取 Sui 交易效果,解析 objectChanges 和 balanceChanges,并将交易摘要与链上实际发生的情况进行核对。

TL;DR

Sui 交易效果是协议对交易实际执行结果的权威记录:包括状态、gas 消耗以及所产生的对象和余额变更集合。要通过 JSON-RPC 读取它们,需调用 sui_getTransactionBlock 并设置 options.showEffects、showObjectChanges、showBalanceChanges 和 showEvents,然后将 effects.status.status 作为成功/失败信号,而不是提交响应。objectChanges 区分 created、mutated、deleted、wrapped、unwrapped 和 published 对象,每个条目包含 objectId、objectType、owner、version 和 previousVersion;version 是你的乐观并发令牌。balanceChanges 报告 coinType、owner 和 amount,amount 为带符号的十进制字符串,因此净变化量必须使用整数或十进制运算计算,绝不能使用浮点数。本文介绍响应结构、所有者模型、可运行的 Node.js 解析器、可针对自己端点填写的结果表,以及导致 effects 为 null 或产生误导的故障模式。

Sui 交易效果在协议中代表什么

在 Sui 中,交易被提交,但真正被网络确认的是其效果。effects 对象是协议对交易结果的记录:交易成功还是失败、消耗了多少 gas,以及确切改变了哪些对象和余额。这就是为什么读取效果是确认交易的正确方式,而不是信任提交响应——提交响应只告诉你某个全节点接受了执行请求。

这一区别很重要,因为 Sui 将执行与最终性分开。交易在被打包进检查点之前,全节点就可能返回摘要;交易也可能在执行期间失败,但仍然消耗 gas。执行后获取的 effects 对象才是“发生了什么”的权威答案。Sui JSON-RPC 参考文档在 docs.sui.io/sui-api-ref 中记录了 effects 和变更模式,承载请求的 JSON-RPC 2.0 信封规范见 jsonrpc.org/specification

对于构建索引器、钱包或对账任务的团队来说,effects 是提交摘要与你必须更新的链上状态之间的连接点。如果你刚接触该网络,请先阅读 Sui 网络概览Sui RPC 指南,然后再将 effects 接入生产环境。

  • Effects 是已确认的结果,而不是提交确认。
  • 它们携带状态、gas、对象变更、余额变更和事件。
  • 它们是对账和索引的正确数据来源。

SuiTransactionBlockResponse 结构及填充它的选项

sui_getTransactionBlock 返回 SuiTransactionBlockResponse。你最常使用的字段是 digest、effects、events、objectChanges、balanceChanges、checkpoint 和 timestampMs。关键在于,其中几个字段只有在你请求时才会被填充:options.showEffects、options.showObjectChanges、options.showBalanceChanges 和 options.showEvents。如果省略它们,这些字段会返回 null 或空值,你可能会错误地认为交易没有产生任何影响。

响应被包装在标准的 JSON-RPC 2.0 result 信封中,因此有效载荷位于 result 下。Sui TypeScript SDK 将此响应类型定义为 SuiTransactionBlockResponse,并暴露 getTransactionBlock,文档见 sdk.mystenlabs.com。SDK 会为你类型化 effect 联合类型,但每种变更类型的语义仍来自协议文档。

因此,一个最小请求看起来就是一个 JSON-RPC 调用,其 params 数组包含摘要和一个 options 对象。下一节展示了一个 curl 形式,你可以立即对任何 Sui 全节点端点运行。

curl -s https://your-sui-endpoint.example \
  -H 'Content-Type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "sui_getTransactionBlock",
    "params": [
      "0xYOUR_TRANSACTION_DIGEST",
      {
        "showEffects": true,
        "showObjectChanges": true,
        "showBalanceChanges": true,
        "showEvents": true
      }
    ]
  }'

将 effects.status 作为权威的成功或失败信号

字段 effects.status.status 要么是 "success",要么是 "failure"。当它为 "failure" 时,effects.status.error 会携带描述出错原因的结构化错误。这是权威信号:链上存在摘要并不等同于交易成功。失败的交易仍然消耗 gas,其 effects 仍会记录 gas 成本以及协议提交的任何部分状态。

一个微妙但重要的情况是:交易成功,但各个命令没有产生对象变更。例如,一个只读取状态并返回值的 Move 调用,或者效果完全在内部的命令,可以成功执行且 objectChanges 数组为空。不要将空的变更集视为失败;先检查 effects.status.status,再解释变更集。

由于失败仍然消耗 gas,对账逻辑必须显式处理失败的摘要。如果你的任务只处理成功的交易,请按 effects.status.status === "success" 过滤,并单独记录失败原因以便可观测。

  • effects.status.status 为 "success" 或 "failure"。
  • effects.status.error 解释失败原因。
  • 成功的摘要仍可能是失败的交易。
  • 成功的交易上 objectChanges 数组为空是有效的。

objectChanges:变更类型和关键字段

objectChanges 是 ObjectChange 条目的数组。每个条目都有一个 type 字段,取值为 created、mutated、deleted、wrapped、unwrapped 或 published 之一。对账时重要的字段是 objectId、objectType、owner、version、previousVersion 和 digest。对于 published 条目,packageId 和 modules 字段标识新包。

要区分变更与创建,请直接检查 type 字段,而不是根据是否存在 previousVersion 来推断。创建的对象没有 previousVersion;变更的对象同时有 version 和 previousVersion。Sui 文档中关于对象所有权、版本和效果模型的说明见 docs.sui.io/concepts,它是这些区别存在原因的权威来源。

version 是你的乐观并发令牌。当你之后变更对象时,必须引用你最后观察到的版本。如果另一个交易此后已经变更了它,你的版本就是过期的,交易将失败。因此,存储 objectChanges 中的 version 是保持本地状态与链上一致的方式。

  • 类型:created、mutated、deleted、wrapped、unwrapped、published。
  • 关键字段:objectId、objectType、owner、version、previousVersion、digest。
  • 使用 type 而非字段是否存在来分类变更。
  • version 是后续写入的乐观并发令牌。

balanceChanges 与不使用浮点数计算净变化量

balanceChanges 是 BalanceChange 条目的数组,包含 coinType、owner 和 amount。amount 是带符号的十进制字符串,不是数字。正数金额是收入,负数金额是支出,gas 成本表现为 gas 支付者的负余额变更。由于金额是字符串,你必须使用大整数或十进制库来解析它们,绝不能使用 JavaScript Number,因为它在处理大数值时会丢失精度。

要按币种和所有者计算净余额变化量,请按 (coinType, owner) 对条目分组,并对解析后的整数金额求和。结果就是该所有者在那个币种上的净变化,包括 gas。这是回答“这个地址赚了或亏了多少?”的正确方式,不会产生浮点漂移。

一个常见错误是只对正数条目求和而忽略 gas,这会高估收到的金额。始终要包含支付者的负 gas 条目。如果你需要将总转账金额与 gas 分开,请对非 gas 条目求和,并将 gas 作为单独的行项目报告。

  • amount 是带符号的十进制字符串;将其解析为整数。
  • 按 (coinType, owner) 分组并求和以得到净变化量。
  • Gas 表现为支付者的负余额变更。
  • 绝不要对 Sui 金额使用浮点数。

所有者模型及每种所有者类型对后续读取的含义

对象变更上的 owner 字段告诉你谁控制该对象,以及你之后必须如何读取它。AddressOwner 表示单个地址拥有它;你可以用 sui_getObject 读取它,并通过该地址签名来变更它。ObjectOwner 表示另一个对象拥有它,这在动态字段和子对象中很常见;你通常通过其父对象来访问它。

Shared 表示对象是共享的,可以被许多交易访问,通常带有共识排序。Immutable 表示对象永远不能再被变更,这通常适用于已发布的包和冻结对象。每种类型都会改变你的后续读取策略,因此请将所有者类型与 objectId 一起记录。

关于读取对象及其动态字段的更深入讨论,请参阅 通过 RPC 读取 Sui 对象。那篇文章涵盖了与本文所述变更路径互补的读取路径。

  • AddressOwner:单地址控制,可直接读写。
  • ObjectOwner:由另一个对象拥有,通常是动态字段。
  • Shared:可被许多交易访问,共识排序。
  • Immutable:永久冻结,通常用于包。

一个可运行的 Node.js 解析器:effects、对象变更和余额变化量

以下示例使用 @mysten/sui 获取摘要,断言 effects.status,然后打印对象变更、余额变化量和已发布包 ID 的表格。它使用 BigInt 解析金额,因此不会丢失精度。请将端点和摘要替换为你自己的值。

该脚本按币种和所有者对余额变更进行分组,以 BigInt 求和,并打印净变化量。它还从 objectChanges 中类型为 published 的条目收集已发布包 ID。请对已索引该交易的全节点运行它;如果 effects 为 null,说明节点尚未索引或已修剪该交易,故障排除部分会对此进行说明。

import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';

const client = new SuiClient({ url: getFullnodeUrl('mainnet') });
const digest = process.env.SUI_DIGEST;

const tx = await client.getTransactionBlock({
  digest,
  options: {
    showEffects: true,
    showObjectChanges: true,
    showBalanceChanges: true,
    showEvents: true,
  },
});

if (!tx.effects) {
  throw new Error('effects is null: node not indexed or pruned');
}

const status = tx.effects.status.status;
console.log('status:', status);
if (status === 'failure') {
  console.error('error:', tx.effects.status.error);
}

console.log('\nObject changes:');
for (const c of tx.objectChanges ?? []) {
  console.log([c.type, c.objectId, c.objectType, c.owner, c.version].join(' | '));
}

const deltas = new Map();
for (const b of tx.balanceChanges ?? []) {
  const key = b.coinType + '|' + JSON.stringify(b.owner);
  const prev = deltas.get(key) ?? 0n;
  deltas.set(key, prev + BigInt(b.amount));
}

console.log('\nNet balance deltas:');
for (const [key, amount] of deltas) {
  console.log(key, amount.toString());
}

const packages = (tx.objectChanges ?? [])
  .filter((c) => c.type === 'published')
  .map((c) => c.packageId);
console.log('\nPublished packages:', packages);

可针对自己端点填写的结果表

由于 effects 的可用性和延迟因提供商而异,唯一可靠的测量是你针对自己端点运行的测量。使用下表作为模板。对于每个摘要,记录 effects 是否存在、状态、对象变更数量、余额变更数量以及获取的墙钟时间。在多个摘要和至少两个端点上重复,以进行比较。

不要将任何单行视为基准。目的是描述你的提供商的索引行为,并在问题进入生产环境之前发现缺口。如果你知道某个摘要已最终确定,但 effects 为 null,这就是一个信号,表明需要调查节点的索引或修剪配置。

  • 列:摘要、effects 是否存在(是/否)、状态、objectChanges 数量、balanceChanges 数量、获取毫秒数。
  • 每个端点至少运行 10 个摘要以获得有意义的样本。
  • 将最近的摘要与较旧的摘要进行比较,以探测修剪情况。
  • 为每一行记录端点 URL 和时间戳。

基于 effects 的对账的故障模式和局限性

最常见的故障模式是在未索引或已修剪的全节点上 effects 为 null。尚未索引该交易或已修剪历史状态的全节点,即使交易已最终确定,也会返回 null effects。这是提供商配置问题,不是协议问题,并且因提供商而异。如果你需要历史 effects,请在依赖它之前验证提供商的保留策略。

第二种故障模式是将事件与 effects 混淆。事件由 Move 代码发出,与对象变更不同;交易可以发出事件而不改变对象,也可以改变对象而不发出事件。使用 通过 RPC 查询 Sui 事件 了解事件路径,并将 effects 保留用于状态对账。

第三种故障模式是十进制字符串运算。将金额作为浮点数求和会静默地破坏大数值。始终解析为 BigInt 或十进制库。最后,一些提供商可能会对大型变更集进行分页或截断;如果你预期有很多变更,请验证完整数组长度,并考虑通过 使用 gRPC 账本服务流式传输 Sui 检查点 进行高容量索引。

  • effects 为 null:未索引或已修剪的全节点,取决于提供商。
  • 事件不是 effects;不要相互替代。
  • 浮点求和会破坏十进制字符串金额。
  • 大型变更集可能被分页或截断。

排查常见的 effects 解析问题

如果 effects 为 null,首先确认摘要正确,并通过区块浏览器或第二个端点检查交易是否已最终确定。如果第二个端点返回 effects,则第一个节点存在索引或修剪缺口。如果两者都返回 null,则摘要可能格式错误,或者交易不存在。

如果成功交易上 objectChanges 为空,这是有效的;交易可能只读取了状态或产生了内部效果。如果 balanceChanges 为空但你预期有转账,请检查请求选项中是否将 showBalanceChanges 设置为 true。如果金额看起来不对,请验证你是将它们作为字符串而不是数字解析。

如果你之后尝试变更对象时看到版本不匹配,说明你使用了过期的版本。使用 通过 RPC 读取 Sui 对象 重新获取对象,并使用最新版本。对于发送前验证,使用 devInspectTransaction 模拟 Sui 交易 让你在提交前检查 effects。

  • 在第二个端点上交叉检查摘要。
  • 确认 showBalanceChanges 和 showObjectChanges 为 true。
  • 将金额解析为字符串,而不是数字。
  • 重新获取对象以刷新过期的版本。

生产环境 effects 流水线的后续步骤

对于生产对账,将 effects 解析与持久队列和以摘要为键的幂等写入结合起来。使用 Sui RPC WebSocket 订阅 实现低延迟通知,并回退到轮询 sui_getTransactionBlock 进行回填。对于高容量索引,gRPC 账本服务通常比按摘要的 RPC 调用更合适。

如果你需要具有可预测保留策略的托管端点,请查看 Sui 网络页面RPC 定价API 服务OnFinality Learn 中心 收集了相关指南,Sui RPC 指南 涵盖了更广泛的方法范围。

首先针对少量摘要运行上面的 Node.js 解析器,填写结果表,然后再将 effects 接入你的对账任务。这个顺序可以防止你构建在一个尚未测量其索引行为的端点上。

  • 以摘要为键进行幂等写入。
  • 使用 WebSocket 订阅实现低延迟,轮询用于回填。
  • 考虑使用 gRPC 进行高容量索引。
  • 在依赖端点之前先测量它。

永远不用担心基础设施

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

开始