每一笔 Solana 交易都被永久记录下来。但这不代表你的 RPC 会把它给你。
★标准 RPC 节点会裁剪旧数据。链记得,而你正在查询的那个端点通常不记得。★ 团队总是在最糟的时候发现这件事 —— 在做月末对账、然后发现最早那些交易全返回 null 的时候。
两个调用,两件事
找某个地址的签名:
const sigs = await connection.getSignaturesForAddress(wallet, {
limit: 1000, // ★单次上限★
});
sigs.forEach((s) => {
console.log(s.signature, s.slot, s.blockTime, s.err ? "失败" : "成功");
});
取某一笔交易的详情:
const tx = await connection.getTransaction(signature, {
maxSupportedTransactionVersion: 0, // ★不加的话版本化交易返回 null★
});
★这个选项在实践中不是可选的。★ 不加它,任何 v0 交易都会返回 null —— 而这读起来像"这笔交易不存在",而不是"你的客户端没声明能解析这种格式"。
向后翻页
getSignaturesForAddress 从新到旧返回,单次上限 1000 条。想往更早翻,用 before:
async function allSignatures(address, connection, stopAtSlot = 0) {
const out = [];
let before = undefined;
while (true) {
const page = await connection.getSignaturesForAddress(address, {
limit: 1000,
before, // ★是签名,不是 slot★
});
if (!page.length) break;
out.push(...page.filter((s) => s.slot >= stopAtSlot));
const last = page[page.length - 1];
if (last.slot < stopAtSlot) break;
before = last.signature; // ★从这里继续★
await sleep(200); // ★这个调用很重★
}
return out;
}
★before 和 until 收的是签名,不是 slot。★ 传一个 slot 号会静默地返回没用的东西 —— 这是个让人困惑的失败,因为调用本身是成功的。
until 才是增量同步真正该用的那个。 存下你处理过的最新签名、把它作为 until 传进去,每次运行就只取新增的部分,而不是把历史重走一遍。
如果你的历史查询没问题、问题在交易落不了块,免费领个 BoltTx key 改一行就能测测提交路径。
你实际能往回查多久
这部分决定了你的架构:
| 节点类型 | 典型保留时长 |
|---|---|
| 标准 RPC | ★只有最近的 slot★ |
| 扩展历史套餐 | 数月 |
| Archive 节点 | ★完整历史★ |
| 你自己的数据库 | ★你存了多少就有多少★ |
★一个只保留近期历史的标准端点是默认状态,不是缺陷。★ 存完整链数据很贵,所以大多数服务商会裁剪,并把更深的历史作为单独一档来卖。
对任何涉及财务的事情,后果是:你不能拿 RPC 当你的记录系统。 一份税务报表、一份盈亏表、或者六个月后的一次客户争议,要的都是你自己保留下来的数据。
// ★边跑边存,不要等到需要时才去取。★
await db.transactions.insert({
signature: sig,
slot: outcome.slot,
blockTime: outcome.blockTime,
err: outcome.err,
meta: extractWhatYouNeed(tx),
});
从 meta 里读余额变化
一笔历史交易里最有用的部分不是指令 —— 而是到底什么东西动了:
const pre = tx.meta.preTokenBalances ?? [];
const post = tx.meta.postTokenBalances ?? [];
for (const p of post) {
const before = pre.find((b) => b.accountIndex === p.accountIndex);
const delta =
BigInt(p.uiTokenAmount.amount) -
BigInt(before?.uiTokenAmount.amount ?? "0");
if (delta !== 0n) console.log(p.mint, delta);
}
★preTokenBalances 和 postTokenBalances 不用解码任何一条指令,就告诉你什么变了。★ 对账时它比解析指令数据可靠得多,因为它反映的是真实结果,包括 CPI 内部发生的一切。
原生 SOL 用 preBalances 和 postBalances,按账户索引对应。注意手续费是从支付方的差额里出的 —— 所以 accountKeys[0] 的差额里含着它。
失败的交易也在里面
getSignaturesForAddress 会把失败交易和成功交易一起返回,靠 err 区分:
const failed = sigs.filter((s) => s.err !== null);
★一笔失败的交易照样落了块、照样消耗了基础手续费。★ 忽略它们的对账对不上钱包余额,因为那些手续费花在了什么都没改变的交易上。
注意什么不在这个列表里:从没落块的交易。它们在任何地方都没留下记录 —— 这正是你自己的日志重要的原因:链没法告诉你一次过期掉的尝试。
限流与这个调用
这两个方法都很重,而历史回填是耗尽配额的常见方式:
有意识地翻页。 一次 limit: 1000 是一个请求;为了充实它而发的一千次 getTransaction 是另外一千个。
只对需要的取详情。 签名列表本身已经带了 slot、区块时间和错误状态。如果这就够了,就不要去取完整交易。
增量运行用 until。 ★每次运行都把同一段历史重走一遍,是回填撞上限流最常见的单一原因。★
回填和交易分开。 历史任务和机器人共用一个端点,意味着回填可以把发送饿死。
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★只有落了块的交易才有历史可言。★ 一笔过期的交易在链上不留痕迹,所以你提交路径的可靠性,决定了你的活动中有多少是日后还能被重建出来的。
BoltTx 在这里的位置
我们负责提交,不做索引也不做历史。你查询用什么,继续用什么。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。因为端点互相独立,一次历史回填耗尽不了你交易发出去的那条路径。
你在本地签名。我们不托管资金、不代签、不修改交易内容。tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
const sender = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
Solana 交易能往回查多久? 在标准 RPC 上只有近期历史,更旧的会被裁剪掉。要完整历史得用 archive 节点,或者一家提供扩展保留时长的服务商套餐。
getTransaction 查一笔旧交易为什么返回 null?
要么节点把它裁剪了,要么你漏了 maxSupportedTransactionVersion: 0 而它是版本化交易。先查后者,再去假设前者。
getSignaturesForAddress 怎么翻页?
把上一页最后一条签名作为 before 传进去。它从新到旧返回、单次最多 1000 条,所以往更早翻意味着重复调用。
before 和 until 有什么区别?
before 从某个签名往回走,until 在某个签名处停下。增量同步用 until 配你处理过的最后一条签名,这样只取新增的。
能给 before 或 until 传 slot 号吗? 不能,两个都要签名。传 slot 会返回没用的东西、而调用本身还是成功的 —— 这让它成为一个很难诊断的失败。
怎么看一笔交易实际动了什么?
比对 meta 里的 preTokenBalances 和 postTokenBalances,原生 SOL 比对 preBalances 和 postBalances。它反映的是真实结果,包括 CPI 造成的影响。
手续费会出现在余额差额里吗?
会,在手续费支付方的原生余额变化里。accountKeys[0] 的差额包含手续费,所以把这笔变动归因到某次交易时要把它减掉。
签名列表里包含失败的交易吗?
包含,带着 err。它们落了块并消耗了基础手续费,所以跳过它们的对账会对不上钱包余额。
能找到从没落块的交易吗? 不能。一笔过期的交易在链上任何地方都不留记录 —— 这就是你自己的提交日志是那些尝试唯一来源的原因。
能拿 RPC 当记账记录吗? 不能。标准节点会裁剪,所以任何几个月后还要用的东西,都必须在它发生时就存下来。把 RPC 当实时数据源,不要当记录系统。
怎么回填历史又不撞限流?
有意识地翻页、只在签名元数据不够时才取完整交易详情,并用 until 让每次运行只覆盖新增活动。
不取详情的话,签名列表里有什么?
签名、slot、区块时间、错误状态和 memo。如果这就回答了你的问题,就完全跳过 getTransaction,每条签名省一个请求。
我的对账为什么和钱包余额对不上? 常见原因是:忽略了照样付了手续费的失败交易、漏算了支付方差额里的手续费,或者 RPC 在你存下来之前就把那段历史裁掉了。
blockTime 总是有值吗? 通常有,但某些更早的条目可能是 null。slot 永远存在而且单调递增,所以排序优先用它,展示再用区块时间。
一次调用最多返回多少条签名?
1000 条。超出就必须用 before 翻页,这让深度历史变成一串请求,而不是一次查询。
历史查询该和我的机器人共用端点吗? 最好不要。回填很重,能把共享配额耗尽 —— 而那恰恰会在机器人最需要发送的时候把它饿死。