Solana 交易机器人 RPC 配置:一个 URL 早晚会不够用

一套能用的 Solana 交易机器人 RPC 配置:该拆什么、该批什么、该订阅什么,以及哪些设置真的会改变成交率。

BoltTx Team··13 min read
solana交易机器人rpc配置交易上链基础设施

大多数交易机器人一开始都是配置文件里一个 RPC URL,然后一路长大。这在机器人还不重要时够用;等它开始重要,两个问题会同时出现:读取撞限流,发送开始输比赛。

这是两个不同的问题、有两套不同的解法,而想用"升个大套餐"同时解决,通常两个都解决不了。

拆分

一个交易机器人用 RPC 做三件事,而它们想要的属性互相冲突:

负载 想要什么 典型调用
读状态 吞吐、宽松限额 getAccountInfogetMultipleAccounts
流式事件 推送、过滤 onLogsonAccountChange
发交易 ★短路径、质押权重★ sendRawTransaction

读要的是批量便宜。发要的是单次够快。★为其中一个调优的端点,对另一个就是妥协★ —— 这就是为什么大多数生产环境最后都会拆开。

// 读取与流式:按你的量选一家合适的服务商。
const reader = new Connection(READ_RPC, {
  commitment: "confirmed",
  wsEndpoint: READ_WS,
});

// 发送:另一个端点,按提交表现来选。
const sender = new Connection(SEND_RPC, { commitment: "processed" });

这两个集成点互相独立,换其中一个不用动另一个。光这一点就值得拆 —— 因为它让你能拿真实流量去测一个新的发送端点,而不用拿读取路径冒险。

如果你已经拆开了、想找个发送端点做基准对比,免费领个 BoltTx key 改一行就行。

读取侧配置

三个比"选哪个套餐"更重要的设置。

批量,别循环。 大多数代码库里读取量降幅最大的一项:

// 贵:N 个请求。
// const infos = await Promise.all(pubkeys.map((pk) => reader.getAccountInfo(pk)));

// 便宜:每 100 个账户一个请求。
const infos = await reader.getMultipleAccountsInfo(pubkeys);

承诺级别按调用设,不要全局设一个。 查价格可以用 processed;任何要记成真相的东西该用 confirmed

连接复用。 每个请求新建 TLS 连接,意味着数据开始传之前要先握手。复用把它完全去掉,而且热连接上 HTTPS 的成本和纯 HTTP 基本一样。

import { Agent } from "undici";

const agent = new Agent({ keepAliveTimeout: 60_000, connections: 8 });

发送侧配置

这里的设置反直觉,因为默认值对交易场景是错的

const sig = await sender.sendRawTransaction(raw, {
  skipPreflight: true,   // ★默认是 false★
  maxRetries: 0,         // ★默认不是 0★
});

skipPreflight: true preflight 拿当前 slot 模拟,要花一次往返。但你会落在之后的 slot、面对不同状态,所以模拟通过什么都预测不了,却花掉了你需要的延迟。真正有用的模拟放在开发阶段。

maxRetries: 0 RPC 自带的重试按一套你观察不到的节奏跑。如果你也在重试,就是两个组件按不同时钟重发,行为没法推理。重试自己驱动 —— 因为只有你知道 blockhash 什么时候过期。

然后是真正能用的循环:

while (await sender.getBlockHeight("confirmed") <= lastValidBlockHeight) {
  const { value } = await sender.getSignatureStatuses([sig]);
  if (value[0]) break;                         // 已上链,看 .err

  await sender.sendRawTransaction(raw, { skipPreflight: true, maxRetries: 0 });
  await new Promise((r) => setTimeout(r, 400)); // 大约一个 slot
}

重发相同字节是安全的 —— 签名一样,最多被收录一次。

blockhash 的处理

这是代价最隐蔽的一个设置。

// 后台刷新;绝不在热路径上取。
let cached = await reader.getLatestBlockhash("confirmed");
setInterval(async () => {
  cached = await reader.getLatestBlockhash("confirmed");
}, 5_000);

★在"我决定交易"和"我提交了"之间取一次 blockhash,是一次你付不起的往返。★ 缓存把它去掉;定时刷新保证它足够新鲜,让你在关键时刻永远不接近过期。

跟踪有效期已经花掉多少 —— 这是几乎没人记录的指标:

log({ blockhashAge: Date.now() - cachedAt });

费用

写死的 priority fee 两个方向都错:网络清闲时浪费,不清闲时不够。

import { ComputeBudgetProgram } from "@solana/web3.js";

// 费用是按账户竞争的,不是全局的。
const recent = await reader.getRecentPrioritizationFees({
  lockedWritableAccounts: writableAccounts,
});
const fees = recent.map((r) => r.prioritizationFee).sort((a, b) => a - b);
const median = fees[Math.floor(fees.length / 2)] ?? 0;

instructions.unshift(
  ComputeBudgetProgram.setComputeUnitPrice({
    microLamports: Math.max(median * 2, 5_000),
  }),
  ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }),
);

★compute unit limit 要设。★ 不设的话会按一个远高于真实用量的默认值计费,恰好浪费在费用高的时候。

该监控什么

三个计数器,分开跟踪。合成一个成功率会把"你到底遇到哪个问题"盖住:

const { value } = await sender.getSignatureStatuses([sig], {
  searchTransactionHistory: true,
});

if (!value[0])          metrics.inc("never_landed");   // ★费用、路径、重试、过期★
else if (value[0].err)  metrics.inc("landed_failed");  // ★滑点、余额、账户状态★
else                    metrics.inc("landed_ok");

再加上按网络状况分桶的"提交到落块的 slot 距离"。★清闲时稳、高负载时掉的上链率,指向路由★ —— 而那是唯一没法在应用层代码里解决的。

上链应该是什么水平

我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。

如果你自己的分布明显更宽,先依次排查费用、blockhash 时长、重试行为 —— 然后再看路径。

一份能用的最小配置

// 读取 + 流式
const reader = new Connection(READ_RPC, {
  commitment: "confirmed",
  wsEndpoint: READ_WS,
});

// 发送
const sender = new Connection(SEND_RPC, { commitment: "processed" });

// 后台刷新 blockhash
let blockhash = await reader.getLatestBlockhash("confirmed");
setInterval(async () => {
  blockhash = await reader.getLatestBlockhash("confirmed");
}, 5_000);

// 每次发送:实时算的费用、CU limit、skipPreflight、自己的重试循环

整个形状就是这样。剩下的都是在这个框架内调参。

BoltTx 在这里的位置

我们是发送那一半。不做索引,不做解析历史,不做流式推送。

提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前,传输途中观察不到。你现在的读取服务商留着就行,集成点互相独立。

你在本地签名。我们不托管资金、不代签、不修改交易内容。tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。

免费领 API key,没有月费:

const sender = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

常见问题

Solana 交易机器人需要怎样的 RPC 配置? 通常是两个端点:一个负责读取和流式,一个负责发送。两种负载想要的属性不同,而集成点互相独立 —— 换其中一个不用动另一个。

交易机器人该用一个 RPC 还是两个? 等机器人开始重要之后,用两个。读要吞吐和宽松限额;发要一条到出块方、背后有质押的短路径。一个端点同时优化两者,对两边都是妥协。

交易机器人该用哪个承诺级别? 按调用设,不要全局设一个。查价格和检测事件用 processed,任何要记成真相的用 confirmed。一个全局设置必然会在某处强迫你做错误的取舍。

交易机器人为什么该开 skipPreflight? preflight 要花一次网络往返,而且模拟的是当前 slot、不是你会落进去的那个。它可能通过了却什么都没告诉你,同时花掉了延迟。真正有用的模拟放在开发阶段。

为什么要把 maxRetries 设成 0? 因为 RPC 自带的重试按一套你看不见的节奏跑。如果你自己也有循环,两个组件按不同时钟重发会让行为无法预测。只有你的循环知道 blockhash 什么时候过期。

怎么降低交易机器人的 RPC 用量?getMultipleAccounts 批量取代循环调 getAccountInfo、把适合推送的数据从轮询换成订阅、并给程序订阅加服务端过滤。

该缓存 blockhash 吗? 该,而且要后台定时刷新。在"决定"和"提交"之间去取,会在唯一付不起往返的地方加一次往返。另外记得记录有效期已经花掉多少。

交易机器人的 priority fee 该设多少?getRecentPrioritizationFees 按你会写入的账户去算,再按那一刻的竞争程度放大。写死的值在清闲时浪费、在繁忙时不够。

为什么我机器人的成功率会误导人? 因为它把两个无关的问题合并了。把"从未上链"和"上链但失败"分开看:前者的成因在提交侧,可能是费用不够、路径被降级,或者根本没重试到过期;后者的成因在链上,滑点、余额或账户状态都可能。

Solana 交易机器人一定要付费 RPC 吗? 低量读取有时不用。但拥堵时发送,共享公共端点是最糟的情况 —— 它们恰好在你的交易最重要的那些时刻最忙。

怎么测试一个新的发送端点是不是更好? 两边交替发,至少一周,按网络状况分桶,对比上链率和 slot 距离。只在清闲时段测,测的是"所有路径都一样"的那种情况。

compute unit limit 该设多少? 开发阶段模拟一次拿到真实消耗,设一个略高的值。用默认值意味着按一个高得多的数字计费,在费用飙升时白白浪费预算。

连接复用对交易机器人重要吗? 重要。新建 TLS 连接要先握手,字节才开始传。复用之后这笔开销就没了 —— 对持续发送的机器人来说,这是你每一笔都在白付的延迟。

读和发该用同一个承诺级别吗? 不该。发送在确认循环里用 processed 更快;而喂给你数据库的读取该用 confirmed 或更高,因为 processed 的结果可能被回滚。

哪些指标能说明瓶颈在 RPC 配置? 按网络状况分桶的上链率。如果清闲时稳、高负载时掉,而费用是实时算的、blockhash 也新鲜,那剩下的变量就是提交路径。

延伸阅读

返回博客列表