> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbitflare.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Apex 捆绑包

> 通过 OrbitFlare Apex 使用 sendBundle 或 POST /send-bundle 发送由 1 到 4 笔交易组成的 Solana 原子捆绑包，并使用 getInflightBundleStatuses 跟踪其状态。

## 什么是捆绑包

捆绑包是一组 **1 到 4 笔交易**，它们**按顺序**执行，并且**要么全部成功，要么全部不执行**。捆绑包中的每笔交易要么按您给定的顺序在同一个区块中上链，要么全都不上链。

当后面的交易只有在前面的交易成功后才有意义时，请使用捆绑包，例如先执行一笔准备交易，再执行一笔兑换。

<Warning>
  **捆绑包只能在启用了 Jito 的领导者上链。** 一组交易无法在质押加权路径或直连 TPU 路径上保持原子性，因此捆绑包只走区块引擎路径。当前领导者未运行 Jito 时，捆绑包会等待下一个运行 Jito 的领导者。对于单笔交易，常规[发送](/cn/apex/sending-transactions)会使用全部三条路径，上链更快。
</Warning>

## 规则

| 规则       | 详情                                                                                    |
| -------- | ------------------------------------------------------------------------------------- |
| **数量**   | 1 到 4 笔交易                                                                             |
| **顺序**   | 按给定顺序执行                                                                               |
| **原子性**  | 要么全部成功，要么全部不执行                                                                        |
| **小费**   | 捆绑包中**恰好一笔**交易携带 Apex 小费，且不低于您所在等级的下限                                                 |
| **成员大小** | 每笔交易限制为 1232 字节。[交易 v1](/cn/apex/transaction-v1) 的大小在捆绑包内不适用                          |
| **速率**   | 每个 API 密钥每秒 2 个捆绑包，可短时突发 4 个，并计入您的交易限额之外。超出后返回 HTTP `429` 或 `-32029`                  |
| **状态**   | `getInflightBundleStatuses` 需要使用发送该捆绑包的 API 密钥，一次最多 100 个 id。用其他密钥发送的捆绑包显示为 `Invalid` |
| **重试**   | Apex 会以同一个捆绑包 id 重新提交捆绑包，直到其上链、**第一笔**交易的 blockhash 过期或重新提交预算用尽                       |

小费遵循与单笔发送相同的[小费规则](/cn/apex/tips)：一条顶层的 SystemProgram 转账，转给已公布的小费账户，由签名者出资，且小费账户位于静态密钥中。该小费减去 5,000 lamports 基础费用后，即成为整个捆绑包的 Jito 出价。由于捆绑包是原子的，只有整个捆绑包上链时才会支付小费。

## JSON-RPC sendBundle

| 参数                   | 类型        | 描述                                          |
| -------------------- | --------- | ------------------------------------------- |
| `params[0]`          | string\[] | 1 到 4 笔已签名交易，按执行顺序排列，全部采用 `params[1]` 指定的编码 |
| `params[1].encoding` | string    | `"base64"` 或 `"base58"`                     |

返回结果是一个**捆绑包 id**。请保留它以查询捆绑包的状态。对于 base58 编码的交易，`sendBundle` 也接受 `{"encoding": "base58"}`。下面的示例使用 base64。

<CodeGroup>
  ```bash cURL theme={null}
  # Two signed transactions from the helper in Sending transactions.
  # Exactly one of them may carry the tip, so build the first one without it.
  TX1=$(node -e 'import("./apex-tx.mjs").then(async (m) => console.log(Buffer.from((await m.buildTx({ tip: false, label: "apex bundle 1" })).serialize()).toString("base64")))')
  TX2=$(node -e 'import("./apex-tx.mjs").then(async (m) => console.log(Buffer.from((await m.buildTx({ tip: true, label: "apex bundle 2" })).serialize()).toString("base64")))')

  BUNDLE_ID=$(curl -s "$APEX_RPC" \
    -H "Content-Type: application/json" \
    -H "x-api-key: $APEX_API_KEY" \
    -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"sendBundle\",\"params\":[[\"$TX1\",\"$TX2\"],{\"encoding\":\"base64\"}]}" \
    | jq -r '.result // .error')
  echo "bundle id: $BUNDLE_ID"

  curl -s "$APEX_RPC" \
    -H "Content-Type: application/json" \
    -H "x-api-key: $APEX_API_KEY" \
    -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getInflightBundleStatuses\",\"params\":[[\"$BUNDLE_ID\"]]}"
  ```

  ```javascript JavaScript theme={null}
  import { apexRpc, buildTx } from "./apex-tx.mjs";

  // Exactly one transaction carries the tip.
  const txs = [
    await buildTx({ tip: false, label: "apex bundle 1" }),
    await buildTx({ tip: true, label: "apex bundle 2" }),
  ];

  const bundleId = await apexRpc("sendBundle", [
    txs.map((tx) => Buffer.from(tx.serialize()).toString("base64")),
    { encoding: "base64" },
  ]);
  console.log("bundle id:", bundleId);

  // Poll until the bundle settles.
  for (;;) {
    const { value } = await apexRpc("getInflightBundleStatuses", [[bundleId]]);
    const { status, landed_slot } = value[0];
    if (status === "Landed") { console.log("landed in slot", landed_slot); break; }
    if (status === "Failed" || status === "Invalid") { console.log("bundle", status); break; }
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
  ```

  ```python Python theme={null}
  import base64
  import time

  from apex_tx import apex_rpc, build_tx

  # Exactly one transaction carries the tip.
  txs = [
      build_tx(tip=False, label="apex bundle 1"),
      build_tx(tip=True, label="apex bundle 2"),
  ]

  bundle_id = apex_rpc("sendBundle", [
      [base64.b64encode(bytes(tx)).decode() for tx in txs],
      {"encoding": "base64"},
  ])
  print("bundle id:", bundle_id)

  # Poll until the bundle settles.
  while True:
      entry = apex_rpc("getInflightBundleStatuses", [[bundle_id]])["value"][0]
      if entry["status"] == "Landed":
          print("landed in slot", entry["landed_slot"])
          break
      if entry["status"] in ("Failed", "Invalid"):
          print("bundle", entry["status"])
          break
      time.sleep(1)
  ```
</CodeGroup>

JavaScript 和 Python 示例从[发送交易](/cn/apex/sending-transactions#构建已签名并带小费的交易)中导入辅助模块。

响应：

```json theme={null}
{ "jsonrpc": "2.0", "id": 1, "result": "<bundle id>" }
```

## POST /send-bundle

该二进制路由采用与 [`/send-batch`](/cn/apex/sending-transactions#post-/send-batch) 相同的帧格式：每笔交易先是一个大端序 u16 长度，后跟原始交易字节，共 1 到 4 帧，按执行顺序排列。

<CodeGroup>
  ```javascript JavaScript theme={null}
  import { APEX_RPC, API_KEY, buildTx } from "./apex-tx.mjs";

  const txs = [
    await buildTx({ tip: false, label: "apex bundle 1" }),
    await buildTx({ tip: true, label: "apex bundle 2" }),
  ];

  // Each frame: u16 big-endian length, then the transaction bytes.
  const frames = txs.map((tx) => {
    const wire = Buffer.from(tx.serialize());
    const length = Buffer.alloc(2);
    length.writeUInt16BE(wire.length);
    return Buffer.concat([length, wire]);
  });

  const res = await fetch(`${APEX_RPC}/send-bundle`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream", "x-api-key": API_KEY },
    body: Buffer.concat(frames),
  });
  const body = await res.json();
  if (!body.bundle_id) throw new Error(`rejected ${res.status} ${body.error}: ${body.message}`);
  console.log("bundle id:", body.bundle_id);
  console.log("signatures:", body.signatures);
  ```

  ```python Python theme={null}
  import struct

  from apex_tx import APEX_RPC, build_tx, session

  txs = [
      bytes(build_tx(tip=False, label="apex bundle 1")),
      bytes(build_tx(tip=True, label="apex bundle 2")),
  ]
  # Each frame: u16 big-endian length, then the transaction bytes.
  payload = b"".join(struct.pack(">H", len(wire)) + wire for wire in txs)

  r = session.post(f"{APEX_RPC}/send-bundle", headers={"Content-Type": "application/octet-stream"},
                   data=payload, timeout=5)
  body = r.json()
  if "bundle_id" not in body:
      raise RuntimeError(f"rejected {r.status_code} {body['error']}: {body['message']}")
  print("bundle id:", body["bundle_id"])
  print("signatures:", body["signatures"])
  ```
</CodeGroup>

响应：

```json theme={null}
{
  "bundle_id": "<bundle id>",
  "signatures": ["<signature of transaction 1>", "<signature of transaction 2>"]
}
```

错误使用与其他[普通 HTTP 路由](/cn/apex/errors-and-rate-limits#普通-http-错误)相同的格式和状态码。被整体拒绝的捆绑包会返回 `bundle` 标签。

## 跟踪捆绑包

使用捆绑包 id 数组调用 `getInflightBundleStatuses`：

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "getInflightBundleStatuses",
  "params": [["<bundle id>"]]
}
```

结果按您请求的顺序，为每个捆绑包 id 返回一个条目：

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "context": { "slot": 367112345 },
    "value": [
      { "bundle_id": "<bundle id>", "status": "Landed", "landed_slot": 367112344 }
    ]
  }
}
```

`landed_slot` 是捆绑包上链的 slot，尚未上链时为 `null`。`status` 是以下四种状态之一：

| 状态        | 含义                                       |
| --------- | ---------------------------------------- |
| `Pending` | Apex 持有该捆绑包并仍在提交                         |
| `Landed`  | 捆绑包已上链。`landed_slot` 中包含 slot            |
| `Failed`  | 捆绑包未能在第一笔交易的 blockhash 过期前上链，或者重新提交预算已用尽 |
| `Invalid` | 该 Apex 端点不认识此捆绑包 id，或者它已超过五分钟            |

Apex 重新提交期间，捆绑包 id 保持不变，因此请继续轮询发送捆绑包时拿到的那个 id。

请向您发送捆绑包的同一个 Apex 端点查询。捆绑包 id 不在端点之间共享。

由于捆绑包是原子的，您也可以像确认任何交易一样确认它：只要其中任意一个签名在 Solana RPC 的 `getSignatureStatuses` 中显示为已确认，整个捆绑包就已上链。

## 捆绑包还是批量？

|            | 捆绑包         | [批量](/cn/apex/sending-transactions#post-/send-batch) |
| ---------- | ----------- | ---------------------------------------------------- |
| **交易数**    | 1 到 4 笔     | 最多 16 笔                                              |
| **原子性**    | 是           | 否，每笔相互独立                                             |
| **有序**     | 是           | 否                                                    |
| **小费**     | 整个捆绑包一笔     | 每笔交易一笔                                               |
| **路径**     | 仅 Jito 区块引擎 | 全部三条                                                 |
| **成员大小限制** | 1232 字节     | 1232 字节，v1 为 4096 字节                                 |
