360 Secure SkillHub
360 安全技能中心

Sui Grpc Data Format

面向 Sentio SDK 4 的 Sui processor 开发参考:说明链上数据改走 gRPC 后,原始交易、对象、事件结构与 Move 类型字符串相对 JSON-RPC 的字段名、嵌套、oneof 联合与格式化差异,给出字段映射表、按括号深度拆分泛型参数、地址归一化及升级检查清单;仅用类型化 data_decoded 的处理器通常无需改动。

philz3906
philz3906
热度 16.02
最近更新 2026-06-26 17:25:21
分类:开发依赖:纯提示词应用场景:概念解释 / 调试排错 / API 集成

安装技能

复制下面的话术发给你的 Agent,它会通过 secure-skills 检索并安装该技能。

执行命令安装此 skill: npx secure-skills-cli add sui-grpc-data-format
Metadata
name
sui-grpc-data-format
description
Use when writing or upgrading a Sentio Sui processor on SDK 4 that touches RAW transaction / object / event structures — e.g. reading fields off `ctx.transaction`, walking programmable-transaction commands, decoding an object from `ctx.client.getObject`, or parsing Move type strings. On SDK 4 Sui chain data is fetched over gRPC, so these raw shapes follow the Sui gRPC protobuf schema and differ from the JSON-RPC shapes used by earlier SDK versions (field names, nesting, protobuf oneof unions, and type-string formatting). Not needed for typed handlers that only use `event.data_decoded`.

Sui gRPC vs JSON-RPC raw data shapes (SDK 4)

Overview

On Sentio SDK 4, Sui processors fetch on-chain data over gRPC. The decoded values from typed handlers are unchanged — the SDK normalizes them. But any code that reaches into raw transaction, event, or object structures now sees the gRPC protobuf shapes, which differ from the older JSON-RPC shapes in field names, nesting, oneof unions, and string formatting.

If you are upgrading a processor from an earlier SDK and it parses raw transactions / objects / Move type strings, those are the spots that need changes. A processor that only uses typed decoded values usually needs no code change (just regenerate bindings).

Authoritative schema — read the proto

The gRPC shapes are defined by the Sui RPC v2 protobufs. Treat the proto, not this doc, as the source of truth:

This is a fork of an upstream schema that evolves, so field names/shapes can change across versions — re-verify against the proto after an SDK bump.

What does NOT change

  • Typed event handlers: onEventXxx((event, ctx) => …) and event.data_decoded.*.
  • Logic that only uses decoded values (amounts, metrics, store entities).

You only adapt code that reads raw structures (ctx.transaction.* beyond data_decoded, objects from ctx.client, type strings).

protobuf oneof pattern

gRPC messages use protobuf oneof (discriminated unions). In TypeScript they appear as { oneofKind: "<case>", <case>: <value> }. Always branch on oneofKind before reading the value:

const data = ctx.transaction.transaction?.kind?.data
const ptb = data?.oneofKind === "programmableTransaction" ? data.programmableTransaction : undefined
const moveCalls = (ptb?.commands ?? []).flatMap((c) =>
  c.command.oneofKind === "moveCall" ? [c.command.moveCall] : []
)

Transaction shape: JSON-RPC → gRPC

You want JSON-RPC (earlier SDK) gRPC (SDK 4)
PTB command list ctx.transaction.transaction.data.transaction.transactions ctx.transaction.transaction.kind.data where oneofKind === "programmableTransaction" → .programmableTransaction.commands
a MoveCall command element .MoveCall (may be absent) element .command.oneofKind === "moveCall" → .moveCall
MoveCall fields .package / .module / .function / .type_arguments .package / .module / .function / .typeArguments (camelCase)
events on the tx ctx.transaction.events ctx.transaction.events?.events
event type ev.type ev.eventType
event parsed payload ev.parsedJson ev.json
tx digest ctx.transaction.digest ctx.transaction.digest — may be undefined; guard before use

Object shape

Reading an object via ctx.client.getObject(...) returns the gRPC object:

const obj = await ctx.client.getObject({ objectId: id, include: { json: true } })
const type = obj.object.type        // full Move type string
const fields = obj.object.json      // decoded contents, e.g. obj.object.json?.fee_rate

Request decoded contents explicitly (include: { json: true }); read fields off obj.object.json, and the object's Move type off obj.object.type.

Move type strings — parse structurally, not by text format

Raw type strings carry two gRPC-vs-JSON-RPC differences. Do not write parsers that assume the JSON-RPC text layout:

  1. Comma separator differs. Between generic args, gRPC uses , (no space) → Pool<A,B>, while JSON-RPC uses , (comma + space) → Pool<A, B>. Regexes that rely on whitespace will mis-split. This comes from the same two formatters as the address difference below: move's StructTag::to_canonical_display joins type args with "," (language_storage.rs), whereas Sui's to_sui_struct_tag_string joins with ", " (sui_serde.rs).

    • Split top-level type args by bracket depth (only the comma at depth 0), which also handles nested generics.
  2. Address form differs by path — and the short-form set is a hardcoded list, not a numeric range.

    • gRPC renders every type-tag address in full 64-hex (StructTag::to_canonical_string(true) — see crates/sui-rpc-api/src/grpc/v2/render.rs). So SUI is 0x0000…0002::sui::SUI.
    • JSON-RPC short-forms (leading zeros stripped) only the addresses in a hardcoded SUI_ADDRESSES set and renders all others full 64-hex — see to_sui_struct_tag_string in crates/sui-types/src/sui_serde.rs:
      let address = if SUI_ADDRESSES.contains(&value.address) {
          value.address.short_str_lossless()                          // framework addrs → short (0x2)
      } else {
          value.address.to_canonical_string(/* with_prefix */ false)  // everything else → full 64-hex
      };
      
      SUI_ADDRESSES is exactly 7 framework/system addresses: 0x0, 0x1, 0x2 (framework), 0x3 (system), 0x5 (system state), 0x6 (clock), 0xdee9 (DeepBook). This is an explicit list — note DeepBook 0xdee9 is short despite being far above 0xf, so it is not the "addresses 0x0–0xf are special" rule from other Move chains.

    So the same coin's type can be spelled differently across paths — 0x2::sui::SUI (JSON-RPC) vs 0x0000…0002::sui::SUI (gRPC) — and exact string equality is unreliable.

    • To compare or match type strings across paths, normalize both sides with @mysten/sui's normalizeStructTag / normalizeSuiAddress, which pad every address to the full 64-hex form.
    • Do not hand-strip leading zeros to bridge the gap (it corrupts ordinary addresses, e.g. 0x027792… → 0x27792…), and don't assume a numeric "special range" — Sui uses the explicit SUI_ADDRESSES list above.
// Extract top-level generic type args from a struct type string,
// robust to comma spacing and nested generics.
function topLevelTypeArgs(type: string): string[] {
  const open = type.indexOf("<")
  const close = type.lastIndexOf(">")
  if (open < 0 || close < 0 || close < open) return []
  const inner = type.slice(open + 1, close)
  const args: string[] = []
  let depth = 0
  let start = 0
  for (let i = 0; i < inner.length; i++) {
    const ch = inner[i]
    if (ch === "<") depth++
    else if (ch === ">") depth--
    else if (ch === "," && depth === 0) {
      args.push(inner.slice(start, i).trim())
      start = i + 1
    }
  }
  args.push(inner.slice(start).trim())
  return args
}

Upgrade checklist

  • Run sentio gen to regenerate type bindings under SDK 4; commit the regenerated files.
  • Find every place that reads raw ctx.transaction / events / objects (not data_decoded) and adapt to the gRPC shapes above (oneof branching, camelCase fields, events.events, eventType, json).
  • Replace whitespace/format-dependent type-string parsing with depth-based splitting + explicit address normalization.
  • Guard against undefined digests / optional fields.
  • After any SDK bump, re-check field names against the proto — the schema is a fork of an evolving upstream.