钱包显示有余额。交易报 insufficient funds 失败。两件事同时为真。
★在 Solana 上,"余额"和"能花的"是两个不同的数字,而制造这个差额的机制有三种。★
这个错误不是一个错误
Transfer: insufficient lamports 5000000, need 5001000
Error: insufficient funds
insufficient funds for rent
custom program error: 0x1
它们来自不同的层,意思也不同:
| 消息 | 来自 | 意思 |
|---|---|---|
insufficient lamports X, need Y |
★System 程序★ | ★精确数字 —— 读它★ |
insufficient funds for rent |
运行时 | ★会跌破租金豁免线★ |
Token 程序 0x1 |
SPL Token | 代币余额,不是 SOL |
| preflight 阶段被拒 | RPC | 手续费支付方付不起手续费 |
★第一条是 Solana 开发里最有用的一条错误信息★ —— 它告诉你到底差多少,而这两个数字的差额,通常就点明了是哪一种机制在起作用。
租金豁免是被锁住,不是被花掉
每个账户都必须持有一个最低余额才能存在。★那个最低额不能拿来花。★
const balance = await connection.getBalance(wallet);
const rentExempt = await connection.getMinimumBalanceForRentExemption(0);
// ★这才是真实的数字。★
const spendable = balance - rentExempt - feeBuffer;
一个余额恰好等于 getMinimumBalanceForRentExemption 返回值的钱包,有余额,但什么都花不了。 试图让余额低于这个下限会报 insufficient funds for rent,即便转账本身的算术是成立的。
这些 lamport 是可以收回的 —— 关闭账户就会退回 —— 但账户还在用的时候不行。
手续费从同一份余额里出
手续费支付方的账户要覆盖基础手续费和优先费,这和交易本身要转移的东西是分开的。
// ★把全部余额转出去,必然失败。★
SystemProgram.transfer({ fromPubkey: wallet, toPubkey: dest, lamports: balance });
// ★给手续费和租金下限留出空间。★
const fee = 5000 + priorityFeeLamports;
const sendable = balance - rentExempt - fee;
★优先费是大家算这个数时最常忘掉的那部分。★ 基础手续费是可预测的;而推导出来的优先费不是,拥堵时它可以是基础费的很多倍。一个只为基础手续费预留的机器人,会恰好在费用飙升时失败 —— 而那正是它最想交易的时候。
如果你的余额算对了、交易还是会错过,免费领个 BoltTx key 改一行就能测测提交路径。
wrapped SOL 是另一份余额
这个会产出这类错误里最让人困惑的版本:
const native = await connection.getBalance(wallet); // ★lamport★
const wsolAta = await getAssociatedTokenAddress(NATIVE_MINT, wallet);
const wrapped = await connection.getTokenAccountBalance(wsolAta); // ★代币★
★它们不同步变动。★ 一笔花 wSOL 的 swap 会报余额不足,而 getBalance 显示钱多得很 —— 因为在你显式包装或解包之前,这两份余额毫无关系。
包装是一次转账加一次同步:
const ixs = [
createAssociatedTokenAccountIdempotentInstruction(payer, ata, owner, NATIVE_MINT),
SystemProgram.transfer({ fromPubkey: owner, toPubkey: ata, lamports: amount }),
createSyncNativeInstruction(ata), // ★不加这个,代币余额一直是 0★
];
createSyncNativeInstruction 是最常被漏掉的那一步。 把 lamport 转进账户还不够 —— 代币余额字段必须被更新成一致,不做同步的话,账户里握着 lamport 却报告零代币余额。
代币账户也要租金
一笔需要创建目标代币账户的 swap,在交易金额之外还要为那个账户的租金准备 SOL:
const ataRent = await connection.getMinimumBalanceForRentExemption(165);
★一个 SOL 恰好够交易的钱包,在这笔交易还要创建账户时会失败。★ 对交易新代币的机器人来说,这几乎每一次首次买入都会发生 —— 这也是"我已经持有的代币就没事"成为一个常见且具误导性的观察的原因。
构建之前先检查
async function canAfford(connection, wallet, amount, willCreateAta) {
const balance = await connection.getBalance(wallet);
const rentExempt = await connection.getMinimumBalanceForRentExemption(0);
const ataRent = willCreateAta
? await connection.getMinimumBalanceForRentExemption(165)
: 0;
const fees = 5000 + estimatedPriorityFee;
const required = amount + rentExempt + ataRent + fees;
return { ok: balance >= required, short: required - balance };
}
★报告差额,不要只给一个布尔值。★ 对面向用户的机器人来说,"余额不足"和"你还需要 0.003 SOL 来覆盖代币账户"之间的差别,就是一张工单和一个自己解决掉的问题之间的差别。
这个检查是一种体贴,不是一个保证。 余额可能在交易落块之前变化 —— 所以真正保护你的是链上检查,而这个预检存在的意义是产出一条好的错误信息,不是取代它。
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★一次在提交前抓到的余额不足不花钱;一次在链上抓到的要付基础手续费。★ 这让预检值得跑,尽管它没法成为权威判断。
BoltTx 在这里的位置
我们负责提交。余额管理完全留在你的代码里。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。
你在本地签名。我们不托管资金、不代签、不修改交易内容。★tip 带在交易里、从你自己的钱包链上支付,所以它也是你需要预留余额的一部分★ —— 交易失败时它跟着一起 revert,这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
我明明有 SOL,为什么报 insufficient funds? 因为租金豁免的那部分必须留在账户里,而手续费也从同一份余额里出。能花的 = 余额 − 租金豁免 − 手续费,不是钱包显示的那个数。
Solana 的租金豁免是什么? 每个账户为了持续存在必须持有的最低余额。它是被锁住而不是被花掉,关闭账户时会退回,但账户还在时它永远不能被转出去。
怎么算我实际能转出多少 SOL? 余额 − 租金豁免下限 − 基础手续费 − 优先费。把全部余额转出去必然失败,因为手续费没有别的地方可出。
getBalance 显示够,我的 swap 为什么失败? 多半这笔 swap 花的是 wrapped SOL,那是一份独立的代币账户余额。原生 SOL 和 wSOL 在你显式包装或解包之前不同步变动。
怎么正确地包装 SOL?
为原生 mint 创建关联代币账户、把 lamport 转进去,然后调 createSyncNativeInstruction。不做同步的话,账户握着 lamport 却报告零代币余额。
转了 lamport 进去,wSOL 余额为什么还是 0? 漏了同步指令。转 lamport 不会更新代币余额字段,必须同步之后,代币程序才看得见这个余额。
买一个新代币需要额外的 SOL 吗? 需要,在交易金额之外还要关联代币账户的租金。这就是为什么同一笔买入对新代币会失败、对已持有的代币却成功。
insufficient lamports X, need Y 是什么意思? 字面意思 —— 你有 X,而交易需要 Y。两者的差额通常就点明了你漏算的是哪一项,典型的是租金或优先费。
优先费从我的余额里出吗? 是,和基础手续费一起从支付方账户出。只为基础手续费预留意味着在拥堵时失败 —— 而那正是费用飙升、也是你最想交易的时候。
Token 程序的 0x1 是什么错误? 是代币余额不足,不是 SOL 不足。它意味着那个代币账户里该代币不够,和缺 lamport 是两个不同的问题。
手续费余量该留多少? 够在抬高的优先费下发好几笔交易的量,而不是按基础费发一笔的量。只留最低限度的机器人,会恰好在条件最差时失败。
代币账户的租金能收回吗? 能,关闭账户就会把租金豁免的 lamport 退给 owner。交易很多代币的机器人会累积这些账户,关掉空的能收回真金白银。
同样的交易为什么对一个代币能用、对另一个不行? 通常是因为一个需要创建代币账户、另一个不需要。差的就是那笔额外租金,而它出现在任何新代币的首次买入上。
发送之前该检查余额吗? 该,为了产出一条有用的错误信息。它不是保证,因为余额可能在落块前变化 —— 真正保护你的是链上检查。
该怎么告诉用户出了什么问题? 报告差额数字,不要只说"余额不足"。 告诉某人他还需要一个具体的数额,能让这个问题不经过客服对话就被解决掉。
失败的交易还会花掉我的 SOL 吗? 如果它落块并 revert 了,会 —— 基础手续费付掉了。如果它在执行之前就被拒绝,不产生手续费,但机会照样丢了。