交易发送后,getTransaction 返回的 meta 对象携带权威的执行后结果:err、logMessages、innerInstructions、余额变化和计算用量。err 字段可以是普通字符串,也可以是诸如 { InstructionError: [index, reason] } 的对象,其中 index 指向消息中的外层指令,而非内层指令。innerInstructions[].index 使用相同的外层指令索引,因此必须先将每个内层列表折叠回其父外层指令,才能确定失败的程序。本文逐一讲解 meta 字段、索引关联规则、err 形态,以及一个可运行的 @solana/web3.js 解码器,用于打印关联后的指令树。文章还涵盖导致 null 结果或误导性错误的失败模式,以及如何针对自己的端点测量行为。
为什么 getTransaction 是发送后解码方法
getTransaction 是 Solana JSON-RPC 方法,按签名返回已确认的交易,包括其 meta 对象。一旦交易已提交且需要权威执行结果,它就是正确的工具。关于发送前模拟的姊妹文章 解码 Solana simulateTransaction 错误 介绍了节点针对当前状态执行但不提交的预发送模拟;而 getTransaction 报告的是链上实际发生的情况。
请求结构很重要。最小调用传入签名以及包含 encoding、commitment 和 maxSupportedTransactionVersion 的配置对象。encoding 控制指令和账户键的返回方式;jsonParsed 对标准程序很方便,而 json 或 base64 保留原始编译数据。commitment 级别决定节点从哪个账本状态读取,processed、confirmed 和 finalized 的语义在 Solana 承诺级别:processed vs confirmed vs finalized 中有详细说明。
省略 maxSupportedTransactionVersion 是版本化(v0)交易失败的常见原因。Solana RPC 文档中关于 getTransaction 的说明 指出,当交易使用版本化消息时该字段是必需的;没有它,节点无法知道要解码哪种消息格式,会返回错误而不是结果。除非有特定理由请求其他受支持版本,否则始终将其设为 0。
- 提交后使用 getTransaction;提交前使用 simulateTransaction。
- 将 encoding 设为 jsonParsed 以获得可读指令,或设为 base64 以保留原始保真度。
- 显式设置 commitment,以便知道正在读取哪个账本状态。
- 对于 v0 交易,将 maxSupportedTransactionVersion 设为 0。
curl -s https://api.mainnet-beta.solana.com -X POST -H 'Content-Type: application/json' -d '{
"jsonrpc": "2.0",
"id": 1,
"method": "getTransaction",
"params": [
"YOUR_SIGNATURE_HERE",
{ "encoding": "jsonParsed", "commitment": "confirmed", "maxSupportedTransactionVersion": 0 }
]
}'meta 对象逐字段解析
meta 对象是执行后的摘要。Solana RPC 文档中关于 getTransaction 的说明 定义了其字段,每个字段回答不同的问题。err 告诉你执行是否失败,以及如何失败。status 是较新的确认摘要,可携带 Ok 或 Err。fee 报告扣除的 lamports。preBalances 和 postBalances 给出每个账户索引的 lamport 变化,而 preTokenBalances 和 postTokenBalances 对 SPL 代币账户做同样的事。
logMessages 是有序的程序日志流,包括诸如 'Program log:' 和 'Program ... failed' 的行。loadedAddresses 列出为版本化交易加载的地址表账户,这很重要,因为指令账户索引可能引用它们。computeUnitsConsumed 报告使用的计算预算。innerInstructions 包含由外层指令触发的跨程序调用(CPI)。rewards 在交易涉及质押或投票路径时列出奖励。
没有任何单一字段是足够的。err 指出失败但不总是程序;logMessages 提供叙述上下文但可能被截断;innerInstructions 提供结构但需要索引关联。将 meta 视为一组相互印证的信号,在向用户呈现原因之前先进行核对。
- err:失败指示器,字符串或对象形式。
- status:确认摘要,Ok 或 Err。
- fee:交易扣除的 lamports。
- preBalances / postBalances:按账户索引的 lamport 变化。
- preTokenBalances / postTokenBalances:按代币账户的代币变化。
- logMessages:有序的程序日志行。
- loadedAddresses:版本化交易的地址表账户。
- computeUnitsConsumed:使用的计算预算。
- innerInstructions:按外层指令索引分组的 CPI。
- rewards:适用时的质押或投票奖励。
innerInstructions 的索引关联规则
最重要的解码规则是 innerInstructions[].index 指的是外层指令在交易消息中的位置,而不是内层指令。每个条目分组了因该外层指令而运行的内层指令。Solana RPC 文档中关于 InnerInstruction 和 CompiledInstruction 的说明 确立了这种索引方式。要归因失败,必须将每个内层列表折叠回其父外层指令。
一个实用的思考方式:消息索引 i 处的外层指令拥有 innerInstructions 中 index 等于 i 的内层指令。如果钱包程序作为内层指令出现,它是由该索引处的外层指令调用的,默认不是由交易的第一条指令调用。这就是为什么按顺序读取 innerInstructions 并假设它们属于第一条指令的朴素解析器会错误归因失败。
构建关联树时,按顺序遍历消息指令,将匹配的内层列表附加到每条指令,然后在合并集合中搜索失败的程序 id。失败的程序是 'Program ... failed' 日志行中命名的程序,如果存在,也是 InstructionError 索引中命名的程序。在告诉用户哪个程序拒绝了交易之前,交叉核对两者。
- innerInstructions[].index 是消息中的外层指令索引。
- 内层指令在 [index, 下一个 index) 范围内运行。
- 在归因之前,将内层列表折叠到其父外层指令上。
- 从日志和 InstructionError 索引中核对失败的程序 id。
err 对象形态及如何呈现
err 字段有两种主要形式。字符串形式出现在交易级失败中,例如 'AccountInUse' 或 'BlockhashNotFound'。这些不绑定到特定指令,通常表示提交或状态条件,而不是程序拒绝。对象形式出现在指令级失败中,最常见的是 { InstructionError: [index, reason] },其中 index 是外层指令位置,reason 描述失败。
reason 本身可以嵌套。程序定义的失败通常表现为 { Custom: n },其中 n 是程序特定的错误码。Solana RPC 文档中关于 TransactionError 的说明 列举了标准变体,程序自己的文档将 Custom 码映射到含义。向用户呈现时,使用消息将外层索引转换为指令的程序 id,然后使用程序的错误表转换 Custom 码。
不要将提交时的 JSON-RPC 错误与 meta.err 混为一谈。提交时返回的错误,例如 -32002 Transaction simulation failed、-32003 Transaction signature verification failure 和 -32005 node is behind,遵循 JSON-RPC 2.0 错误对象封装,包含 code、message 和 data,如 JSON-RPC 2.0 规范所定义。这些是传输层或预接受错误;meta.err 是链上结果。
- 字符串形式:交易级条件,不是指令特定的。
- 对象形式:{ InstructionError: [index, reason] } 用于指令失败。
- 嵌套形式:{ Custom: n } 用于程序定义的错误码。
- 提交错误使用 JSON-RPC 2.0 的 code/message/data 封装。
使用 @solana/web3.js 的可运行 Node.js 解码器
以下示例按签名获取交易,读取 meta.err,遍历 meta.logMessages 查找失败行,并打印关联后的指令树,指出失败的程序。它使用 @solana/web3.js,并假设已配置 RPC 端点。将端点和签名替换为你自己的值。
解码器使用索引规则将 innerInstructions 折叠到其父外层指令上,然后在合并集合中搜索失败日志中命名的程序 id。它打印外层指令索引、程序 id 以及其下的任何内层指令。这给你一个可归因的失败,而不是原始错误字符串。
const { Connection, PublicKey } = require('@solana/web3.js');
async function decodeTransaction(endpoint, signature) {
const connection = new Connection(endpoint, 'confirmed');
const tx = await connection.getTransaction(signature, {
commitment: 'confirmed',
maxSupportedTransactionVersion: 0,
});
if (!tx) {
console.log('No transaction found for signature:', signature);
return;
}
const meta = tx.meta;
console.log('err:', JSON.stringify(meta.err));
console.log('computeUnitsConsumed:', meta.computeUnitsConsumed);
const message = tx.transaction.message;
const accountKeys = message.staticAccountKeys || message.accountKeys;
const outerInstructions = message.compiledInstructions || message.instructions;
const innerByIndex = new Map();
for (const group of meta.innerInstructions || []) {
innerByIndex.set(group.index, group.instructions);
}
const failingPrograms = new Set();
for (const line of meta.logMessages || []) {
const failed = line.match(/Program (\S+) failed/);
if (failed) failingPrograms.add(failed[1]);
}
outerInstructions.forEach((ix, outerIndex) => {
const programId = accountKeys[ix.programIdIndex].toString();
const inner = innerByIndex.get(outerIndex) || [];
const isFailing = failingPrograms.has(programId);
console.log(
`outer[${outerIndex}] program=${programId}${isFailing ? ' <-- FAILED' : ''}`
);
inner.forEach((innerIx, innerIndex) => {
const innerProgramId = accountKeys[innerIx.programIdIndex].toString();
const innerFailing = failingPrograms.has(innerProgramId);
console.log(
` inner[${outerIndex}.${innerIndex}] program=${innerProgramId}${innerFailing ? ' <-- FAILED' : ''}`
);
});
});
if (meta.err && meta.err.InstructionError) {
const [index, reason] = meta.err.InstructionError;
const programId = accountKeys[outerInstructions[index].programIdIndex].toString();
console.log(`InstructionError at outer[${index}] program=${programId} reason=${JSON.stringify(reason)}`);
}
}
decodeTransaction('YOUR_RPC_ENDPOINT', 'YOUR_SIGNATURE');关联日志、内层指令和程序 ID
日志关联是原始 err 对象与人类可读解释之间的桥梁。'Program log:' 行显示程序打印的内容,'Program ... failed' 行指出中止的程序。由于内层指令按外层索引分组,你可以同时遍历日志流和指令树,查看哪个外层指令触发了失败的 CPI。
一个可靠的流程是:首先从日志行定位失败的程序 id,然后在关联树中找到该程序 id 的每次出现,接着检查 InstructionError 索引是否指向拥有它的外层指令。如果失败的程序仅作为内层指令出现,那么该索引处的外层指令就是调用它的入口点。当钱包或聚合器程序调用拒绝转账的代币程序时,这种区分很重要。
Solana Cookbook 和 @solana/web3.js 源码记录了内层指令、日志消息和按索引的程序 id 的客户端解码路径。将它们作为字段名和编译指令布局的参考,尤其是在 jsonParsed 和 base64 编码之间切换时。
- 从 'Program ... failed' 日志行找到失败的程序 id。
- 在关联的指令树中定位该程序 id。
- 检查 InstructionError 索引是否指向拥有的外层指令。
- 归因时区分外层入口点和内层 CPI。
结果表:针对自己的端点测量
提供商行为各不相同,因此应针对自己的端点测量,而不是依赖一般性说法。下表是模板:用你的 RPC 提供商观察到的结果填写每一行。不要将任何行视为固定预期,因为记录的行为因提供商和承诺级别而异。
对每个使用的端点运行相同的签名并记录结果。这会在影响生产之前暴露日志截断、null 处理和版本化交易支持方面的差异。
- Endpoint:你测试的 RPC URL。
- Commitment:你请求的级别。
- Result:交易对象或 null。
- meta.err:观察到的 err 值。
- logMessages count:返回的日志行数。
- innerInstructions groups:返回的组数。
- maxSupportedTransactionVersion:不带它调用是否成功。
- Notes:观察到的任何截断或提供商特定行为。
失败模式:null 结果、承诺级别和截断
null 结果是最常见的意外。当签名在请求的承诺级别未找到时,getTransaction 返回 null,这通常意味着交易尚未确认或节点尚未跟上。如果你请求 finalized 但交易仅 confirmed,在最终化之前可能看到 null。用较低的承诺级别重试或等待,并考虑 Solana RPC 超时与重试策略 中的重试指导。
如前所述,省略 maxSupportedTransactionVersion 会在 v0 交易上失败。承诺级别不匹配会产生 null 或陈旧数据,而不是显式错误,因此始终记录你使用的承诺级别。一些提供商会截断 logMessages,这可能隐藏 'Program ... failed' 行;如果日志看起来很短,用第二个端点或区块浏览器交叉核对。
当你在扫描历史而不是获取单笔交易时,签名查找本身可以分页。如果你先枚举签名,请参阅 Solana getSignaturesForAddress 分页 了解游标模式,然后将每个签名输入 getTransaction。
- null 结果:在请求的承诺级别未找到,或节点滞后。
- 承诺级别不匹配:null 或陈旧数据,没有显式错误。
- 缺少 maxSupportedTransactionVersion:v0 交易失败。
- 日志截断:提供商特定,可能隐藏失败程序行。
局限性与权衡
getTransaction 给你链上结果,但不解释意图。Custom 错误码只有结合程序的错误表才有意义,截断的日志流可能使失败模糊不清。meta 对象也反映你请求的承诺级别时的状态,因此 processed 读取可能与 finalized 读取不同。没有单一字段在所有情况下都能指出失败的程序;归因需要核对 err、日志和内层指令。
性能和可用性权衡取决于提供商。较高的承诺级别更安全但返回更慢,一些提供商限制历史查询或日志保留。对于生产系统,按签名缓存解码结果,并同时存储承诺级别,以便以后重现确切的视图。Solana RPC API 指南(RPC Assistant) 涵盖端点选择和方法覆盖,RPC 定价 描述计划级差异。
- Custom 码需要程序自己的错误表。
- 截断的日志可能使归因模糊。
- 承诺级别改变 meta 对象反映的内容。
- 缓存解码结果并附带承诺级别以保证可重现性。
顽固交易的故障排查清单
当交易无法干净解码时,按顺序完成清单。首先确认签名正确,并且交易在你请求的承诺级别已确认。其次,如果缺少 maxSupportedTransactionVersion,请添加它。第三,比较两个端点上的 logMessages 长度以检测截断。第四,验证你的内层指令折叠使用外层索引,而不是内层顺序。
如果 err 对象是诸如 'BlockhashNotFound' 的字符串,交易可能从未落地;将其视为提交问题而不是程序失败。如果 err 是 { InstructionError: [index, reason] } 且带有嵌套的 { Custom: n },解析该外层索引处的程序 id,并在程序文档中查找 n。对于版本化交易,请记住账户索引可能引用 loadedAddresses,这在 Solana 版本化交易与 getBlock 解析 中有介绍。
- 解码前确认签名和承诺级别。
- 为 v0 交易添加 maxSupportedTransactionVersion。
- 比较端点间的日志长度以检测截断。
- 按外层索引折叠内层指令,而不是按内层顺序。
- 根据程序的错误表解析 Custom 码。
后续步骤与相关阅读
要深入了解,请从 OnFinality Learn 中心 开始完整的 RPC 故障排查路径,并查看 Solana 网络页面 了解端点和网络背景。Solana RPC API 指南(RPC Assistant) 涵盖方法级细节,API 服务 页面解释如何将托管端点连接到你的应用程序。
对于相邻的解码主题,请阅读 解码 Solana simulateTransaction 错误 了解发送前路径,Solana 承诺级别:processed vs confirmed vs finalized 了解确认语义,以及 Solana getSignaturesForAddress 分页 了解历史扫描。这些共同覆盖了从提交到发送后归因的完整生命周期。
- Learn 中心:/en/learn
- Solana 网络:/en/networks/solana
- RPC Assistant 指南:/en/rpc-assistant/solana-api-guide
- API 服务:/en/api-service
- RPC 定价:/en/pricing/rpc