Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
集成与开发阅读约 14 分钟

Solana accountSubscribe:base64 与 jsonParsed 编码对比

了解 Solana accountSubscribe 通知在 base64 与 jsonParsed 编码下的差异、各自适用场景,以及如何可靠地解码两种数据形态。

TL;DR

Solana accountSubscribe 接受一个可选的 encoding 参数,文档中定义了三个取值:base64、base64+zstd 和 jsonParsed。通知信封(context 与 value)在所有编码下完全一致,只有 value.data 的形态会变化。base64 返回一个包含账户原始字节的两元素 [data, encoding] 元组,而 jsonParsed 返回一个由所属程序解析器生成、包含 program 与 parsed 字段的对象。jsonParsed 并非通用:当运行时没有对应程序的解析器时,账户会返回 parsed: null,因此只实现 jsonParsed 分支的客户端会静默丢弃这些更新。生产环境中的索引器通常请求 base64 以保证稳定性,而用 jsonParsed 做检查,因为解析器输出可能随节点版本变化。本文提供两种编码的可运行 Node.js 示例、用于对照自己端点验证的结果表,以及针对结构不匹配、压缩载荷和订阅 ID 关联的故障排查。

accountSubscribe 请求及其 encoding 参数

accountSubscribe 是 Solana WebSocket 方法,用于注册对某个由 base58 公钥标识的单一账户变更的订阅。该请求是通过 WebSocket 连接发送的标准 JSON-RPC 2.0 调用,包含 id、方法名和 params 数组。Solana 关于 accountSubscribe 的文档规定 params 为账户公钥、可选的 commitment 级别以及可选的 encoding 值。文档中列出的 encoding 取值为 base64、base64+zstd 和 jsonParsed。

encoding 参数只控制账户 data 字段在通知中的表示方式。它不会改变所监视的账户、通知触发时机,也不会改变通知信封本身。如果省略 encoding,节点会应用其默认值,按文档行为即 base64。由于该选择是按订阅生效的,你可以对同一账户用不同编码打开两个订阅并并排比较,这是最快理解差异的方式。

订阅会通过一个包含订阅 id 的响应得到确认,后续通知以 method 为 accountNotification 的 JSON-RPC 通知形式到达。该 id 用于将通知与产生它的订阅关联起来,这一主题在 JSON-RPC 通知 ID 关联与批量排序 中有所介绍。

  • Params:账户公钥(base58 字符串)、可选 commitment、可选 encoding。
  • 文档中的编码:base64、base64+zstd、jsonParsed。
  • 通知方法:accountNotification,带有用于关联的订阅 id。

base64 返回什么:元组中的原始字节

使用 encoding base64 时,通知的 value.data 是一个两元素数组:第一个元素是账户原始字节的 base64 字符串,第二个元素是字面字符串 "base64"。这种元组形式在 Solana RPC JSON 结构 页面中有文档说明,该页面描述了账户数据的表示方式。字节是无损的:程序写入账户的内容就是你收到的内容,节点不做任何解释。

由于节点不做解析,base64 是生产索引器的稳定选择。反序列化步骤由你的客户端负责,这意味着你必须维护与链上程序保持同步的 Borsh 布局。这是实实在在的工作,但它是你可控的工作。节点升级不会改变字节的含义,因为节点从一开始就没有赋予它们含义。

元组形态很容易被误处理。一个常见 bug 是把 value.data 当作字符串并直接调用 Buffer.from(data, 'base64'),这会失败,因为 data 是数组。正确的访问方式是 data[0] 取 base64 字符串,data[1] 取编码标签。如果你的代码路径也可能收到 jsonParsed,那么基于 data[1] 分支而不是假设 base64,是一个成本很低的防御习惯。

  • value.data 为 [base64String, "base64"]。
  • 无损:节点侧不做任何解释。
  • 客户端必须用与程序保持同步的布局进行反序列化。

base64+zstd 返回什么,以及压缩何时有帮助

base64+zstd 在 base64 编码之前对同样的原始字节应用 zstd 压缩,因此线上载荷更小。元组中的文档编码标签为 "base64+zstd"。这对于带宽或消息大小受限的大型账户最有用,例如持有大量状态或数组的账户。

代价是你的客户端必须先解压才能反序列化。Node.js 标准库不提供 zstd 解压器,因此你需要一个依赖或原生绑定。这会给你的流水线增加一个活动部件。对于小账户,压缩开销可能不值得增加依赖,读者应当实际测量而不是假设。

压缩改变的是线上表示,而不是语义。解压后,字节与 base64 本会传递的完全相同。如果你在调试结构不匹配,在编写解压逻辑之前先确认你实际收到的编码标签,因为 base64 订阅永远不会产生 zstd 载荷。

  • 与 base64 相同的字节,经 zstd 压缩后再 base64 编码。
  • 元组标签为 "base64+zstd"。
  • 客户端需要 zstd 解压器;按账户大小衡量收益。

jsonParsed 返回什么:解析器输出及其限制

使用 encoding jsonParsed 时,节点会用账户所属程序关联的解析器处理该账户,并将 value.data 返回为一个包含 program 和 parsed 字段的对象。parsed 字段包含程序特定的结构,例如 Token 程序的代币账户字段。当解析器存在时,这是最方便的表示方式,因为它免去了客户端 Borsh 布局的需要。

关键限制在于覆盖范围。运行时只会解析它已知程序所拥有的账户,例如 System、Token、Token-2022、Stake 等内置程序。对于自定义程序拥有的账户,节点没有解析器,文档行为是 parsed 为 null。在这种情况下,原始字节是唯一真实的表示。

这正是让团队措手不及的失败模式。只实现 jsonParsed 分支的客户端会收到自定义程序账户的通知,看到 parsed: null,然后要么抛错,要么静默跳过更新。订阅本身是正常的;是客户端在丢弃数据。修复方法是根据结构分支并回退到原始字节,这也是为什么许多团队对任何打算索引的内容都请求 base64。

  • value.data 是一个包含 program 和 parsed 的对象。
  • 当运行时没有所属程序的解析器时,parsed 为 null。
  • 客户端必须根据结构分支,而不是假设 parsed 一定存在。

AccountNotification 信封在所有编码下完全一致

切换编码时,通知结果结构不会改变。根据 Solana 文档,accountNotification 携带的 result 包含 context: { slot } 和 value: { lamports, owner, data, executable, rentEpoch }。唯一形态会变化的字段是 data。在 base64 和 base64+zstd 下它是元组;在 jsonParsed 下它是对象。

这对客户端设计很重要。你可以为信封编写一个处理器,并把编码相关的逻辑隔离在一个函数中,将 data 规范化为字节或解析对象。这样可以把分支限制在一处,并让可能破坏结构假设的地方一目了然。

这也意味着你无法从信封推断编码。编码标签存在于元组形式的 data 内部,而对象形式本身即可识别。健壮的处理器会检查 data,而不是信任配置标志,因为配置错误的订阅与解析器缺失在其他情况下无法区分。

  • 信封:context.slot 加上包含 lamports、owner、data、executable、rentEpoch 的 value。
  • 编码之间只有 value.data 的形态变化。
  • 在一处规范化 data;让处理器其余部分与编码无关。

可运行的 Node.js 示例:使用 base64 与 jsonParsed 订阅

下面的示例对同一账户打开两个订阅,一个使用 encoding base64,一个使用 jsonParsed,并打印各自产生的数据形态。它使用 ws 包和全局 fetch 进行初始账户读取。请将端点和账户公钥替换为你自己的。目标是观察你自己端点上的形态,而不是相信截图。

注意规范化函数:它检测元组形式,解码 base64 并报告字节长度;它检测对象形式并报告 parsed 是否为 null。这个单一函数就是要带入生产环境的模式,因为它让解析器缺失变得可见而不是静默。

// npm install ws
import WebSocket from 'ws';

const WS_URL = process.env.SOLANA_WS_URL || 'wss://your-endpoint.example';
const ACCOUNT = process.env.ACCOUNT_PUBKEY || 'YourAccountPubkeyHere';

function normalizeData(data) {
  if (Array.isArray(data)) {
    const [payload, encoding] = data;
    const bytes = Buffer.from(payload, 'base64');
    return { kind: 'raw', encoding, byteLength: bytes.length };
  }
  if (data && typeof data === 'object') {
    return {
      kind: 'parsed',
      program: data.program,
      parsedIsNull: data.parsed === null,
      parsed: data.parsed,
    };
  }
  return { kind: 'unknown', data };
}

function subscribe(encoding) {
  const ws = new WebSocket(WS_URL);
  ws.on('open', () => {
    ws.send(JSON.stringify({
      jsonrpc: '2.0',
      id: 1,
      method: 'accountSubscribe',
      params: [ACCOUNT, { encoding, commitment: 'confirmed' }],
    }));
  });
  ws.on('message', (raw) => {
    const msg = JSON.parse(raw.toString());
    if (msg.method === 'accountNotification') {
      const { slot } = msg.params.result.context;
      const { owner, data } = msg.params.result.value;
      console.log(encoding, 'slot', slot, 'owner', owner, normalizeData(data));
    } else {
      console.log(encoding, 'control message', msg);
    }
  });
  ws.on('error', (err) => console.error(encoding, 'ws error', err.message));
  return ws;
}

const a = subscribe('base64');
const b = subscribe('jsonParsed');

// Close after 60s for a bounded experiment.
setTimeout(() => { a.close(); b.close(); }, 60000);

结果表:在你的端点上测量编码行为

下表是模板,不是一组实测值。请对照你自己的端点和账户填写,因为线上大小、客户端工作量和解析器行为取决于账户和节点版本。运行上面的示例,每种编码捕获一条通知,并记录观察到的值。不要从任何文章(包括本文)复制数字。

对于线上大小,在 JSON.parse 之前记录原始消息长度。对于客户端工作量,注明你是否需要 Borsh 布局或 zstd 依赖。对于解析器敏感性,如果可能,比较两个节点版本或两个提供商的 parsed 输出,并记录任何字段差异。重点是为你的工作负载把权衡具体化。

  • 编码 | 线上大小(字节) | 客户端工作量 | 解析器版本敏感性 | 观察到 parsed 为 null?
  • base64 | 测量 | 需要 Borsh 布局 | 无(字节稳定) | 不适用
  • base64+zstd | 测量 | Borsh 布局加 zstd 解压器 | 无 | 不适用
  • jsonParsed | 测量 | 受支持程序无需额外工作 | 是,解析器输出可能变化 | 是,自定义程序账户

反序列化成本与解析器版本漂移

核心权衡在于反序列化发生在哪里。base64 把工作移到你的客户端,客户端必须维护与链上程序匹配的 Borsh 布局。该布局是维护负担,但它由你版本化,因此节点升级不会静默改变数据的含义。这就是生产索引器通常请求 base64 以保证稳定性的原因。

jsonParsed 把工作移到节点。这很方便,免去了布局负担,但引入了对节点解析器的依赖。解析器输出是程序特定的,可能随节点解析器变化而变化,这意味着同一账户在不同节点版本或提供商上可能产生不同的解析结构。对于检查和调试,这没问题。对于长期运行的索引器,这是稳定性风险。

一个实用模式是:用 jsonParsed 做人工检查和一次性脚本,用 base64 处理任何要写入数据库或供下游消费者使用的内容。如果两者都需要,可以订阅两次并协调,或者用 base64 订阅并在本地用你可控的布局解析。Solana WebSocket API 页面在你搭建这些时是方法面的有用参考。

  • base64:客户端负责反序列化;跨节点升级稳定。
  • jsonParsed:节点负责反序列化;输出可能随解析器变化而漂移。
  • 常见模式:jsonParsed 用于检查,base64 用于索引。

故障排查:parsed 为 null、结构不匹配与订阅 ID

当 parsed 为 null 时,该账户由运行时没有解析器的程序拥有。这是文档行为,不是错误。修复方法是回退到原始字节,这意味着你需要该程序的 Borsh 布局。如果你无法维护,考虑你是否真的需要该账户,或者像 带 dataSlice 和过滤器的 getProgramAccounts 这样的其他方法是否更适合该访问模式。

当你看到意外的元组或对象形态时,检查 data 内部的编码标签。带有 "base64" 的元组意味着你用 base64 订阅;对象意味着 jsonParsed。如果你的处理器假设了一种却收到了另一种,说明订阅配置与处理器不同步。在一处规范化,并在开发期间首次通知时记录形态。

对于压缩载荷,在解压前确认标签为 "base64+zstd"。尝试对 zstd 载荷直接 base64 解码而不解压会产生垃圾字节,这些字节可能仍会反序列化成无意义内容。在信任解码结构之前,用 getAccountInfo 读取来验证一个已知字段,例如 lamports。

对于订阅 ID 关联,存储 accountSubscribe 响应返回的 id,并将其与每条通知中的 subscription 字段匹配。如果你打开多个订阅,包括对同一账户使用不同编码,id 是区分它们的唯一可靠方式。logsSubscribe 通知解析 文章介绍了日志订阅的同样关联纪律。

如果通知停止到达,检查连接生命周期而不是编码。WebSocket 连接可能断开,而连接断开意味着无论编码如何都没有通知。重连逻辑和重新订阅在 Solana WebSocket 指南 中有所介绍。

  • parsed 为 null:所属程序没有解析器;回退到原始字节。
  • 结构不匹配:检查 data 内部的编码标签。
  • 压缩载荷:先解压再 base64 解码;验证已知字段。
  • 通过订阅 id 关联通知,而不是按到达顺序。

限制:解析器覆盖、版本漂移与账户范围

jsonParsed 的覆盖范围仅限于运行时已知的程序。自定义程序不会被解析,而可解析程序的集合也不是你能控制的。把 jsonParsed 当作受支持程序的便利功能,而不是通用表示。任何假设通用解析的客户端都会丢弃自定义程序账户的更新。

解析器输出可能随节点版本和提供商漂移。文档行为是 parsed 结构因程序而异;实际后果是你不应把 parsed 输出当作长期存储的稳定模式。如果你需要稳定性,base64 是更安全的契约,因为字节就是程序自身的序列化。

accountSubscribe 监视单个账户。它不是程序范围的订阅,也不会给你导致变更的交易。如果你需要原因,可以配合 logsSubscribe 或按签名获取交易。如果你需要许多账户,考虑 getProgramAccounts 或程序订阅模式,并注意你的提供商的成本和速率特性,这些因提供商和套餐而异。关于端点选项,请参阅 Solana 网络 和 RPC 定价。

  • jsonParsed 只覆盖运行时已知的程序;自定义程序返回 parsed 为 null。
  • 解析输出可能随节点版本和提供商漂移。
  • accountSubscribe 是按账户的,不包含导致变更的交易。

后续步骤:选择编码并接入你的技术栈

先在你的端点上运行上面的示例并填写结果表。这会给你一个具体的决策依据,而不是经验法则。如果你的账户由受支持的程序拥有且你只需要检查,jsonParsed 很方便。如果你在做索引,base64 是稳定的默认选择,而如果带宽受限,base64+zstd 值得测量。

然后加固处理器:在一个函数中规范化 data,根据结构分支,在首次通知时记录编码标签,并按订阅 id 关联。添加重连逻辑和重新订阅,这样连接断开不会静默停止你的更新。如果你在 OnFinality 上构建,Solana WebSocket API 参考和 API 服务 页面描述了端点面,OnFinality Learn 中心 收集了相关指南,包括 读取 Solana 账户信息、租金和代币账户。

最后,决定解析在你的架构中位于何处。如果你在本地解析,请将 Borsh 布局与程序一起版本化,并针对已知账户测试它们。如果你依赖 jsonParsed,请将你的期望固定到某个节点版本并监控漂移。无论哪种方式,都要在配置中明确编码选择并在日志中可见,这样下一个调试结构不匹配的人就能立即看到。

  • 先测量:在你的端点上填写结果表。
  • 加固处理器:规范化、分支、记录日志、关联。
  • 决定解析位于何处,并有意识地版本化。

永远不用担心基础设施

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

开始