Solana网络拥堵下稳定发送SPL代币的技术咨询
Solana网络拥堵时稳定发送SPL代币的优化方案
问题描述
尝试在Solana网络拥堵时稳定发送SPL代币,使用Jupiter的强制多发送交易函数,50次尝试仅成功2次。观察到Phantom钱包可无失败发送,说明方案可行,但自身代码调整重试次数(最高至100000)、提高计算费用后,稳定性仍无法保障,寻求优化建议。
当前代码实现
const { Connection, Keypair, VersionedTransaction, PublicKey, TransactionExpiredBlockheightExceededError, VersionedTransactionResponse, Transaction } = require("@solana/web3.js"); const { createTransferInstruction, getAssociatedTokenAddress, TOKEN_PROGRAM_ID, getOrCreateAssociatedTokenAccount } = require('@solana/spl-token'); const fetch = require("cross-fetch"); const { Wallet } = require("@project-serum/anchor"); const bs58 = require("bs58"); const promiseRetry = require("promise-retry"); async function getWallet(privateKeys) { return new Wallet(Keypair.fromSecretKey(bs58.decode(privateKeys))) } const QUICKNODE_RPC ="https://mainnet.helius-rpc.com/"; const connection = new Connection(QUICKNODE_RPC, "confirmed"); const wait = (time) => new Promise((resolve) => setTimeout(resolve, time)); async function transactionSenderAndConfirmationWaiter({ connection, serializedTransaction, blockhashWithExpiryBlockHeight, }) { const txid = await connection.sendRawTransaction( serializedTransaction, { skipPreflight: true } ); const controller = new AbortController(); const abortSignal = controller.signal; const abortableResender = async () => { while (true) { await wait(2_000); if (abortSignal.aborted) return; try { await connection.sendRawTransaction( serializedTransaction, { skipPreflight: true } ); } catch (e) { console.warn(`Failed to resend transaction: ${e}`); } } }; try { abortableResender(); const lastValidBlockHeight = blockhashWithExpiryBlockHeight.lastValidBlockHeight - 150; await Promise.race([ connection.confirmTransaction( { ...blockhashWithExpiryBlockHeight, lastValidBlockHeight, signature: txid, abortSignal, }, "confirmed" ), new Promise(async (resolve) => { while (!abortSignal.aborted) { await wait(2_000); const tx = await connection.getSignatureStatus(txid, { searchTransactionHistory: false, }); if (tx?.value?.confirmationStatus === "confirmed") { resolve(tx); } } }), ]); } catch (e) { if (e instanceof TransactionExpiredBlockheightExceededError) { console.log("TransactionExpiredBlockheightExceededError") return null; } else { throw e; } } finally { controller.abort(); } const response = promiseRetry( async (retry) => { const response = await connection.getTransaction(txid, { commitment: "confirmed", maxSupportedTransactionVersion: 0, }); if (!response) { retry(response); } return response; }, { retries: 100000, minTimeout: 1e3, } ); return response; } const sendSplTokenWithBase58 = async ( privateKeyBase58, toKeyBase58, numOfToken, tokenAddress, connection, decimal = 6 ) => { try { console.log("started"); const privateKey = bs58.decode(privateKeyBase58); const fromKeyPair = Keypair.fromSecretKey(privateKey); const mintAddress = new PublicKey(tokenAddress); const tokenAccount = await getOrCreateAssociatedTokenAccount( connection, fromKeyPair, mintAddress, fromKeyPair.publicKey, ); const destinationAccount = await getOrCreateAssociatedTokenAccount( connection, fromKeyPair, mintAddress, new PublicKey(toKeyBase58), ); const transaction = new Transaction({ feePayer: fromKeyPair.publicKey }) .add( createTransferInstruction( tokenAccount.address, destinationAccount.address, fromKeyPair.publicKey, numOfToken * Math.pow(10, decimal), ), ); const latestBlockHash = await connection.getLatestBlockhash("confirmed"); transaction.recentBlockhash = latestBlockHash.blockhash; // Sign the transaction transaction.sign(fromKeyPair); // Serialize the transaction const serializedTransaction = transaction.serialize(); // Get blockhash with expiry block height const blockhashWithExpiryBlockHeight = { blockhash: latestBlockHash.blockhash, lastValidBlockHeight: latestBlockHash.lastValidBlockHeight }; // Send and confirm the transaction using the new function const transactionResponse = await transactionSenderAndConfirmationWaiter({ connection, serializedTransaction, blockhashWithExpiryBlockHeight, }); // Extract relevant information from the transaction response const transactionHash = transactionResponse?.transaction?.signatures[0].toBase58(); const transactionStatus = transactionResponse ? true : false; const splTokensSent = numOfToken; console.log({ transactionHash, transactionStatus, splTokensSent }); return { transactionHash, transactionStatus, splTokensSent }; } catch (error) { console.error("Error occurred:", error); return { transactionHash: false }; } };
已尝试的调整
修改sendRawTransaction参数提高费用与重试次数:
const txid = await connection.sendRawTransaction( serializedTransaction, { skipPreflight: true, maxRetries: 5, priorityFeePerComputeUnit: 25000 } // Increased fee and added maxRetries );
优化建议与代码调整
1. 切换至VersionedTransaction(核心优化)
传统Transaction已被Solana官方弃用,Phantom等钱包默认使用VersionedTransaction,其在拥堵网络下的打包优先级更高。修改交易创建逻辑:
// 替换原Transaction创建代码 const instructions = [ createTransferInstruction( tokenAccount.address, destinationAccount.address, fromKeyPair.publicKey, numOfToken * Math.pow(10, decimal), ) ]; // 使用processed状态获取最新blockhash,减少过期概率 const latestBlockhash = await connection.getLatestBlockhash("processed"); const messageV0 = new TransactionMessage({ payerKey: fromKeyPair.publicKey, recentBlockhash: latestBlockhash.blockhash, instructions, }).compileToV0Message(); const transaction = new VersionedTransaction(messageV0); transaction.sign([fromKeyPair]); const serializedTransaction = transaction.serialize();
2. 动态计算优先级费用
固定费用在拥堵时缺乏竞争力,通过RPC获取实时网络费用并上浮:
// 获取SPL转账所需的实时优先级费用 const prioritizationFees = await connection.getRecentPrioritizationFees([ { account: TOKEN_PROGRAM_ID, computeUnits: 1000 // SPL转账计算量约为1000CU } ]); // 取网络平均费用,上浮30%确保优先级 const baseFee = prioritizationFees[0]?.prioritizationFee || 25000; const adjustedPriorityFee = Math.floor(baseFee * 1.3); // 发送交易时使用调整后的费用 const txid = await connection.sendRawTransaction(serializedTransaction, { skipPreflight: false, // 开启预检查,避免发送无效交易 priorityFeePerComputeUnit: adjustedPriorityFee });
3. 优化重试逻辑(避免无效重试)
当前重试仅重复发送过期交易,需改为重新构建交易+获取新blockhash:
// 修改sendSplTokenWithBase58函数,增加重试逻辑 const MAX_RETRIES = 20; let attempt = 0; while (attempt < MAX_RETRIES) { try { // 这里放入原交易构建、发送的逻辑(使用VersionedTransaction) // ... // 成功则返回结果 return { transactionHash, transactionStatus, splTokensSent }; } catch (error) { attempt++; console.log(`Retry ${attempt}/${MAX_RETRIES} failed:`, error.message); // 交易过期时,等待1秒后重试 if (error instanceof TransactionExpiredBlockheightExceededError) { await wait(1000); } else { // 其他错误直接抛出或终止 throw error; } } } throw new Error("Max retries exceeded");
4. 简化交易指令(减少计算量)
取消不必要的getOrCreateAssociatedTokenAccount,先查询账户是否存在:
// 替换原getOrCreateAssociatedTokenAccount逻辑 const destinationAddress = await getAssociatedTokenAddress( mintAddress, new PublicKey(toKeyBase58) ); // 检查接收方账户是否存在 const destinationAccountInfo = await connection.getAccountInfo(destinationAddress); let destinationAccount; if (!destinationAccountInfo) { // 不存在时再创建 destinationAccount = await createAssociatedTokenAccount( connection, fromKeyPair, mintAddress, new PublicKey(toKeyBase58) ); } else { destinationAccount = { address: destinationAddress }; }
5. 优化RPC节点与连接配置
- 使用付费RPC节点,提高拥堵时的吞吐量
- 修改连接的commitment为
processed,更快获取最新blockhash:
const connection = new Connection(QUICKNODE_RPC, "processed");
6. 调整重发策略
缩短重发间隔至1秒,同时避免高频发送导致限流:
// 修改abortableResender中的等待时间 await wait(1000);
内容的提问来源于stack exchange,提问作者Olaiwola Ayoola
相关产品推荐
相关产品推荐

