什么是捆绑包
捆绑包是一组 1 到 4 笔交易,它们按顺序执行,并且要么全部成功,要么全部不执行。捆绑包中的每笔交易要么按您给定的顺序在同一个区块中上链,要么全都不上链。 当后面的交易只有在前面的交易成功后才有意义时,请使用捆绑包,例如先执行一笔准备交易,再执行一笔兑换。捆绑包只能在启用了 Jito 的领导者上链。 一组交易无法在质押加权路径或直连 TPU 路径上保持原子性,因此捆绑包只走区块引擎路径。当前领导者未运行 Jito 时,捆绑包会等待下一个运行 Jito 的领导者。对于单笔交易,常规发送会使用全部三条路径,上链更快。
规则
| 规则 | 详情 |
|---|---|
| 数量 | 1 到 4 笔交易 |
| 顺序 | 按给定顺序执行 |
| 原子性 | 要么全部成功,要么全部不执行 |
| 小费 | 捆绑包中恰好一笔交易携带 Apex 小费,且不低于您所在等级的下限 |
| 成员大小 | 与单笔交易相同:交易 v1 最大 4096 字节,legacy 或 v0 为 1232 字节 |
| 速率 | 每个 API 密钥每秒 2 个捆绑包,可短时突发 4 个,并计入您的交易限额之外。超出后返回 HTTP 429 或 -32029 |
| 状态 | getInflightBundleStatuses 需要使用发送该捆绑包的 API 密钥,一次最多 100 个 id。用其他密钥发送的捆绑包显示为 Invalid |
| 重试 | Apex 会以同一个捆绑包 id 重新提交捆绑包,直到其上链、第一笔交易的 blockhash 过期或重新提交预算用尽 |
JSON-RPC sendBundle
| 参数 | 类型 | 描述 |
|---|---|---|
params[0] | string[] | 1 到 4 笔已签名交易,按执行顺序排列,使用同一种编码 |
params[1].encoding | string,可选 | "base64" 或 "base58"。省略时逐笔自动识别 |
# 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\"]]}"
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));
}
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)
{ "jsonrpc": "2.0", "id": 1, "result": "<bundle id>" }
POST /send-bundle
该二进制路由采用与/send-batch 相同的帧格式:每笔交易先是一个大端序 u16 长度,后跟原始交易字节,共 1 到 4 帧,按执行顺序排列。
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);
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"])
{
"bundle_id": "<bundle id>",
"signatures": ["<signature of transaction 1>", "<signature of transaction 2>"]
}
bundle 标签。
跟踪捆绑包
使用捆绑包 id 数组调用getInflightBundleStatuses:
{
"jsonrpc": "2.0",
"id": 1,
"method": "getInflightBundleStatuses",
"params": [["<bundle id>"]]
}
{
"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,或者它已超过五分钟 |
getSignatureStatuses 中显示为已确认,整个捆绑包就已上链。
捆绑包还是批量?
| 捆绑包 | 批量 | |
|---|---|---|
| 交易数 | 1 到 4 笔 | 最多 16 笔 |
| 原子性 | 是 | 否,每笔相互独立 |
| 有序 | 是 | 否 |
| 小费 | 整个捆绑包一笔 | 每笔交易一笔 |
| 路径 | 仅 Jito 区块引擎 | 全部三条 |
| 成员大小限制 | 1232 字节,v1 为 4096 字节 | 1232 字节,v1 为 4096 字节 |