一次限时 NFT mint,把 Solana 提交环节的所有难题压缩进了大约十秒钟。
几千个参与者在同一个窗口内、对着同一批账户、争抢同一份有限供应量。你提交路径上的任何弱点,只会在那里暴露,别的地方都不会 —— 这就是为什么测试时完美的 mint 机器人,到了当天就挂。
开抢那一刻为什么是最难的
普通拥堵是弥散的。mint 恰恰相反:这是一次所有人都知道精确秒数的、协同的尖峰。
三件事同时发生:
区块空间竞争达到顶点。 每个参与者都往同样的几个 slot 里挤。
特定账户上的 priority fee 飙升。 费用是按可写账户竞争的,而所有人都在写同一个 candy machine 和 collection 账户。★一分钟前还很慷慨的费用,在开抢那一刻什么都不是。★
供应量是有限的。 和交易不同 —— 交易晚成交只是价格差一点,而 ★mint 晚了就是没有。没有部分得分。★
最后这一点改变了优化目标。交易中你为"多次尝试的期望值"调优;★而 mint 只有一个窗口,交易要么落在里面,要么结果为零。★
倒计时开始前,一切必须就绪
可避免的失败里,最大的一类是:把本可以在 T-10 分钟做完的事,留到了 T-0 去做。
// 后台持续刷新。绝不在 mint 那一刻去取。
let cached = await connection.getLatestBlockhash("confirmed");
setInterval(async () => {
cached = await connection.getLatestBlockhash("confirmed");
}, 5_000);
// 预热连接,让热路径上不发生 TLS 握手。
await connection.getSlot();
// 提前推导好所有 PDA 和派生地址。
const mintPdas = await precomputeAllAddresses();
★从"倒计时归零"到"字节上线"之间的每一次网络往返,都是你白送出去的一个 slot。★ 一次 getLatestBlockhash 调用、一次 DNS 解析、一次 TLS 握手 —— 每一个都能靠提前准备避免掉。
无法预先算好的是 mint keypair(如果程序要求每次 mint 用一个新的)。那也提前生成好,放进一个池子里,让生成不在关键路径上。
如果准备工作已经做扎实、问题在最后一跳,免费领个 BoltTx key 改一行就能拿来对比。
开抢那一刻的费用
近期费用数据反映的是 mint 之前的那一刻,而不是你正在提交进去的那一刻。
import { ComputeBudgetProgram } from "@solana/web3.js";
// 传入你实际会写的账户。mint 期间,
// 所有人写的就是和你一样的那几个。
const recent = await connection.getRecentPrioritizationFees({
lockedWritableAccounts: [candyMachine, collectionMint, yourTokenAccount],
});
const fees = recent.map((r) => r.prioritizationFee).sort((a, b) => a - b);
const median = fees[Math.floor(fees.length / 2)] ?? 0;
instructions.unshift(
// ★mint 是这些账户一生中竞争最激烈的时刻。
// 近期中位数是下限,不是目标。★
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: Math.max(median * 5, 50_000),
}),
ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 }),
);
★mint 指令是计算密集的★ —— 创建账户、写元数据、验证 collection,往往还有好几次 CPI。在 devnet 上对一次真实 mint 跑 simulateTransaction,读 unitsConsumed,再按它加余量设置 setComputeUnitLimit。估低了会让一笔本来能成功的交易失败,而开抢那一刻没有第二次机会。
重试循环在这里比别处更重要
开抢那一刻只提交一次,就是掷硬币。持续重试,窗口关了就停:
const raw = tx.serialize();
const sig = await connection.sendRawTransaction(raw, {
skipPreflight: true, // ★一次往返都省不出来★
maxRetries: 0, // 我们自己驱动
});
while (await connection.getBlockHeight("confirmed") <= lastValidBlockHeight) {
const { value } = await connection.getSignatureStatuses([sig]);
if (value[0]) break; // 已上链;看 .err 区分售罄还是成功
await connection.sendRawTransaction(raw, {
skipPreflight: true,
maxRetries: 0,
});
await new Promise((r) => setTimeout(r, 400));
}
skipPreflight 在这里比任何地方都重要。preflight 要花一次往返,而且它模拟的是当前 slot —— 而 mint 期间,它模拟所依据的那个状态正被几千笔其它交易改写。★开抢那一刻模拟通过,什么都预测不了。★
正确解读失败
mint 结束后,交易失败的原因看起来很像,含义却完全不同:
| 结果 | 含义 | 是你的问题吗 |
|---|---|---|
状态返回 null |
★从未上链★ | ★是 —— 费用、路径、重试★ |
| 上链,售罄错误 | 你晚了 | 部分是 |
| 上链,超计算预算 | ★CU limit 太低★ | ★是 —— 本可避免★ |
| 上链,余额不足 | 余额不够 | 是 |
| 上链,不在白名单 | 没资格 | 不是 |
★第二行是诚实的结果 —— 你参与了竞争然后输了。★ 第一行和第三行是自己造成的,而那两行才是值得下功夫的。
const { value } = await connection.getSignatureStatuses([sig], {
searchTransactionHistory: true,
});
if (!value[0]) log("never_landed"); // 提交侧问题
else if (value[0].err) log("landed_failed", value[0].err);
else log("minted");
多开钱包不会成倍提高中签率
一个值得拆穿的常见假设。
用五个钱包提交,意味着五笔交易在同样竞争激烈的 slot 里互相竞争,每笔各付各的费用。★排程是按交易计算的,所以五笔平庸的提交加不出一笔好的。★
更糟的是,它们可能写同一批账户、互相串行化。你可能一边和自己竞争,一边付五倍的费用。
如果这次 mint 有每钱包上限、而你确实想要多个名额,那是另一回事。但作为"提高单个钱包中签率"的策略,它不成立。
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★mint 期间那个数字会拉长★ —— 大部分交易不受影响,少数明显变慢。而 mint 按定义就把你放进了那少数里,因为这场 mint 本身就是那个拥堵事件。
开抢时落在两个 slot 内,通常还在供应量之内;五个 slot,通常已经没了。
开抢前检查清单
□ blockhash 已缓存,并在后台定时刷新
□ 连接已预热 —— 热路径上没有 TLS 握手
□ 所有 PDA 和 `getAssociatedTokenAddress` 结果已提前算好
□ mint keypair 已提前生成、放进池子
□ `setComputeUnitLimit` 由 `simulateTransaction` 测得,并加了余量
□ `getRecentPrioritizationFees` 查询的是具体那几个可写账户
□ 重试循环以 lastValidBlockHeight 为界
□ skipPreflight 开,maxRetries 设 0
□ 失败分类:never_landed 还是 landed_failed
★以上每一项都能在倒计时开始前验证。★ 做了这件事,区别在于:你的机器人是"因为一个你能修的原因失败",还是"因为一个你永远不知道的原因失败"。
BoltTx 在这里的位置
我们做最后一跳。不做 mint 数据源、不做白名单管理、不做索引。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前,传输途中观察不到。你在本地签名,我们不托管资金、不代签、不修改交易内容。
tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
// 选离你机器人最近的区域
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
然后在一次真实 mint 里记录 slot 距离,做个对比。
常见问题
我的 NFT mint 交易为什么失败了?
先看它到底有没有上链。状态返回 null 说明它从未进区块 —— 那是费用、路由或重试的问题。上链但带错误,说明你到了链上、被别的东西挡住了,比如供应量已经没了。
Solana 上怎么尽可能快地 mint? 在开抢之前把一切做完:后台刷新的 blockhash 缓存、预热连接、预算好 PDA、预生成 keypair。开抢那一刻立即提交,并且每个 slot 重试一次,直到 blockhash 过期。
NFT mint 该设多少 priority fee? 远高于相关账户上的近期中位数。mint 是那些账户一生中竞争最激烈的时刻,所以近期费用数据反映的是已经不适用的状况。
多开钱包能提高中签率吗? 不能。排程是按交易计算的,几笔平庸的提交合不成一笔好的。它们还可能写同一批账户、互相串行化,同时让你付好几倍的费用。
NFT mint 需要多少 compute unit? 比普通转账多,因为 mint 涉及创建账户、写元数据,往往还有好几次 CPI。在 devnet 上对一次真实 mint 模拟测量,然后加余量 —— 估低了会让一笔本来能成功的交易失败。
mint 该开 skipPreflight 吗? 该。preflight 要花一次你省不出来的往返,而且它模拟的状态正被几千笔其它交易改写。开抢那一刻模拟通过,对你的结果没有任何预测力。
为什么我的交易上链了却提示售罄? 你到达链上时供应量已经被抢完了。这是诚实的竞争结果,不是 bug —— 不过更小的 slot 距离本可以改善你的位置。
开抢前多久该开始准备? 提前几分钟就把一切缓存好、预热好,不是提前几秒。开抢那一刻唯一该发生的事,是签名和提交。
mint 交易能预签吗? 只有当 mint 地址和所有账户都提前已知时才行,而那不常见。你能做的是预生成 keypair、预算好派生地址,让这些不出现在关键路径上。
为什么我的机器人测试正常、mint 当天就挂? 测试发生在网络平静时,那时候每条提交路径表现都一样。mint 是一次协同尖峰,而那是唯一能让差别显现的时刻。
mint 该用什么 blockhash? 一个后台缓存、短周期刷新的。开抢那一刻去取,会在唯一付不起往返的地方加一次往返;而启动时缓存的那个,等你要用时已经接近过期。
mint 值得上独占 RPC 节点吗? 独占容量解决的是限流,不是上链。mint 期间的约束是"你的交易怎么被路由到出块方"以及那条路径背后的质押权重 —— 而独占性改变不了这两样。
怎么知道我是输在速度还是资格上? 给失败分类。从未上链是提交问题;上链带售罄错误是你晚了;上链带白名单错误是资格问题。这三个的解法完全不同。
mint 售罄之后还该继续重试吗? 不该。一旦交易上链并返回售罄错误,重试只会复现同样的失败,并且再付一次基础费。碰到这个特定错误就停。
mint 的现实 slot 目标是多少? 落在开抢后一两个 slot 内。再往后,你通常是在和一份已经分配完的供应量竞争,而再怎么重试也改变不了。