Solana NFT mint:怎么在开抢那一刻把交易送上链

限时 mint 是 Solana 上最难的提交场景。开抢头几秒发生了什么、大多数机器人为什么栽在那里,以及倒计时开始前该准备好什么。

BoltTx Team··15 min read
solananft-mint交易上链拥堵candy-machine交易机器人

一次限时 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 内。再往后,你通常是在和一份已经分配完的供应量竞争,而再怎么重试也改变不了。

延伸阅读

← 返回博客列表