一笔 swap 报 AccountNotFound,而你的代码看起来哪儿都没错。钱包在,mint 在,池子也在。
不存在的是代币账户 —— 那个"某个钱包持有某个 mint"的位置。在 Solana 上,持有一种代币需要一个对应的账户,而它在有人创建之前并不存在。
ATA 是什么
关联代币账户(ATA)是由钱包 + mint 确定性推导出来的地址。同样的钱包加同样的 mint,永远得到同样的地址 —— 所以谁都不需要去查表。
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
const ata = getAssociatedTokenAddressSync(
mint, // 哪个代币
owner, // 哪个钱包
false, // ★allowOwnerOffCurve —— 见下文★
);
★推导地址永远会成功。而这完全不说明这个账户在链上存不存在。★ 这是最大的困惑来源 —— 你手上有一个合法地址,它指向的是空的。
const info = await connection.getAccountInfo(ata);
if (!info) {
// 地址合法。账户还不存在。
}
四种失败方式
AccountNotFound。 你引用了一个从未被创建的 ATA。这个钱包第一次持有这个 mint。
MintMismatch。 你用错误的 mint 推导了地址。账户存在,但它装的是另一种代币。
OwnerMismatch。 你用错误的 owner 推导,或者签名者不是这个账户的所有者。
allowOwnerOffCurve 传错了。 ★这个产生的 bug 最让人摸不着头脑。★ 当 owner 是一个 PDA 而不是普通钱包时,必须传 true。传 false 的话,函数要么抛异常,要么推导出一个和链上账户不同的地址。
// 普通钱包 owner。
getAssociatedTokenAddressSync(mint, wallet, false);
// ★PDA owner —— 比如一个程序控制的金库。★
getAssociatedTokenAddressSync(mint, pda, true);
如果你的账户处理都对、交易却仍然不上链,免费领个 BoltTx key 改一行就能测测路由那一侧。
在交易里顺手创建
惯用解法是在前面加一条"账户已存在就什么都不做"的创建指令:
import { createAssociatedTokenAccountIdempotentInstruction } from "@solana/spl-token";
const instructions = [
// ★幂等:每次都带上都是安全的。★
createAssociatedTokenAccountIdempotentInstruction(
payer, // 支付租金豁免
ata,
owner,
mint,
),
...yourSwapInstructions,
];
★用幂等版本,不要用普通版本。★ 非幂等的那条在账户已存在时会失败,这意味着你得先读一次 —— 而那次读取是热路径上你付不起的一次网络往返,还会留下一个竞态:在你读取和提交之间,别的东西可能把账户创建了。
它的代价
两项成本,而第二项是让人意外的那个。
租金豁免。 每个代币账户是 165 字节,必须租金豁免。由 payer 出这笔钱,而它会一直锁着,直到账户被关闭。
计算单元。 创建账户在计算上不是免费的。★一笔同时创建两个 ATA 的 swap,消耗明显高于对已有账户做同样的 swap★ —— 而一个按后者测出来的 compute limit,在前者身上会失败。
// 按最坏情况模拟 —— 首次买入,两个账户都缺。
const sim = await connection.simulateTransaction(txWithAtaCreation, {
sigVerify: false,
replaceRecentBlockhash: true,
});
const limit = Math.ceil((sim.value.unitsConsumed ?? 200_000) * 1.2);
这件事怎么咬交易机器人
真正花钱的模式是这个:
★你每买一个新代币,就要新建一个 ATA。★ 对做 meme 币的机器人来说,这意味着几乎每一次首次买入都要创建账户 —— 额外的计算、额外锁住的租金,而且恰好发生在最时间敏感的那些交易里。
狙击一个新上线
→ 你从未持有过这个 mint
→ ★交易必须创建 ATA★
→ 更多计算、更多租金、更大的交易体积
→ 全部发生在最拥堵的那一刻
两个值得知道的缓解办法:
为你预期会交易的代币预先创建 ATA。 对上新场景不可行(mint 那时还不存在),但对固定的几个交易对有用。
按"要创建账户"的情况来定 compute 预算。 ★按首次买入测你的 limit,不是按重复买入。★ 一个按便宜路径调出来的 limit,会在昂贵路径上失败 —— 而且是在最糟的时刻失败。
关闭与回收
空的 ATA 可以关闭,租金豁免会还给你:
import { createCloseAccountInstruction } from "@solana/spl-token";
const ix = createCloseAccountInstruction(ata, destination, owner);
★只对零余额有效 —— 关闭一个还有代币的账户会烧掉它们。★ 要在同一笔交易里检查余额,别依赖一个"等你落块时可能已经过时"的读取。
对一个交易过几百个代币的机器人来说,定期清理能收回相当可观的资金 —— 那些钱本来躺在只剩粉尘的账户里。
Token-2022 的账户不一样
值得单独提,因为它会产生一种特别让人困惑的失败。
Token-2022 和最初的 SPL Token 是两个不同的程序。★Token-2022 mint 的 ATA 在另一个 program ID 下推导★,所以用默认参数推导会给你一个错误的地址。
import { TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
const ata = getAssociatedTokenAddressSync(
mint,
owner,
false,
TOKEN_2022_PROGRAM_ID, // ★Token-2022 mint 必须传★
);
如果对 Token-2022 的 mint 不传这个参数,你会得到一个看起来完全合法、但永远不会有任何内容的地址。
排查清单
① ATA 存在吗? → getAccountInfo(ata)
② 推导对不对? → 检查 mint、owner、allowOwnerOffCurve
③ 是哪个代币程序? → Token 还是 Token-2022
④ SOL 够付租金吗? → getMinimumBalanceForRentExemption(165)
⑤ 创建的 compute 预算够吗? → 按首次买入模拟
★第 3 步是最耗时间的一个★ —— 因为一切看起来都对,而那个地址只是属于另一个程序的账户。
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★ATA 相关的失败发生在执行阶段,意味着交易已经进了区块、付了基础费。★ 预防比重试便宜 —— 尤其是解法通常只是一条幂等指令。
BoltTx 在这里的位置
我们做提交。账户结构在你签名的那笔交易里就已经确定,在到达我们之前。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前,传输途中观察不到。你在本地签名,我们不托管资金、不代签、不修改交易内容。
tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
Solana 的关联代币账户是什么? 一个由钱包和 mint 确定性推导出的地址,是那个钱包持有那种代币的位置。同一对永远推出同一个地址,不需要查表 —— 但账户在被创建之前并不存在。
Solana swap 里为什么报 AccountNotFound? 几乎总是缺 ATA。这个钱包从未持有过那个 mint,所以代币账户从未被创建过。在前面加一条幂等创建指令就能解决。
怎么创建关联代币账户?
用 createAssociatedTokenAccountIdempotentInstruction,放在其它指令之前。账户已存在时它什么都不做,所以每笔交易都带上是安全的。
幂等版和普通版的创建指令有什么区别? 普通版在账户已存在时会失败,所以你得先读一次。幂等版在那种情况下是空操作 —— 这同时省掉了那次往返,也消除了"读取和提交之间"的竞态。
allowOwnerOffCurve 是干什么的?
它允许为一个 PDA(而不是普通钱包)推导 ATA。对 PDA owner 传 false 的话,函数要么抛异常,要么推出一个和链上账户不同的地址。
为什么我算出的 ATA 地址和链上对不上? 查三件事:mint、owner、以及这个 mint 是不是 Token-2022 的。Token-2022 的 mint 在另一个 program ID 下推导,漏掉它会得到一个看起来合法、实际上什么都没有的地址。
一个 ATA 要花多少钱?
165 字节对应的租金豁免额(是锁住不是花掉),加上创建所需的计算单元。查 getMinimumBalanceForRentExemption(165),别写死。
关联代币账户能关闭吗?
能,用 createCloseAccountInstruction,租金豁免会还给你。只对零余额有效 —— 关闭一个还有代币的账户会烧掉它们。
为什么第一次买某个代币消耗的计算更多? 因为那笔交易还要创建 ATA。创建账户的计算量是加在 swap 之上的,所以按重复买入测出来的 limit,在首次买入时会失败。
交易机器人该预先创建 ATA 吗? 对固定的几个交易对,该 —— 这把创建成本从时间敏感的那笔交易里移走了。对上新场景不可行,因为 mint 在上新之前并不存在。
MintMismatch 是什么? 你引用的代币账户装的 mint 和指令期望的不一样。通常是用错误的 mint 推导了 ATA,或者缓存了上一个代币的旧地址。
Token-2022 的 mint 用同样的 ATA 推导方式吗?
推导函数是同一个,但必须传 TOKEN_2022_PROGRAM_ID。不传的话,你会在原始代币程序下推出一个地址,而它永远不会和真实账户对上。
我的机器人会积累多少个代币账户? 它持有过多少种不同的 mint,就有多少个。交易品种多的机器人会积累几百个,每个都锁着租金直到被关闭,而大多数最后只剩已清仓头寸的粉尘。
两个钱包能共用一个代币账户吗? 不能。ATA 是由特定的 owner 和 mint 推导的,所以每个钱包每种代币都有自己的账户。程序控制的金库可以替别人持币,但那是另一种账户结构。
我的 ATA 创建为什么报 InsufficientFundsForRent? payer 的 SOL 不够付豁免额,或者一笔并发交易先把余额消耗掉了。按你的在途交易数量预留余量。
每笔交易前都该检查 ATA 存不存在吗? 不该 —— 那次读取是热路径上付不起的一次往返,而且结果到提交时可能已经过时。用幂等创建指令,让链上去解决这件事。
延伸阅读
- Solana 租金豁免
- Solana 交易错误码
- Solana 交易模拟
- Solana compute unit 计价
- Solana 交易上链完全指南