Solana交易的低成本解析

本文的目的是创建一个比现有库小 45 倍的 最小库,用于在 JavaScript 中解码 Solana 交易。

Solana交易的低成本解析
一键发币: SUI | SOL | BNB | ETH | BASE | ARB | OP | POLYGON | AVAX | FTM | OK

本文的目的是创建一个比现有库小 45 倍的 最小库,用于在 JavaScript 中解码 Solana 交易。

1、背景概述

Solana 使用的技术包括:

  • Anchor:Solana 智能合约框架,IDL 规范。
  • Solana 使用 BORSH(Binary Object Representation Serializer for Hashing)序列化数据。

用例:在前端解析 Jupiter Swap 事件,可在 Solscan 上查看交易。

const tx = getTransaction(txHash)
const swaps = parseTxSwaps(tx) // 虚构函数
[
  {
    amm: "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo",
    inputMint: "MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5",
    inputAmount: 341209n,
    outputMint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    outputAmount: 14667n,
  }
]

即使使用 web3.jsgetParsedTransaction,数据仍然是原始的 base58 编码形式:

rpc.getParsedTransaction(txHash)
// Jupiter
{
  "accounts": ["D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf"],
  "data": "QMqFu4fYGGeUEysFnenhAvBobXTzswhLdvQq6s8axxcbKUPRksm2543pJNNNHVd1VJ58FCg7NVh9cMuPYiMKNyfUpUXSDci9arMkqVwgC1zp94XrEkgEX68QGBNbfpkzGSTG2i4ReApCRe6qocBT275xZsK54Z8h8GxZS4WWsSd6AvK",
  "programId": "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
  "stackHeight": 2
},
// 某些程序(如 SPL)会被解码
{
  "parsed": {
    "info": {
      "authority": "HLujcj6D7kdH3gLktJRXB95vRbfhp558HvsFoSLMKaSZ",
      "destination": "AhR2gTxbKpouGEzTJ86Cki2z2qSDe9p7to8jxYCAWsfZ",
      "mint": "MEW1gQWJ3nEXg2qgERiKu7FAFj79PHvQVREQUzScPP5",
      "source": "CGb9s5dyTqJuXRKwJkvWmVkLPzy3B9iYYksdn2Q7nGLb",
      "tokenAmount": { "amount": "341209", "decimals": 5, "uiAmount": 3.41209, "uiAmountString": "3.41209" }
    },
    "type": "transferChecked"
  },
  "program": "spl-token",
  "programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
  "stackHeight": 3
},

对于TypeScript 生态系统,Jupiter 提供了 instruction-parser 包来解析事件:

import { extract } from "@jup-ag/instruction-parser";
extract(tx);

成本:

依赖图 非常庞大。主要依赖包括:

@jup-ag/instruction-parser → @coral-xyz/anchor → @coral-xyz/borsh → buffer-layout

我们可以使用 @coral-xyz/anchor,通过 IDL 解析,但它仍然很大。

目前的问题在于:

  • 大包体积:618 kB -> 187 kB (gzip),依赖项无 ESM 支持。
  • 过多依赖:使用 bn.js(已有原生 BigInt),bs58(现代替代品为 @scure/base)。
  • 使用 Buffer:尽管 web3.js 已包含,但其重写中已移除。
  • 基于类:树摇优化不佳,类中包含无法移除的额外代码。
  • 需包含完整 IDL:Jupiter 的 IDL 为 18kb,且解析器本身也是开销。

2、针对 Swap 事件的最小解析器

我们的目标有限,仅需解码 Jupiter 的 Swap 事件。以下是最小步骤:

2.1 获取 Swap 交易 数据 2a4EpB...:

const tx = await getTransaction("2a4EpB...")
tx.meta.innerInstructions.forEach(ix => {...})

筛选出 Jupiter 程序 ID JUP6Lkb... 的指令数据:

ix {
  accounts: [ 'D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf' ],
  data: 'QMqFu4fYGGeUEysFnenhAvBobXTzswhLdvQq6s8axxcbKUPRksm2543pJNNNHVd1VJ58FCg7NVh9cMuPYiMKNyfUpUXSDci9arMkqVwgC1zp94XrEkgEX68QGBNbfpkzGSTG2i4ReApCRe6qocBT275xZsK54Z8h8GxZS4WWsSd6AvK',
  programId: 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4',
  stackHeight: 2
}
ix {
  accounts: [ 'D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf' ],
  data: 'QMqFu4fYGGeUEysFnenhAvBobXTzswhLdvQq6s8axxcbKUPRksm2543pJNNNHVd1VJ4E2hRWa3GsBQPU2sRp3sRtjPENk4z91Q3X1PbK516ePc2y6ByX88EtCjDWkqotkzT2RmM7oWpZpVXPJqk9N7YoG7hjSjejznGCKmaoH7u68dM',
  programId: 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4',
  stackHeight: 2
}
ix {
  accounts: [ 'D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf' ],
  data: 'QMqFu4fYGGeUEysFnenhAvDWgqp1W7DbrMv3z8JcyrP4Bu3Yyyj7irLW76wEzMiFqkMXcsUXJG1WLwjdCWzNTL6957kdfWSD7SPFG2av5YHKd5MazCGSGzUpJNtxRdjzMQ124wR1QyZj2zDKLPDXmi2Q4WgHVPnzBgFHQNvvw93wLk7',
  programId: 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4',
  stackHeight: 2
}

2.2 从 base58 解码:

const ixData = base58.decode(ix.data);

在 Solscan 上,Instruction Data Raw 默认以十六进制显示,便于可视化二进制结构:

base16.encode(ixData).toLowerCase()

// Hex
e4 45 a5 2e 51 cb 9a 1d  40 c6 cd e8 26 08 71 e2
04 e9 e1 2f bc 84 e8 26  c9 32 cc e9 e2 64 0c ce
15 59 0c 1c 62 73 b0 92  57 08 ba 3b 85 20 b0 bc
06 9b 88 57 fe ab 81 84  fb 68 7f 63 46 18 c0 35
da c4 39 dc 1a eb 3b 55  98 a0 f0 00 00 00 00 01
a0 86 01 00 00 00 00 00  05 2e e1 83 38 96 96 9f
8c d1 cd 46 83 18 c5 98  c7 e0 58 96 07 4a 59 1c
2a e0 98 60 2f 16 80 00  d9 34 05 00 00 00 00 00

前 8 个字节是指令区分符(identifier),我们只关心 Swap 事件,可以跳过(源码):

// Skip
// e4 45 a5 2e 51 cb 9a 1d : `global:${instruction_name}` e.g. `global:swap`
const eventData = ixData.subarray(8);

剩余的是事件数据。首先需要确定事件类型以进行反序列化,因为可能有多个事件:

.. .. .. .. .. .. .. ..  40 c6 cd e8 26 08 71 e2
04 e9 e1 2f bc 84 e8 26  c9 32 cc e9 e2 64 0c ce
15 59 0c 1c 62 73 b0 92  57 08 ba 3b 85 20 b0 bc
06 9b 88 57 fe ab 81 84  fb 68 7f 63 46 18 c0 35
da c4 39 dc 1a eb 3b 55  98 a0 f0 00 00 00 00 01
a0 86 01 00 00 00 00 00  05 2e e1 83 38 96 96 9f
8c d1 cd 46 83 18 c5 98  c7 e0 58 96 07 4a 59 1c
2a e0 98 60 2f 16 80 00  d9 34 05 00 00 00 00 00

事件区分符在前 8 个字节,是 event:${event_name} 的 sha256 哈希的前 8 字节(源码)。可在 CyberChef 上尝试:

// namespace = "event"
// event name from IDL = "SwapEvent"
const signature = `${namespace}:${name}`;

// 40 c6 cd e8 26 08 71 e2
sha256(signature).subarray(0, 8);

现在可以识别 Swap 事件并使用适当的结构体反序列化。

2.3 反序列化数据

Jupiter 的程序 IDL 可确定结构体。结构体很简单,无需 IDL 解析器,可直接在代码中声明:

"events": [
    {
      "name": "SwapEvent",
      "fields": [
        { "name": "amm", "type": "publicKey", "index": false },
        { "name": "inputMint", "type": "publicKey", "index": false },
        { "name": "inputAmount", "type": "u64", "index": false },
        { "name": "outputMint", "type": "publicKey", "index": false },
        { "name": "outputAmount", "type": "u64", "index": false }
      ]
    }
],

根据此结构体,我们可以编写特定数据类型的反序列化器并组合解析事件:

  • 使用现代 JavaScript 功能实现,无需大依赖。
  • 2024年10月19日更新Web3js v2 已发布,导出了可用的解码器,推荐使用。

包括:

  1. 反序列化公钥 - 32 字节类型
  2. 反序列化小端 uint64 - 8 字节类型

结合两者,可成功以最小的代码和依赖 反序列化 Swap 事件

3、比较

影响显著:

  • @jup-ag/instruction-parser - 包体积 618 kB -> 187 kB (gzip) | bundlejs
  • @coral-xyz/anchor - 包体积 426 kB -> 124 kB (gzip) | bundlejs
  • 自定义二进制解码器 - 包体积 7.82 kB -> 3.48 kB (gzip) | bundlejs
  • 其中 92% 是用于基数转换和哈希的库 @scure/base@noble/hashes

值得一提的是,以下是实现序列化/反序列化的有用参考:

  • 使用 @dao-xyz/borsh 解码 | bundlejs | 包体积 17.8 kB -> 6.3 kB (gzip)
  • 使用装饰器,可能不适用于某些构建,且我不喜欢。
  • 官方 borsh-ts 重写 存在,但 未发布到 npm
  • 解决了一些问题,但仍是基于类。
  • @hackbg/borshest 看起来不错,但 API 不确定。

4、结束语

尚不清楚是否会遇到更复杂数据类型或场景的问题。我创建了一个实验性库 AnchorES,具有简单 API 和易于扩展或提供新结构体的解码方式。结构体声明受验证库启发。


原文链接:Decode Solana Transactions on a budget

DefiPlot翻译整理,转载请标明出处

免责声明:本站资源仅用于学习目的,也不应被视为投资建议,读者在采取任何行动之前应自行研究并对自己的决定承担全部责任。
通过 NowPayments 打赏