You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 02:20:54