Skip to main content
当您从Laserstream接收交易数据时,有两个重要的方面需要注意:
  • Message → 用户想要做什么(他们签署的提案)
  • Meta → 实际发生了什么(执行结果)
挑战: 原始交易数据以二进制字节数组形式出现,如<Buffer 00 bf a0 e8...>,而不是可读的地址和签名。 本指南将向您展示如何: 将这些二进制数据解码为人类可读的格式,提取有意义的信息,并理解从提案到执行的完整交易过程。

实时流,无需解码

运行下面的最小客户端。过滤器标志去除投票和失败的交易,accountInclude数组限制结果为涉及Jupiter程序ID的活动。
你的控制台现在显示一个包装器—filters, createdAt加上一个transaction分支,隐藏了两个子项:
  • transaction.transaction.transaction → 签名的消息
  • transaction.transaction.meta → 执行的元数据
所有看起来像Uint8Array的东西目前仍然不透明。 当您运行带有解码功能的脚本时,您将看到实际的嵌套结构和可读的地址:

解码二进制数据

为什么要解码? 原始Laserstream数据包含签名、账户密钥和作为二进制Uint8Array对象的哈希,这些不可读。你需要将这些转换为base58字符串,以便理解交易。 解决方案: Laserstream使用Yellowstone gRPC,它提供了内置的解码工具。我们使用一个递归函数将所有二进制数据转换为人类可读的格式,而不是为每种字段类型编写单独的解码器。
这种方法利用了内置的解码功能,同时处理需要手动转换的二进制字段。交易结构已经被解析 - 你只需要将二进制字段转换为人类可读的格式。

理解交易结构

现在我们可以看到解码后的数据,让我们来探索每个 Laserstream 交易更新的两个主要部分。记住在我们最初的例子中,每个交易包含两个关键对象:
  • 消息(提案) → transaction.transaction.transaction → 签名的消息(用户的提案)
  • 元数据(执行) → transaction.transaction.meta → 执行元数据(验证者的响应)
这种两部分结构讲述了一个完整的故事:用户请求了什么与实际发生了什么。让我们详细检查每个部分。

提案:消息中的所有内容

用户创建一个消息,指定什么,谁以及何时为止。以下是如何解码每个部分:

交易头

numRequiredSignatures 告诉验证者要验证多少签名,而另外两个 numReadonly* 值标记运行时可以视为只读的账户,从而实现并行执行。

账户密钥字典

accountKeys是一个公共密钥列表,作为查找表。事务中的每个后续整数 - programIdIndex,每个指令中的每个元素accounts数组 - 通过索引指向此列表,从而每条消息节省超过一千字节。

防重放保护

recentBlockhash在它滚动出最后150个区块哈希时过期,大约在主网消耗九十秒。

指令:实际命令

每个指令包含三个关键部分:
  • 程序ID (programIdIndex): 指向accountKeys数组中的地址(例如,索引10 = ComputeBudget111111111111111111111111111111)
  • 账户 (accounts): base58编码的字符串,表示该指令接触的账户索引
  • 数据 (data): 实际的指令数据,编码为base58
由于convertBuffers函数,账户显示为base58,但实际上包含账户索引(例如,"3vtmrQMafzDoG2CBz1iqgXPTnC"解码为索引[21, 19, 12, 17, 2, 6, 1, 22]) 这种设计意味着每个指令只需引用查找表中的位置,而不是重复完整的 32 字节地址。

签名:授权证明

signatures包含加密签名,证明所需账户授权了此交易。签名数量必须与header.numRequiredSignatures匹配。

地址表查找

如果versioned是true,addressTableLookups会出现一个链上表和两个索引列表。查找表可以在保持数据包在1,232字节MTU以下的同时,将地址数量的硬性限制提升到几十个。

交易 v1:将计算预算放在头部

交易 v1(SIMD-0385,Agave 4.2)向消息中添加了一个字段:transactionConfig。
v1 交易在此处携带其计算预算,而不是在 ComputeBudget 程序指令中,因此 v1 交易的 instructions 数组中从不包含 ComputeBudget111111111111111111111111111111 条目。priorityFee 是整个交易的 lamports 总费用,而不是每个计算单元的微 lamports。如果没有设置 null 字段,则表示发送者没有设置。传统消息和 v0 消息没有 transactionConfig,因此其存在标识了 v1 交易。 在解码器中需要检查两点:
  • 优先费用提取。 可读取 transactionConfig.priorityFee 如果存在,并且仅对传统和 v0 交易回退扫描 ComputeBudget 指令。仅扫描指令的代码会读取每个 v1 交易的优先费用为零。
  • Proto 版本。 yellowstone-grpc-proto 12.6.0 是第一个携带 v1 字段的版本,而 helius-laserstream 0.8.4(JavaScript)、0.6.3(Rust)和 0.2.0(Go)是基于它构建的第一个 SDK 版本。较旧版本会默默地丢弃 transactionConfig。
查看 Transaction v1 support 以获取完整的更改列表。

如何连接:流程

根据基本原则,以下是会发生的情况:
  1. 建立查找表:accountKeys 列出此交易将触及的所有地址
  2. 设置规则:header 指定需要多少签名以及哪些账户是只读的
  3. 创建命令:每个 instruction 指向:
    • 一个程序(通过 programIdIndex → accountKeys[index])
    • 所需的账户(通过 accounts → 多个 accountKeys[index] 位置)
    • 指令数据(编码在 data 中)
  4. 添加授权:signatures 证明所需账户批准了此交易
  5. 设置过期时间:recentBlockhash 确保此交易不能稍后重播

执行:所有内容在 meta 内

虽然消息显示了用户想要做的事情,但 meta 显示了验证者执行交易时实际发生的事情。

基本执行信息

成功/失败
  • err: null = 成功
  • err: {...} = 失败并带有错误详情
  • fee = 针对此交易收取的 lamports
余额变化
余额数组按索引对应 accountKeys 数组:
  • 账户 0:损失 15000 lamports(费用支付)
  • 账户 1:获得 1461600 lamports(新账户创建)
  • 账户 3:获得 2001231920 lamports(程序账户)
计算使用情况
显示使用了多少计算预算(相对于请求的数量)。

高级执行细节

内部指令
内部指令是在执行期间程序调用的附加指令。它们不是原始交易的一部分,而是由主要指令触发。 日志消息
日志消息提供了程序执行的时间顺序跟踪,显示了调用了哪些程序以及它们输出的任何自定义日志消息。 代币余额变化
代币余额变化显示 SPL 代币账户的前后状态,包括正确的小数处理的人类可读金额。

实用解码模式

以下是从解码交易中提取有用信息的常见模式:

完整示例:Jupiter 交换解码器

这是一个完整的示例,解码 Jupiter 交换交易并提取有意义的信息:
此示例展示了如何结合消息解码与 meta 分析从复杂的 DeFi 交易中提取与业务相关的信息。

关键要点

  • 两部分结构:每个交易都有一个 message(请求的内容)和 meta(实际发生的事情)
  • 二进制解码:使用 bs58.encode() 将二进制字段转换为可读的 base58 字符串
  • 账户键查找:指令按 accountKeys 数组中的索引引用账户
  • 余额跟踪:比较 preBalances 和 postBalances 以查看变化
  • 交易 v1:在存在 transactionConfig 时读取计算预算和优先费用;v1 交易没有 ComputeBudget 指令
理解 Solana 交易的关键是认识到它们为提高效率而设计:使用查找表和索引代替重复地址,以最小化交易大小,同时最大化信息密度。