在 Solana 上发送交易有两种主要方法:
- 使用抵押连接 (默认)
- 使用专业着陆服务如Sender (推荐)
本文涵盖使用 staked connections 的交易优化最佳实践,这是所有 Helius 付费计划的默认方法。
Staked connections 最适合于延迟对您的业务不关键的用例(例如,支付、钱包、社交应用等)。
如果您是高级交易员(例如,高频交易、MEV 搜索者、套利者、代币狙击手等)并寻找专门的超低延迟交易着陆服务,请阅读我们的 Sender 教程。
Helius 的 staked connections 保证 100% 的交易交付,确认时间最短。为了优化使用 staked connections 的交易着陆率,我们建议以下最佳实践:
- 使用提交 “confirmed” 来获取最新区块哈希
- 添加优先费用并动态计算
- 优化计算单元(CU)使用
- 设置
maxRetries 为0并实施强大的重试逻辑
- 将
skipPreflight 设置为 true (可选)
想深入了解吗?我们在这篇 博客文章 中涵盖了所有基础知识。
为交易员推荐的优化
对于对延迟敏感的交易用例,我们建议 使用 Sender。
但是,如果您使用 staked connections 并希望优化您的设置以获得尽可能低的延迟,我们建议以下优化(除了应用上述最佳实践):
- 你的客户端服务器(用于发送交易的机器)应该位于美国东部或西欧。
- 如果想与Helius交易发送服务器共同定位,请选择FRA或PIT。
- 避免从离验证器网络较远的地区(如 LATAM,南非)发送。
- 预热Helius区域缓存以最小化尾部延迟。
- 每个区域只需一个预热线程——多余的没有任何好处。
- 每秒使用相同的端点和API密钥发送一个
getHealth RPC 调用。
这些优势只有经验丰富的交易员才能察觉。对于普通应用开发人员,我们建议遵循下面智能交易发送部分的指南。
发送智能交易
Helius Node.js 和 Rust SDK 都可以发送智能交易。这种新方法构建并发送优化的交易,同时处理其确认状态。
用户可以配置交易的发送选项,比如是否应跳过预检。
在最基本的层面上,用户必须提供他们的密钥对和要执行的指令,其他的我们来处理。
我们:
- 获取最新的区块哈希
- 构建初始交易
- 模拟初始交易以获取消耗的计算单元(CUs)
- 将CU限制设置为上一步消耗的CUs,加上一些余量
- 通过我们的优先费API获取Helius推荐的优先费用
- 将优先费用(每CU的微拉姆波兹)设置为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 SDK 和 Rust SDK 都是开源的,因此可以随时查看发送智能交易功能的底层代码。
准备并构建初始交易
首先,准备并构建初始交易。这包括创建带有一组指令的新交易,添加最近的区块哈希,并指定费用支付者。
对于版本化交易,创建一个 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 CU 的指令。
这样做是为了确保交易模拟成功。
例如:
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 包 对交易进行编码。
您的代码应类似于 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);
构建并发送优化后的交易
这一步几乎是第一步的重复。然而,初始指令数组已经被更改,以最佳方式添加两个指令来设置计算单元限制和价格。
现在,发送交易。
无论是否使用预检检查发送或更改其他发送选项,交易都会通过我们所有付费计划的已质押连接进行路由。
轮询交易状态和重新广播
虽然已质押连接会直接将交易转发给领导者,但交易仍有可能在 Banking Stage 中被丢弃。建议用户使用自己的重新广播逻辑,而不是依赖于 RPC 为其重试交易。
sendTransaction RPC 方法 有一个 maxRetries 参数,可以设置为覆盖 RPC 的默认重试逻辑,使开发者能够更好地控制重试过程。
常见的模式是通过 getLatestBlockhash 获取当前 blockhash,存储 lastValidBlockHeight,并在 blockhash 过期前重试交易。
只有在 blockhash 不再有效时才重新签名交易是至关重要的,否则有可能两个交易都会被网络接受。
一旦发送交易,重要的是轮询其确认状态,以查看网络是否已处理和确认,然后再重试。使用 getSignatureStatuses RPC 方法 检查交易列表的确认状态。
@solana/web3.js SDK 也有一个 getSignatureStatuses 方法在其 Connection 类上用于获取多个签名的当前状态。
sendSmartTransaction 如何处理轮询和重广播
sendSmartTransaction 方法有一个 60 秒的超时时间。由于 blockhash 在 150 个槽中有效,并假设每个槽 400ms,我们可以合理地假设交易的 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);
});
}