定时轮询 RPC 一直够用 —— 直到你需要"某件事发生的那一刻就知道"。然后你换成 WebSocket 订阅,接着发现了另一批问题。
订阅确实更快。但它也会静默丢事件,而轮询不会;而订阅代码里的大多数 bug,都源于把"推送"当成了"保证"。
值得知道的四种订阅
Solana 提供了好几种,但这四种覆盖几乎所有真实场景:
onAccountChange(对应 accountSubscribe 这个 RPC 方法)—— 某个特定账户的数据变化时触发。适合盯一个池子、一条曲线、或一个代币账户。
onProgramAccountChange(对应 programSubscribe 这个 RPC 方法)—— 某个程序拥有的任意账户变化时触发,可加过滤。很强大,也很容易变得很贵。
onLogs —— 某个程序或地址出现在交易日志里时触发。这是大多数交易机器人在用的,因为它能捕捉到那些你事先猜不到账户地址的活动。
onSignature —— 某个特定签名达到承诺级别时触发一次。适合做确认,不适合做发现。
承诺级别是第一个决策
每个订阅都要选一个承诺级别,而这是个真实的取舍,不是一个可以照单全收的默认值。
connection.onLogs(
programId,
(logs) => handle(logs),
"processed", // 对比 "confirmed" 和 "finalized"
);
| 级别 | 速度 | 风险 |
|---|---|---|
processed |
★最快★ | 可能被回滚 |
confirmed |
慢一个以上 slot | 很少被回滚 |
finalized |
慢很多 slot | 基本永久 |
★对时间敏感的场景用 processed,然后在动钱之前验证。★ 等 confirmed 就是在等你正在抢的那个东西。代价是你看到的东西里有一小部分可能活不下来 —— 而这靠验证来处理,不是靠等。
而任何要写进你自己数据库当作真相的东西,用 confirmed 或更高。
如果订阅已经跑通、问题在发送侧,免费领个 BoltTx key 改一行就能拿来做对比。
断线重连的问题
WebSocket 连接会断。不是偶尔 —— 是经常:网络抖动、服务商重启、空闲超时。
客户端的默认行为和大多数人以为的不一样:
// 这样写看着没问题,断线之后就静默地不工作了。
connection.onLogs(programId, handler, "processed");
★socket 一死,你的 handler 就再也不会被调用。没有报错,没有异常 —— 只有沉默。★ 一个机器人可以连着几小时什么都没收到,而看起来一切正常。
解法是一个**不依赖"事件有没有到"**的存活检查 —— 因为当你盯的东西本来就安静时,"没有事件"和"连接已死"是分不出来的:
let lastEventAt = Date.now();
let subId: number | null = null;
async function subscribe() {
subId = connection.onLogs(
programId,
(logs) => {
lastEventAt = Date.now();
handle(logs);
},
"processed",
);
}
// 独立心跳:用一个结果可预期的调用来证明连接还活着,
// 而不是靠等事件。
setInterval(async () => {
try {
await connection.getSlot("processed"); // socket 没了就会失败
if (Date.now() - lastEventAt > 120_000) {
// 一个本该繁忙的程序安静了两分钟。
// 重新订阅,别默认网络就是闲的。
if (subId !== null) await connection.removeOnLogsListener(subId);
await subscribe();
}
} catch {
if (subId !== null) await connection.removeOnLogsListener(subId).catch(() => {});
await subscribe();
}
}, 30_000);
日志不是数据
onLogs 给你的是日志行和一个签名。它不给你权威状态。
connection.onLogs(programId, async (logs) => {
// 错:从日志字符串里解析金额。
// 日志格式不是稳定接口,不同版本之间会变。
const amount = parseAmountFromLogs(logs.logs);
// 对:把日志当触发器,然后去读状态。
const account = await connection.getAccountInfo(derivedPda);
const state = deserialize(account.data);
});
★把订阅当通知,别当数据本身。★ 日志告诉你"发生了某件事",账户告诉你"事实是什么"。
这一点在日志格式变化时最要命 —— 程序升级时会变,而且解析器是静默地坏掉,不是响亮地报错。
订阅在什么时候变得很贵
在一个繁忙的程序上不加过滤地用 onProgramAccountChange,会把你淹了。该程序拥有的每个账户、每次变化,全推给你的进程。
// 贵:该程序下的每一个账户。
connection.onProgramAccountChange(programId, handler);
// 好一点:在服务端过滤,只接收你需要的。
connection.onProgramAccountChange(
programId,
handler,
"processed",
[
{ dataSize: 165 }, // 只要这个布局
{ memcmp: { offset: 32, bytes: ownerPubkey.toBase58() } },
],
);
★过滤是在服务商那一侧执行的,所以不加过滤的订阅在两端都要付带宽和 CPU。★ 在共享档上,这也是最快撞到限流的方式。
订阅对"发送"没有帮助
这一点值得明说,因为它是个常见误解。
WebSocket 订阅让你更早知道事件发生了。它对"你自己的交易多快到达出块方"没有任何帮助 —— 因为提交是一次 HTTP 调用,走的是另一条路。
检测 → WebSocket 订阅在这里起作用
提交 → ★和你的订阅配置完全无关★
这就是为什么很多生产环境用一家做流式推送、另一家专做投递。两种负载想要的东西不同,而集成点互相独立。
★一个检测快到极致、提交路径却很慢的机器人,照样迟到。★
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
如果你的检测是瞬时的、却稳定在事件发生后四个以上 slot 才落块,差距在发送侧,不在订阅代码里。
BoltTx 在这里的位置
我们不做订阅。我们做提交。
BoltTx 把签好名的交易路由到我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前,传输途中观察不到。检测那一半,你配一家适合自己需求的流式服务商就行,两个集成点互相独立。
你在本地签名。我们不托管资金、不代签、不修改交易内容。tip 带在交易里、从你自己的钱包链上支付,交易失败时 tip 跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
// 检测那边不用动,只换发送端点。
const sender = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
Solana 的 WebSocket 订阅是什么?
一条持久连接,由 RPC 把事件推给你,而不是你去轮询。常用的有 onAccountChange、onProgramAccountChange、onLogs、onSignature。
订阅该用哪个承诺级别?
时间敏感的检测用 processed,因为等 confirmed 会花掉一个以上 slot。任何要当作真相记录下来的东西,用 confirmed 或 finalized —— processed 的结果可能被回滚。
我的 Solana WebSocket 订阅为什么不触发了? 连接断了,而你的 handler 再也没被调用。不会抛错,所以这是静默失败。 加一个独立心跳来验证存活,而不是靠"有没有收到事件"来推断。
怎么检测 Solana WebSocket 断线了? 别靠事件沉默来判断 —— 一个安静的程序和一个死掉的 socket 看起来一模一样。定时调一个结果可预期的接口,失败时重订阅;或者当安静时间超过该程序合理的沉默上限时也重订阅。
onLogs 可靠到能拿来交易吗? 当触发器可以,当数据源不行。用日志知道"发生了什么事",然后读账户拿权威状态。日志格式在程序升级时会变,而且是静默地把解析器搞坏。
onLogs 和 onProgramAccountChange 有什么区别?
onLogs 在程序出现在交易日志里时触发,能捕捉到那些你事先猜不到账户的活动。onProgramAccountChange 在该程序拥有的账户变化时触发,更精准,但前提是你知道该盯什么。
怎么避免被 onProgramAccountChange 淹掉?
加过滤 —— dataSize 和 memcmp —— 让服务商只发匹配的账户。在繁忙程序上不加过滤会把一切都推过来,两端都要付带宽,还会烧掉限流额度。
WebSocket 订阅算不算进我的限流额度? 通常和 HTTP 请求算法不同,而且各家政策不一样。但有一点是共通的:不加过滤的高流量订阅,是最快撞到任何限制的方式。
能用 WebSocket 订阅来发送交易吗? 发送是 HTTP 调用,不是订阅。有些客户端会开一条 WebSocket 接收确认通知,但提交本身走的是另一条路 —— 这就是为什么检测速度和提交速度是两个独立的问题。
为什么我的机器人瞬间就检测到事件,交易还是晚? 因为检测和提交互相独立。订阅让你更早知道,但对"交易多快到达出块方"毫无作用。把检测时的 slot 和落块时的 slot 都记下来,就知道是哪一半在拖后腿。
一条连接上能开多少个订阅? 实际上比大多数机器人需要的多,不过各家有自己的上限。真正的约束通常是流经的事件量,而不是订阅的数量。
订阅和发送该用两家服务商吗? 很多生产环境就是这么配的。流式推送要吞吐和灵活的过滤;发送要一条到出块方的短路径。用一个端点同时优化两者,意味着两边都要妥协,而集成点本来就是独立的。
WebSocket 重连期间的事件会怎样? 丢了。没有重放缓冲区。 如果漏事件不可接受,重连之后用一次轮询查询把中间那段补回来。
onSignature 对交易机器人有用吗? 用来确认你自己发出去的交易,有用 —— 它触发一次然后自动取消订阅。用来发现新活动,没用,因为你得事先知道签名。
怎么测试重连逻辑是对的? 把跑机器人那台机器的网络断三十秒,然后验证它有没有重订阅并恢复。等真实断线来测,等于在你最在意的时段才发现问题。
延伸阅读
- Solana 交易延迟该测什么
- Solana 跟单 API
- 搭一个 Solana 钱包追踪器
- Solana RPC 监控
- Solana 交易上链完全指南