Helius 的质押连接保证 100% 的交易交付,并且确认时间最短。

摘要

我们推荐以下最佳实践来帮助您完成交易:
  • 使用承诺 “processed” 或 “confirmed” 来获取最新的 blockhash
  • 添加优先费用动态计算
  • 优化计算单元 (CU) 的使用
  • 将 maxRetries 设置为 0 并实现强大的重试逻辑
  • 发送时将 skipPreflight 设置为 true(可选)
想深入了解吗?我们在这篇博客文章中涵盖了所有基础知识。

为交易者推荐的优化

以下优化建议适用于对延迟敏感的交易者。您必须已经应用了上述发送交易的最佳实践。
  • 您的客户端服务器(用于发送交易的机器)应位于美国东部或西欧。
  • 如果您想与 Helius 交易发送服务器共址,请选择法兰克福或匹兹堡。
  • 避免从远离验证器网络的地区发送(例如,拉丁美洲或南非)。
  • 预热 Helius 区域缓存以最小化尾部延迟。
  • 每秒使用与发送交易相同的端点和 API 密钥发送一个 getHealth 调用。
  • 每个区域只需要一个预热线程。再多也没有任何好处。
这些好处只有经验丰富的交易者才能注意到。我们建议应用开发者查看发送智能交易部分。

发送智能交易

Helius 的 Node.jsRust SDK 都可以发送智能交易。这种新方法在处理确认状态的同时构建并发送优化的交易。用户可以配置交易的发送选项,例如是否跳过预检检查。 在最基本的层面上,用户必须提供他们的密钥对和他们希望执行的指令,其余的由我们处理。 我们:
  • 获取最新的 blockhash
  • 构建初始交易
  • 模拟初始交易以获取消耗的计算单元
  • 将计算单元限制设置为上一步中消耗的计算单元,并留有一定余量
  • 通过我们的 Priority Fee API 获取 Helius 推荐的优先费用
  • 将优先费用(每计算单元的 microlamports)设置为 Helius 推荐的费用
  • 添加一个小的安全缓冲费用,以防推荐值在接下来的几秒钟内发生变化
  • 构建并发送优化的交易
  • 如果成功,返回交易签名
要求推荐值(或更高)用于我们的质押连接,确保 Helius 发送高质量的交易,并且我们不会被验证者限速。
这种方法被设计为在 Solana 上构建、发送和落地交易的最简单方式。 请注意,通过使用 Helius 推荐的费用,Helius 用户在我们的标准付费计划之一上发送的交易将通过我们的质押连接路由,几乎保证 100% 的交易交付。

Node.js SDK

sendSmartTransaction 方法在我们的 Helius Node.js SDK 中可用,适用于 版本 >= 1.3.2。要更新到更高版本的 SDK,请运行 npm update helius-sdk 以下示例将 SOL 转移到您选择的账户。它利用 sendSmartTransaction 发送一个不跳过预检检查的优化交易:
import { Helius } from "helius-sdk";
import {
  Keypair,
  SystemProgram,
  LAMPORTS_PER_SOL,
  TransactionInstruction,
} from "@solana/web3.js";

const helius = new Helius("YOUR_API_KEY");
const fromKeypair = /* Your keypair goes here */;
const fromPubkey = fromKeypair.publicKey;
const toPubkey = /* The person we're sending 0.5 SOL to */;

const instructions: TransactionInstruction[] = [
  SystemProgram.transfer({
    fromPubkey: fromPubkey,
    toPubkey: toPubkey,
    lamports: 0.5 * LAMPORTS_PER_SOL, 
  }),
];

const transactionSignature = await helius.rpc.sendSmartTransaction(instructions, [fromKeypair]);
console.log(`Successful transfer: ${transactionSignature}`);

Rust SDK

send_smart_transaction 方法在我们的 Rust SDK 中可用,适用于 版本 >= 0.1.5。要更新到更高版本的 SDK,请运行 cargo update helius 以下示例将 0.01 SOL 转移到您选择的账户。它利用 send_smart_transaction 发送一个跳过预检检查并在必要时重试两次的优化交易:
use helius::types::*;
use helius::Helius;
use solana_sdk::{
    pubkey::Pubkey,
    signature::Keypair,
    system_instruction
};

#[tokio::main]
async fn main() {
    let api_key: &str = "YOUR_API_KEY";
    let cluster: Cluster = Cluster::MainnetBeta;
    let helius: Helius = Helius::new(api_key, cluster).unwrap();
    
    let from_keypair: Keypair = /* Your keypair goes here */;
    let from_pubkey: Pubkey = from_keypair.pubkey();
    let to_pubkey: Pubkey = /* The person we're sending 0.01 SOL to */;

    // Create a simple instruction (transfer 0.01 SOL from from_pubkey to to_pubkey)
    let transfer_amount = 100_000; // 0.01 SOL in lamports
    let instruction = system_instruction::transfer(&from_pubkey, &to_pubkey, transfer_amount);

    // Create the SmartTransactionConfig
    let config = SmartTransactionConfig {
        instructions,
        signers: vec![&from_keypair],
        send_options: RpcSendTransactionConfig {
            skip_preflight: true,
            preflight_commitment: None,
            encoding: None,
            max_retries: Some(2),
            min_context_slot: None,
        },
        lookup_tables: None,
    };

    // Send the optimized transaction
    match helius.send_smart_transaction(config).await {
        Ok(signature) => {
            println!("Transaction sent successfully: {}", signature);
        }
        Err(e) => {
            eprintln!("Failed to send transaction: {:?}", e);
        }
    }
}

不使用 SDK 发送交易

我们建议使用我们的 SDK 之一发送智能交易,但不使用 SDK 也可以实现相同的功能。 Node.js SDKRust SDK 都是开源的,因此可以随时查看发送智能交易功能的底层代码。

准备和构建初始交易

首先,准备和构建初始交易。这包括创建一个带有一组指令的新交易,添加最近的 blockhash,并指定费用支付者。对于版本化交易,创建一个 TransactionMessage 并在存在查找表时进行编译。然后,创建一个新的版本化交易并签名——这是模拟交易的下一步所必需的,因为交易必须签名。 例如,如果我们想准备一个版本化交易:
// Prepare your instructions and set them to an instructions variable
// The payerKey is the public key that will be paying for this transaction
// Prepare your lookup tables and set them to a lookupTables variable
let recentBlockhash = (await this.connection.getLatestBlockhash()).blockhash;
const v0Message = new TransactionMessage({
    instructions: instructions,
    payerKey: pubKey,
    recentBlockhash: recentBlockhash,
}).compileToV0Message(lookupTables);
versionedTransaction = new VersionedTransaction(v0Message);
versionedTransaction.sign([fromKeypair]);

优化交易的计算单元 (CU) 使用

为了优化交易的计算单元 (CU) 使用,我们可以使用 simulateTransaction RPC 方法来模拟交易。模拟交易将返回使用的 CU 数量,因此我们可以使用此值来相应地设置我们的计算限制。 建议首先使用带有所需指令的测试交易,再加上一条将计算限制设置为1.4m CUs的指令。这是为了确保交易模拟成功。 例如:
const testInstructions = [
    ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }),
    ...instructions,
];

const testTransaction = new VersionedTransaction(
    new TransactionMessage({
        instructions: testInstructions,
        payerKey: payer,
        recentBlockhash: (await this.connection.getLatestBlockhash()).blockhash,
    }).compileToV0Message(lookupTables)
);

const rpcResponse = await this.connection.simulateTransaction(testTransaction, {
    replaceRecentBlockhash: true,
    sigVerify: false,
});

const unitsConsumed = rpcResponse.value.unitsConsumed;
还建议增加一些余量以确保交易执行没有任何问题。 我们可以通过设置以下内容来实现:
let customersCU = Math.ceil(unitsConsumed * 1.1);
然后,创建一个指令,将计算单元限制设置为此值,并将其添加到您的指令数组中:
const computeUnitIx = ComputeBudgetProgram.setComputeUnitLimit({
    units: customersCU
});
instructions.push(computeUnitIx);

序列化和编码交易

这相对简单。 首先,要序列化交易,Transaction 和 VersionedTransaction 类型都有一个 .serialize() 方法。然后使用 bs58 package 来编码交易。 您的代码应类似于 bs58.encode(txt.serialize());

设置正确的优先费用

首先,使用 Priority Fee API 获取优先费用估算。我们希望传入我们的交易并通过推荐参数获取 Helius 推荐的费用:
const response = await fetch(HeliusURL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
        jsonrpc: "2.0",
        id: "1",
        method: "getPriorityFeeEstimate",
        params: [
            {
                transaction: bs58.encode(versionedTransaction), // Pass the serialized transaction in
                options: { recommended: true },
            },
        ],   
    }),
});

const data = await response.json();
const priorityFeeRecommendation = data.result.priorityFeeEstimate;
然后,创建一个指令,将计算单元价格设置为此值,并将该指令添加到您之前的指令中:
const computeBudgetIx = ComputeBudgetProgram.setComputeUnitPrice({
    microLamports: priorityFeeRecommendation,
});

instructions.push(computeBudgetIx);

构建并发送优化后的交易

这一步几乎是第一步的重复。然而,初始指令数组已被更改,以最佳方式添加了两个指令来设置计算单元限制和价格。 现在,发送交易。
无论是否使用预检检查发送或更改任何其他发送选项都无关紧要——交易将通过我们的质押连接路由,适用于所有付费计划。

轮询交易状态和重新广播

虽然质押连接会将交易直接转发给领导者,但交易仍有可能在银行阶段被丢弃。建议用户使用自己的重新广播逻辑,而不是依赖RPC为他们重试交易。
sendTransaction RPC方法有一个maxRetries参数,可以设置以覆盖RPC的默认重试逻辑,从而让开发者对重试过程有更多控制。一个常见的模式是通过getLatestBlockhash获取当前的blockhash,存储lastValidBlockHeight,并在blockhash过期前重试交易。只有在blockhash不再有效时才重新签署交易是至关重要的,否则两个交易都有可能被网络接受。 一旦交易被发送,重要的是轮询其确认状态,以查看网络是否已处理并确认交易,然后再重试。使用getSignatureStatuses RPC方法检查一组交易的确认状态。 @solana/web3.js SDK在其Connection类中也有一个getSignatureStatuses方法,用于获取多个签名的当前状态。

sendSmartTransaction如何处理轮询和重新广播

sendSmartTransaction方法有一个60秒的超时时间。由于一个blockhash在150个槽位内有效,并假设每个槽位完美地为400毫秒,我们可以合理地假设交易的blockhash将在一分钟后失效。 该方法发送交易并在此超时时间内轮询其交易签名:
try {
   // Create a smart transaction
   const transaction = await this.createSmartTransaction(instructions, signers, lookupTables, sendOptions);
  
   const timeout = 60000;
   const startTime = Date.now();
   let txtSig;
  
   while (Date.now() - startTime < timeout) {
     try {
       txtSig = await this.connection.sendRawTransaction(transaction.serialize(), {
         skipPreflight: sendOptions.skipPreflight,
         ...sendOptions,
       });
  
       return await this.pollTransactionConfirmation(txtSig);
     } catch (error) {
       continue;
     }
   }
} catch (error) {
   throw new Error(`Error sending smart transaction: ${error}`);
}
txtSig 被设置为刚刚发送的交易的签名。然后,该方法使用 pollTransactionConfirmation() 方法轮询交易的确认状态。此方法每五秒检查一次交易状态,最多检查三次。 如果在此期间交易未被确认,则返回错误:
async pollTransactionConfirmation(txtSig: TransactionSignature): Promise<TransactionSignature> {
    // 15 second timeout
    const timeout = 15000;
    // 5 second retry interval
    const interval = 5000;
    let elapsed = 0;

    return new Promise<TransactionSignature>((resolve, reject) => {
      const intervalId = setInterval(async () => {
        elapsed += interval;

        if (elapsed >= timeout) {
          clearInterval(intervalId);
          reject(new Error(`Transaction ${txtSig}'s confirmation timed out`));
        }

        const status = await this.connection.getSignatureStatuses([txtSig]);

        if (status?.value[0]?.confirmationStatus === "confirmed") {
          clearInterval(intervalId);
          resolve(txtSig);
        }
      }, interval);
   });
}