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

Solana jsonParsed 与 base64:交易编码对比

在 Solana 交易读取中选择 jsonParsed 还是 base64,需要理解解析器的保证、版本化消息解析以及客户端解码的权衡。

TL;DR

Solana JSON-RPC 交易读取接受一个 encoding 参数,用于控制节点返回原始序列化字节(base64 或 base64+zstd)还是尽力而为的结构化解释(jsonParsed)。jsonParsed 仅解码节点识别其程序的指令;未知程序会以 partiallyDecoded 形式出现并携带原始数据,因此假设完全解码的消费者在首次遇到不熟悉的程序时就会失败。base64 返回确定性字节,不依赖节点解析器覆盖范围或版本,使其成为索引器和跨提供商可复现性的稳定选择,代价是需要维护客户端 Borsh 或 bincode 解码器。版本化交易增加了一个复杂点:解析指令账户需要按文档顺序合并静态消息密钥与从查找表加载的地址。本文解释编码词汇、每种编码提供的保证、与 maxSupportedTransactionVersion 的交互,并提供可运行的 Node.js 示例,对比两种编码并仅从 base64 重建指令。

Solana 交易读取的编码词汇

返回交易的 Solana JSON-RPC 方法——getTransaction、getBlock 以及 transactionSubscribe 系列——接受一个 encoding 参数,用于决定交易负载的表示形式。文档中列出的值有 base64(原始序列化交易字节)、base64+zstd(用 zstd 压缩的相同字节)和 jsonParsed(对消息、指令及已加载地址的尽力而为的结构化解释)。请求和响应信封遵循 JSON-RPC 2.0 规范,因此编码选择只影响结果负载,不影响帧结构。

encoding 参数不是格式偏好;它在两种根本不同的契约之间做出选择。base64 和 base64+zstd 返回规范的序列化交易。jsonParsed 返回节点生成的解释,取决于节点的解析器覆盖范围和软件版本。Solana 文档中关于 getTransaction 的页面列举了这些值及响应结构,而 RPC JSON 结构 页面记录了 jsonParsed 和 partiallyDecoded 指令形式。

对于在托管端点上构建的团队,编码决策与提供商无关。Solana 网络页面 描述了 RPC 接口,而 Solana RPC 方法参考 列出了接受 encoding 的方法。选择权在你;节点只是遵从。

  • base64:原始序列化交易字节,跨节点和版本具有确定性。
  • base64+zstd:相同字节的压缩形式;在 base64 解码前需要 zstd 解码器。
  • jsonParsed:结构化的消息和指令,尽力而为,对节点版本敏感。

jsonParsed 实际保证什么

jsonParsed 并不保证每条指令都被解码。运行时将其理解其程序的指令解析为结构化形式,其余则保留为 partiallyDecoded,携带原始指令数据和账户索引。假设每条指令都有 parsed 字段的消费者会在遇到第一个未知程序时崩溃。这是“jsonParsed 不一致”错误报告最常见的单一来源:如果解析器覆盖范围不同,同一笔交易在一个节点上可能完全解析,在另一个节点上则部分解码。

partiallyDecoded 形式不是错误。它是节点无法解释的指令的文档化表示。你的代码必须根据 parsed 与 partiallyDecoded 的存在进行分支,并在需要时回退到原始数据。RPC JSON 结构 页面并排展示了两种形式。

由于 jsonParsed 是尽力而为的,它不适合作为索引器的唯一来源,因为索引器必须跨提供商或随时间逐字段比较记录。即使底层交易字节完全相同,当节点的解析器更新时,结构化字段也可能发生变化。

  • parsed:指令数据和账户由节点结构化。
  • partiallyDecoded:返回原始数据和账户索引;客户端必须解码。
  • 节点版本和解析器覆盖范围决定你收到哪种形式。

为什么 base64 是索引器的稳定选择

base64 返回的交易字节与账本中包含的序列化交易完全一致。这些字节从不依赖节点的解析器覆盖范围或软件版本,因此客户端拥有 Borsh 或 bincode 布局,数据可跨提供商和随时间进行差异比较和复现。代价是维护解码器:你必须自己跟踪程序布局和账户结构。

对于索引器,base64 的确定性胜过 jsonParsed 的便利性。基于 base64 构建的记录可以与来自另一个提供商的记录逐字节比较。基于 jsonParsed 构建的记录则不能,因为结构化字段是节点生成的解释。如果你既需要便利性又需要确定性,请获取 base64 并在本地解码,或获取两种编码并进行协调。

版本化交易与 getBlock 解析 文章介绍了 getBlock 如何返回版本化交易以及为什么原始字节是规范来源。getTransaction meta 与内部指令 文章介绍了发送后的 meta 对象,它与编码选择是分开的。

  • 跨节点、提供商和时间具有确定性。
  • 需要客户端 Borsh 或 bincode 解码器。
  • 支持字节级差异比较和可复现的索引。

版本化交易解析与账户查找表

在解析指令的账户之前,v0 消息的静态密钥和从地址查找表解析出的地址都必须存在。解码器必须按文档顺序将消息的账户密钥与已加载地址合并,而不是仅索引静态密钥列表。如果你只索引静态密钥,引用已加载地址的指令账户将解析为错误的公钥或超出范围。

jsonParsed 并未消除这一要求;节点在能够执行合并时会为你执行,但底层解析规则相同。当你自己解码 base64 时,必须实现合并。版本化交易与 getBlock 解析 文章详细介绍了顺序和查找表机制。

编码与 getTransaction 的 maxSupportedTransactionVersion 参数相互作用。当请求的版本不受支持时,版本门控的响应可能是错误而非解码后的消息,jsonParsed 不会移除该门控。你必须将 maxSupportedTransactionVersion 设置为你能够处理的最高版本,否则节点可能拒绝请求。

  • 按文档顺序将静态账户密钥与已加载地址合并。
  • jsonParsed 在能够执行合并时会执行,但规则不变。
  • 无论编码如何,maxSupportedTransactionVersion 都会门控响应。

可运行对比:同一签名的 base64 与 jsonParsed

以下 Node.js 脚本两次获取同一交易签名——一次使用 base64,一次使用 jsonParsed——并报告两者不一致之处。它突出显示 partiallyDecoded 指令和缺失的内部指令。通过设置 RPC_URL 环境变量,针对你自己的端点运行它。该脚本使用 Node.js 18+ 中可用的内置 fetch API。

该脚本并不断言哪种编码更好;它呈现差异以便你决定。输出是一份报告,你可以用自己的程序布局进行扩展。

const RPC_URL = process.env.RPC_URL || 'https://your-endpoint.example';
const SIGNATURE = process.env.SIGNATURE;

async function rpc(method, params) {
  const res = await fetch(RPC_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  const json = await res.json();
  if (json.error) throw new Error(JSON.stringify(json.error));
  return json.result;
}

async function fetchBoth(signature) {
  const base64 = await rpc('getTransaction', [
    signature,
    { encoding: 'base64', maxSupportedTransactionVersion: 0 }
  ]);
  const jsonParsed = await rpc('getTransaction', [
    signature,
    { encoding: 'jsonParsed', maxSupportedTransactionVersion: 0 }
  ]);
  return { base64, jsonParsed };
}

function reportDifferences(base64, jsonParsed) {
  const b64Ix = base64.transaction.message.instructions;
  const jpIx = jsonParsed.transaction.message.instructions;
  console.log('base64 instruction count:', b64Ix.length);
  console.log('jsonParsed instruction count:', jpIx.length);
  jpIx.forEach((ix, i) => {
    if (ix.parsed === undefined) {
      console.log(`jsonParsed instruction ${i} is partiallyDecoded`);
    }
  });
  const b64Inner = base64.meta?.innerInstructions || [];
  const jpInner = jsonParsed.meta?.innerInstructions || [];
  console.log('base64 inner instruction groups:', b64Inner.length);
  console.log('jsonParsed inner instruction groups:', jpInner.length);
}

(async () => {
  const { base64, jsonParsed } = await fetchBoth(SIGNATURE);
  reportDifferences(base64, jsonParsed);
})();

仅从 base64 重建指令列表

要从 base64 重建指令,你必须反序列化交易、解析账户密钥(包括 v0 的已加载地址),并根据其程序布局解码每条指令的数据。以下 Node.js 示例使用 @solana/web3.js 反序列化 base64 交易,并打印带有程序 ID 和账户密钥的指令列表。它不解码指令数据;那需要特定于程序的布局。

这种方法为你提供一个确定性的指令列表,不依赖节点的解析器。你可以通过为你关心的程序添加 Borsh 或 bincode 解码器来扩展它。getTransaction meta 与内部指令 文章介绍了如何将其与 meta 对象配对以处理内部指令。

const { Connection, PublicKey, VersionedTransaction } = require('@solana/web3.js');

const RPC_URL = process.env.RPC_URL || 'https://your-endpoint.example';
const SIGNATURE = process.env.SIGNATURE;

(async () => {
  const connection = new Connection(RPC_URL, 'confirmed');
  const tx = await connection.getTransaction(SIGNATURE, {
    encoding: 'base64',
    maxSupportedTransactionVersion: 0
  });
  if (!tx) throw new Error('Transaction not found');
  const raw = Buffer.from(tx.transaction[0], 'base64');
  const decoded = VersionedTransaction.deserialize(raw);
  const message = decoded.message;
  const staticKeys = message.staticAccountKeys.map(k => k.toBase58());
  const loaded = tx.meta?.loadedAddresses || { writable: [], readonly: [] };
  const allKeys = [
    ...staticKeys,
    ...loaded.writable,
    ...loaded.readonly
  ];
  message.compiledInstructions.forEach((ix, i) => {
    const programId = allKeys[ix.programIdIndex];
    const accounts = ix.accountKeyIndexes.map(idx => allKeys[idx]);
    console.log(`Instruction ${i}: program=${programId}`);
    console.log('  accounts:', accounts.join(', '));
    console.log('  data length:', ix.data.length);
  });
})();

决策表:编码 vs 确定性 vs 客户端工作量 vs 节点版本敏感性

下表总结了权衡。通过使用每种编码获取同一签名并记录结果,对照你自己的端点验证每一行。该表是指南,不是基准;你节点的解析器覆盖范围和版本决定实际行为。

使用该表为每个用例选择编码。对于索引器,base64 是稳定选择。对于探索性工具,jsonParsed 减少客户端工作量。对于带宽受限的环境,base64+zstd 以减少负载大小,代价是增加解压缩步骤。

  • base64:确定性高;客户端工作量大(完整解码器);节点版本敏感性低。
  • base64+zstd:确定性高;客户端工作量大外加 zstd;节点版本敏感性低。
  • jsonParsed:确定性低;对已知程序客户端工作量小;节点版本敏感性高。
  • 带 partiallyDecoded 回退的 jsonParsed:确定性中等;客户端工作量中等;节点版本敏感性中等。

针对你自己的端点测量编码行为

由于解析器覆盖范围和节点版本各不相同,你应该测量端点的行为,而不是依赖一般性说法。以下方法可复现:使用两种编码获取一组签名,统计 partiallyDecoded 指令,并记录差异。用你自己的数字填写结果表。

在包含已知程序(System、Token、Associated Token)和至少一个不熟悉程序的签名样本上运行前面部分中的比较脚本。记录计数。在任何节点升级后重复测量,以检测解析器覆盖范围的变化。

  • 结果表列:签名、base64 指令数、jsonParsed 指令数、partiallyDecoded 数、内部指令组数(base64)、内部指令组数(jsonParsed)。
  • 第 1 行:[填入你的测量结果]
  • 第 2 行:[填入你的测量结果]
  • 第 3 行:[填入你的测量结果]

排查与编码相关的故障

partiallyDecoded 意外:如果你的代码假设每条指令都有 parsed 字段,它会在 partiallyDecoded 上抛出异常。根据 parsed 的存在进行分支,并回退到原始数据。这是文档化行为,不是错误。

maxSupportedTransactionVersion 错误:如果节点返回关于不支持交易版本的错误,请将 maxSupportedTransactionVersion 设置为你能够处理的最高版本。jsonParsed 不会绕过此门控。已弃用方法的迁移指南 涵盖了相关的版本化变更。

base64+zstd 解码失败:确保在 base64 解码之前使用 zstd 解码器解压缩。一个常见错误是直接对压缩字节进行 base64 解码,这会产生乱码。accountSubscribe 编码文章 涵盖了订阅的相同编码词汇。

编码之间的字段差异:如果你比较基于 base64 的记录与基于 jsonParsed 的记录,预期在指令表示、账户密钥排序和内部指令分组方面存在差异。在未归一化为共同表示之前,不要逐字段比较它们。

  • 根据 parsed 与 partiallyDecoded 进行分支。
  • 显式设置 maxSupportedTransactionVersion。
  • 在 base64 解码前解压缩 zstd。
  • 跨编码比较前先归一化。

限制与权衡

jsonParsed 是尽力而为的,且对节点版本敏感。它对已知程序很方便,但不适合作为确定性索引的唯一来源。base64 是确定性的,但需要客户端解码器,且随着程序布局演变你必须维护它。base64+zstd 增加了压缩依赖。

版本化交易需要将静态密钥与已加载地址合并;不这样做会导致账户解析错误。编码选择不会消除这一要求。Solana 网络页面 和 RPC 定价 页面描述了端点接口和成本模型;API 服务 页面描述了托管访问。

没有任何编码能消除理解交易格式的需要。根据你重视确定性(base64)还是减少客户端工作量(jsonParsed)来选择,并针对你自己的端点进行测量。

  • jsonParsed:客户端工作量低,确定性低。
  • base64:客户端工作量高,确定性高。
  • 版本化交易:无论编码如何都要合并密钥。

集成后续步骤

首先使用两种编码获取一个已知签名并运行比较脚本。在结果表中记录差异。然后决定哪种编码适合你的用例。对于索引器,为你关心的程序构建 base64 解码器。对于工具,使用带 partiallyDecoded 回退的 jsonParsed。

查看 OnFinality Learn 中心 获取有关 Solana RPC 集成的相关文章,以及 Solana RPC 方法参考 获取完整方法列表。accountSubscribe 编码文章 涵盖了订阅编码,版本化交易文章 涵盖了 getBlock 解析。

测量、决定,并在集成笔记中记录你的编码选择,以便未来的维护者理解为什么客户端解码 base64 或依赖 jsonParsed。

  • 在你的端点上运行比较脚本。
  • 为确定性选择 base64,为便利性选择 jsonParsed。
  • 记录选择及回退行为。

永远不用担心基础设施

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

开始