use crate::error::{HeliusError, Result};
use crate::request_handler::SDK_USER_AGENT;
use crate::types::{
CreateSmartTransactionConfig, CreateSmartTransactionSeedConfig, GetPriorityFeeEstimateOptions,
GetPriorityFeeEstimateRequest, GetPriorityFeeEstimateResponse, PriorityLevel, SenderSendOptions, SmartTransaction,
SmartTransactionConfig, Timeout,
};
use crate::Helius;
use std::collections::HashSet;
use std::str::FromStr;
use std::sync::Arc;
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use bincode::{serialize, ErrorKind};
use phf::phf_map;
use rand::Rng;
use reqwest::StatusCode;
use serde_json::json;
use solana_client::{
rpc_client::SerializableTransaction,
rpc_config::{RpcSendTransactionConfig, RpcSimulateTransactionConfig},
rpc_response::{Response, RpcSimulateTransactionResult},
};
use solana_commitment_config::CommitmentConfig;
use solana_compute_budget_interface::ComputeBudgetInstruction;
use solana_sdk::signature::keypair_from_seed;
use solana_sdk::{
bs58::encode,
hash::Hash,
instruction::Instruction,
message::AddressLookupTableAccount,
message::{v0, VersionedMessage},
pubkey::Pubkey,
signature::{Signature, Signer},
signer::keypair::Keypair,
transaction::{Transaction, VersionedTransaction},
};
use solana_system_interface::instruction as system_instruction;
use solana_transaction_status::TransactionConfirmationStatus;
use std::time::{Duration, Instant};
use tokio::time::sleep;
const CU_BUFFER_MULTIPLIER_DEFAULT: f32 = 1.25;
const MIN_TIP_LAMPORTS_DUAL: u64 = 200_000;
const MIN_TIP_LAMPORTS_SWQOS: u64 = 5_000;
fn collect_unique_signers(signers: &[Arc<dyn Signer>], fee_payer: Option<&Arc<dyn Signer>>) -> Vec<Arc<dyn Signer>> {
let mut all_signers: Vec<Arc<dyn Signer>> = Vec::with_capacity(signers.len() + usize::from(fee_payer.is_some()));
let mut seen: HashSet<Pubkey> = HashSet::with_capacity(all_signers.capacity());
if let Some(fee_payer) = fee_payer {
if seen.insert(fee_payer.pubkey()) {
all_signers.push(fee_payer.clone());
}
}
for signer in signers {
if seen.insert(signer.pubkey()) {
all_signers.push(signer.clone());
}
}
all_signers
}
fn collect_unique_keypair_refs<'a>(signers: &'a [Keypair], fee_payer: &'a Keypair) -> Vec<&'a Keypair> {
let mut all_signers: Vec<&Keypair> = Vec::with_capacity(signers.len() + 1);
let mut seen: HashSet<Pubkey> = HashSet::with_capacity(all_signers.capacity());
if seen.insert(fee_payer.pubkey()) {
all_signers.push(fee_payer);
}
for signer in signers {
if seen.insert(signer.pubkey()) {
all_signers.push(signer);
}
}
all_signers
}
fn is_retryable_confirmation_error(err: &HeliusError) -> bool {
matches!(err, HeliusError::Timeout { .. })
}
const TIP_FLOOR_URL: &str = "https://bundles.jito.wtf/api/v1/bundles/tip_floor";
const SENDER_TIP_ACCOUNTS: [&str; 10] = [
"4ACfpUFoaSD9bfPdeu6DBt89gB6ENTeHBXCAi87NhDEE",
"D2L6yPZ2FmmmTKPgzaMKdhu6EWZcTpLy1Vhx8uvZe7NZ",
"9bnz4RShgq1hAnLnZbP8kbgBg1kEmcJBYQq3gQbmnSta",
"5VY91ws6B2hMmBFRsXkoAAdsPHBJwRfBht4DXox3xkwn",
"2nyhqdwKcJZR2vcqCyrYsaPVdAnFoJjiksCXJ7hfEYgD",
"2q5pghRs6arqVjRvT5gfgWfWcHWmw1ZuCzphgd5KfWGJ",
"wyvPkWjVZz1M8fHQnMMCDTQDbkManefNNhweYk5WkcF",
"3KCKozbAaF75qEU33jtzozcJ29yJuaLJTy2jFdzUY8bT",
"4vieeGHPYPG2MmyPRcYjdiDmmhN3ww7hsFNap8pVN3Ey",
"4TQLFNWK8AovT1gFvda5jfw2oJeRMKEmw7aH6MGBJ3or",
];
pub static SENDER_ENDPOINTS: phf::Map<&'static str, &'static str> = phf_map! {
"Default" => "http://sender.helius-rpc.com",
"US_SLC" => "http://slc-sender.helius-rpc.com",
"US_EAST" => "http://ewr-sender.helius-rpc.com",
"EU_WEST" => "http://lon-sender.helius-rpc.com",
"EU_CENTRAL" => "http://fra-sender.helius-rpc.com",
"EU_NORTH" => "http://ams-sender.helius-rpc.com",
"AP_SINGAPORE" => "http://sg-sender.helius-rpc.com",
"AP_TOKYO" => "http://tyo-sender.helius-rpc.com",
};
pub static SENDER_REGION_ALIASES: phf::Map<&'static str, &'static str> = phf_map! {
"US-EAST" => "US_EAST",
"US-SLC" => "US_SLC",
"EU-WEST" => "EU_WEST",
"EU-CENTRAL" => "EU_CENTRAL",
"EU-NORTH" => "EU_NORTH",
"AP-SINGAPORE" => "AP_SINGAPORE",
"AP-TOKYO" => "AP_TOKYO",
};
const SENDER_DEFAULT_BASE: &str = "http://slc-sender.helius-rpc.com";
#[inline]
fn normalize_region(region: &str) -> &str {
SENDER_REGION_ALIASES.get(region).copied().unwrap_or(region)
}
#[inline]
fn sender_base_url(region: &str) -> &'static str {
let key: &str = normalize_region(region);
SENDER_ENDPOINTS.get(key).copied().unwrap_or(SENDER_DEFAULT_BASE)
}
#[inline]
pub fn sender_fast_url(region: &str) -> String {
format!("{}/fast", sender_base_url(region))
}
#[inline]
pub fn sender_ping_url(region: &str) -> String {
format!("{}/ping", sender_base_url(region))
}
async fn post_to_sender(tx64: &str, opts: &SenderSendOptions) -> Result<Signature> {
let mut endpoint: String = sender_fast_url(&opts.region);
if opts.swqos_only {
endpoint.push_str("?swqos_only=true");
}
let body = json!({
"jsonrpc": "2.0",
"id": format!("helius-rust-{}", std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis()),
"method": "sendTransaction",
"params": [
tx64,
{ "encoding": "base64", "skipPreflight": true, "maxRetries": 0 }
]
});
let res = reqwest::Client::new()
.post(&endpoint)
.header("User-Agent", SDK_USER_AGENT)
.json(&body)
.send()
.await
.map_err(|e| HeliusError::InvalidInput(format!("Sender request error: {e}")))?;
let status = res.status();
if !status.is_success() {
let text = res.text().await.unwrap_or_default();
Err(HeliusError::InvalidInput(format!(
"Sender HTTP {}: {}",
status,
text.chars().take(200).collect::<String>()
)))
} else {
let val: serde_json::Value = res
.json()
.await
.map_err(|e| HeliusError::InvalidInput(format!("Sender JSON parse error: {e}")))?;
if let Some(s) = val.as_str() {
return Signature::from_str(s)
.map_err(|e| HeliusError::InvalidInput(format!("Invalid signature from Sender: {e}")));
}
if let Some(err) = val.get("error") {
return Err(HeliusError::InvalidInput(format!("Sender error: {err}")));
}
if let Some(result) = val.get("result").and_then(|r| r.as_str()) {
return Signature::from_str(result)
.map_err(|e| HeliusError::InvalidInput(format!("Invalid signature from Sender: {e}")));
}
Err(HeliusError::InvalidInput(format!(
"Unexpected Sender response: {}",
val.to_string().chars().take(200).collect::<String>()
)))
}
}
impl Helius {
fn build_unsigned_preflight_tx(
payer: &Pubkey,
instructions: &[Instruction],
lookup_tables: Option<&[AddressLookupTableAccount]>,
recent_blockhash: Hash,
) -> Result<Vec<u8>> {
if let Some(luts) = lookup_tables {
let v0_message: v0::Message = v0::Message::try_compile(payer, instructions, luts, recent_blockhash)?;
let versioned_tx: VersionedTransaction = VersionedTransaction {
signatures: vec![],
message: VersionedMessage::V0(v0_message),
};
serialize(&versioned_tx).map_err(|e: Box<ErrorKind>| crate::error::HeliusError::InvalidInput(e.to_string()))
} else {
let mut tx: Transaction = Transaction::new_with_payer(instructions, Some(payer));
tx.message.recent_blockhash = recent_blockhash;
serialize(&tx).map_err(|e: Box<ErrorKind>| crate::error::HeliusError::InvalidInput(e.to_string()))
}
}
pub async fn get_compute_units(
&self,
instructions: Vec<Instruction>,
payer: Pubkey,
lookup_tables: Vec<AddressLookupTableAccount>,
signers: Option<&[Arc<dyn Signer>]>,
) -> Result<Option<u64>> {
let test_instructions: Vec<Instruction> = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)]
.into_iter()
.chain(instructions)
.collect::<Vec<_>>();
let recent_blockhash: Hash = self.connection().get_latest_blockhash()?;
let v0_message: v0::Message =
v0::Message::try_compile(&payer, &test_instructions, &lookup_tables, recent_blockhash)?;
let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message);
let transaction: VersionedTransaction = if let Some(signers) = signers {
VersionedTransaction::try_new(versioned_message, signers)
.map_err(|e| HeliusError::InvalidInput(format!("Signing error: {:?}", e)))?
} else {
VersionedTransaction {
signatures: vec![],
message: versioned_message,
}
};
let config: RpcSimulateTransactionConfig = RpcSimulateTransactionConfig {
sig_verify: signers.is_some(),
..Default::default()
};
let result: Response<RpcSimulateTransactionResult> = self
.connection()
.simulate_transaction_with_config(&transaction, config)?;
Ok(result.value.units_consumed)
}
pub async fn poll_transaction_confirmation(&self, txt_sig: Signature) -> Result<Signature> {
let timeout: Duration = Duration::from_secs(15);
let interval: Duration = Duration::from_secs(5);
let start: Instant = Instant::now();
loop {
if start.elapsed() >= timeout {
return Err(HeliusError::Timeout {
code: StatusCode::REQUEST_TIMEOUT,
text: format!("Transaction {}'s confirmation timed out", txt_sig),
});
}
let status = self.connection().get_signature_statuses(&[txt_sig])?;
match status.value[0].clone() {
Some(status) => {
if status.err.is_none()
&& (status.confirmation_status == Some(TransactionConfirmationStatus::Confirmed)
|| status.confirmation_status == Some(TransactionConfirmationStatus::Finalized))
{
return Ok(txt_sig);
}
if let Some(err) = status.err {
return Err(HeliusError::TransactionError(err));
}
}
None => {
sleep(interval).await;
}
}
}
}
pub async fn create_smart_transaction(
&self,
config: &CreateSmartTransactionConfig,
) -> Result<(SmartTransaction, u64)> {
if config.signers.is_empty() {
return Err(HeliusError::InvalidInput(
"The fee payer must sign the transaction".to_string(),
));
}
let payer_pubkey: Pubkey = config
.fee_payer
.as_ref()
.map_or(config.signers[0].pubkey(), |signer| signer.pubkey());
let (recent_blockhash, last_valid_block_hash) = self
.connection()
.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())?;
let mut final_instructions: Vec<Instruction> = vec![];
let existing_compute_budget_instructions: bool = config.instructions.iter().any(|instruction| {
instruction.program_id == ComputeBudgetInstruction::set_compute_unit_limit(0).program_id
|| instruction.program_id == ComputeBudgetInstruction::set_compute_unit_price(0).program_id
});
if existing_compute_budget_instructions {
return Err(HeliusError::InvalidInput(
"Cannot provide instructions that set the compute unit price and/or limit".to_string(),
));
}
let is_versioned: bool = config.lookup_tables.is_some();
let preflight_bytes: Vec<u8> = Helius::build_unsigned_preflight_tx(
&payer_pubkey,
&config.instructions,
config.lookup_tables.as_deref(),
recent_blockhash,
)?;
let transaction_base58: String = encode(&preflight_bytes).into_string();
let priority_fee_request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest {
transaction: Some(transaction_base58),
account_keys: None,
options: Some(GetPriorityFeeEstimateOptions {
priority_level: Some(PriorityLevel::High),
..Default::default()
}),
};
let priority_fee_estimate: GetPriorityFeeEstimateResponse =
self.rpc().get_priority_fee_estimate(priority_fee_request).await?;
let priority_fee_recommendation: u64 =
priority_fee_estimate
.priority_fee_estimate
.ok_or(HeliusError::InvalidInput(
"Priority fee estimate not available".to_string(),
))? as u64;
let priority_fee: u64 = if let Some(provided_fee) = config.priority_fee_cap {
std::cmp::min(priority_fee_recommendation, provided_fee)
} else {
priority_fee_recommendation
};
let compute_budget_ix: Instruction = ComputeBudgetInstruction::set_compute_unit_price(priority_fee);
let mut updated_instructions: Vec<Instruction> = config.instructions.clone();
updated_instructions.push(compute_budget_ix.clone());
final_instructions.push(compute_budget_ix);
let all_signers: Vec<Arc<dyn Signer>> = collect_unique_signers(&config.signers, config.fee_payer.as_ref());
let units: Option<u64> = self
.get_compute_units(
updated_instructions,
payer_pubkey,
config.lookup_tables.clone().unwrap_or_default(),
Some(&all_signers),
)
.await?;
if units.is_none() {
return Err(HeliusError::InvalidInput(
"Error fetching compute units for the instructions provided".to_string(),
));
}
let compute_units: u64 = units.ok_or(HeliusError::InvalidInput(
"Error fetching compute units for the instructions provided".to_string(),
))?;
let multiplier: f32 = config.cu_buffer_multiplier.unwrap_or(CU_BUFFER_MULTIPLIER_DEFAULT);
let customers_cu: u32 = if compute_units < 1000 {
1000
} else {
(compute_units as f64 * multiplier as f64).ceil() as u32
};
let compute_units_ix: Instruction = ComputeBudgetInstruction::set_compute_unit_limit(customers_cu);
final_instructions.push(compute_units_ix);
final_instructions.extend(config.instructions.clone());
if is_versioned {
let lookup_tables: &[AddressLookupTableAccount] = config.lookup_tables.as_deref().unwrap();
let v0_message: v0::Message =
v0::Message::try_compile(&payer_pubkey, &final_instructions, lookup_tables, recent_blockhash)?;
let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message);
let versioned_transaction: VersionedTransaction =
VersionedTransaction::try_new(versioned_message, all_signers.as_slice())
.map_err(|e| HeliusError::InvalidInput(format!("Signing error: {:?}", e)))?;
Ok((
SmartTransaction::Versioned(versioned_transaction),
last_valid_block_hash,
))
} else {
let mut tx: Transaction = Transaction::new_with_payer(&final_instructions, Some(&payer_pubkey));
tx.try_partial_sign(&all_signers, recent_blockhash)?;
Ok((SmartTransaction::Legacy(tx), last_valid_block_hash))
}
}
pub async fn send_smart_transaction(&self, config: SmartTransactionConfig) -> Result<Signature> {
let (transaction, last_valid_block_height) = self.create_smart_transaction(&config.create_config).await?;
match transaction {
SmartTransaction::Legacy(tx) => {
self.send_and_confirm_transaction(
&tx,
config.send_options,
last_valid_block_height,
Some(config.timeout.into()),
)
.await
}
SmartTransaction::Versioned(tx) => {
self.send_and_confirm_transaction(
&tx,
config.send_options,
last_valid_block_height,
Some(config.timeout.into()),
)
.await
}
}
}
pub async fn send_and_confirm_transaction(
&self,
transaction: &impl SerializableTransaction,
send_transaction_config: RpcSendTransactionConfig,
last_valid_block_height: u64,
timeout: Option<Duration>,
) -> Result<Signature> {
let timeout: Duration = timeout.unwrap_or(Duration::from_secs(60));
let start_time: Instant = Instant::now();
while Instant::now().duration_since(start_time) < timeout
|| self.connection().get_block_height()? <= last_valid_block_height
{
let result = self
.connection()
.send_transaction_with_config(transaction, send_transaction_config);
match result {
Ok(signature) => {
match self.poll_transaction_confirmation(signature).await {
Ok(sig) => return Ok(sig),
Err(err) if is_retryable_confirmation_error(&err) => continue,
Err(err) => return Err(err),
}
}
Err(_) => continue,
}
}
Err(HeliusError::Timeout {
code: StatusCode::REQUEST_TIMEOUT,
text: "Transaction failed to confirm in 60s".to_string(),
})
}
pub async fn get_compute_units_thread_safe(
&self,
instructions: Vec<Instruction>,
payer: Pubkey,
lookup_tables: Vec<AddressLookupTableAccount>,
keypairs: Option<&[&Keypair]>,
) -> Result<Option<u64>> {
let test_instructions: Vec<Instruction> = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)]
.into_iter()
.chain(instructions)
.collect::<Vec<_>>();
let recent_blockhash: Hash = self.connection().get_latest_blockhash()?;
let v0_message: v0::Message =
v0::Message::try_compile(&payer, &test_instructions, &lookup_tables, recent_blockhash)?;
let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message);
let transaction: VersionedTransaction = if let Some(keypairs) = keypairs {
VersionedTransaction::try_new(versioned_message, keypairs)
.map_err(|e| HeliusError::InvalidInput(format!("Signing error: {:?}", e)))?
} else {
VersionedTransaction {
signatures: vec![],
message: versioned_message,
}
};
let config: RpcSimulateTransactionConfig = RpcSimulateTransactionConfig {
sig_verify: keypairs.is_some(),
..Default::default()
};
let result: Response<RpcSimulateTransactionResult> = self
.connection()
.simulate_transaction_with_config(&transaction, config)?;
Ok(result.value.units_consumed)
}
pub async fn create_smart_transaction_with_seeds(
&self,
create_config: &CreateSmartTransactionSeedConfig,
) -> Result<(SmartTransaction, u64)> {
if create_config.signer_seeds.is_empty() {
return Err(HeliusError::InvalidInput(
"At least one signer seed must be provided".to_string(),
));
}
let keypairs: Vec<Keypair> = create_config
.signer_seeds
.iter()
.map(|seed| keypair_from_seed(seed).expect("Failed to create keypair from seed"))
.collect();
let fee_payer: Keypair = if let Some(fee_payer_seed) = create_config.fee_payer_seed {
keypair_from_seed(&fee_payer_seed).expect("Failed to create keypair from seed")
} else {
keypairs[0].insecure_clone()
};
let (recent_blockhash, last_valid_block_hash) = self
.connection()
.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())?;
let mut final_instructions: Vec<Instruction> = vec![];
let preflight_bytes = Self::build_unsigned_preflight_tx(
&fee_payer.pubkey(),
&create_config.instructions,
create_config.lookup_tables.as_deref(),
recent_blockhash,
)?;
let transaction_base58: String = encode(&preflight_bytes).into_string();
let priority_fee_request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest {
transaction: Some(transaction_base58),
account_keys: None,
options: Some(GetPriorityFeeEstimateOptions {
priority_level: Some(PriorityLevel::High),
..Default::default()
}),
};
let priority_fee_estimate: GetPriorityFeeEstimateResponse =
self.rpc().get_priority_fee_estimate(priority_fee_request).await?;
let priority_fee_recommendation: u64 =
priority_fee_estimate
.priority_fee_estimate
.ok_or(HeliusError::InvalidInput(
"Priority fee estimate not available".to_string(),
))? as u64;
let priority_fee: u64 = if let Some(provided_fee) = create_config.priority_fee_cap {
std::cmp::min(priority_fee_recommendation, provided_fee)
} else {
priority_fee_recommendation
};
final_instructions.push(ComputeBudgetInstruction::set_compute_unit_price(priority_fee));
let mut test_instructions: Vec<Instruction> = final_instructions.clone();
test_instructions.extend(create_config.instructions.clone());
let all_signers: Vec<&Keypair> = collect_unique_keypair_refs(&keypairs, &fee_payer);
let units: Option<u64> = self
.get_compute_units_thread_safe(
test_instructions,
fee_payer.pubkey(),
create_config.lookup_tables.clone().unwrap_or_default(),
Some(&all_signers),
)
.await?;
let compute_units: u64 = units.ok_or(HeliusError::InvalidInput(
"Error fetching compute units for the instructions provided".to_string(),
))?;
let multiplier: f32 = create_config
.cu_buffer_multiplier
.unwrap_or(CU_BUFFER_MULTIPLIER_DEFAULT);
let customers_cu: u32 = if compute_units < 1000 {
1000
} else {
(compute_units as f64 * multiplier as f64).ceil() as u32
};
final_instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(customers_cu));
final_instructions.extend(create_config.instructions.clone());
let transaction: SmartTransaction = if let Some(lookup_tables) = &create_config.lookup_tables {
let message: v0::Message = v0::Message::try_compile(
&fee_payer.pubkey(),
&final_instructions,
lookup_tables,
recent_blockhash,
)?;
let versioned_message: VersionedMessage = VersionedMessage::V0(message);
let tx: VersionedTransaction = VersionedTransaction::try_new(versioned_message, all_signers.as_slice())
.map_err(|e| HeliusError::InvalidInput(format!("Signing error: {:?}", e)))?;
SmartTransaction::Versioned(tx)
} else {
let mut tx: Transaction = Transaction::new_with_payer(&final_instructions, Some(&fee_payer.pubkey()));
tx.sign(&all_signers, recent_blockhash);
SmartTransaction::Legacy(tx)
};
Ok((transaction, last_valid_block_hash))
}
pub async fn send_smart_transaction_with_seeds(
&self,
create_config: CreateSmartTransactionSeedConfig,
send_options: Option<RpcSendTransactionConfig>,
timeout: Option<Timeout>,
) -> Result<Signature> {
if create_config.signer_seeds.is_empty() {
return Err(HeliusError::InvalidInput(
"At least one signer seed required".to_string(),
));
}
let (transaction, last_valid_block_hash) = self.create_smart_transaction_with_seeds(&create_config).await?;
match transaction {
SmartTransaction::Legacy(tx) => {
self.send_and_confirm_transaction(
&tx,
send_options.unwrap_or_default(),
last_valid_block_hash,
Some(timeout.unwrap_or_default().into()),
)
.await
}
SmartTransaction::Versioned(tx) => {
self.send_and_confirm_transaction(
&tx,
send_options.unwrap_or_default(),
last_valid_block_hash,
Some(timeout.unwrap_or_default().into()),
)
.await
}
}
}
pub async fn create_smart_transaction_without_signers(
&self,
config: &CreateSmartTransactionConfig,
) -> Result<(SmartTransaction, u64)> {
let fee_payer: &Arc<dyn Signer> = config.fee_payer.as_ref().ok_or_else(|| {
HeliusError::InvalidInput("Fee payer must be provided for unsigned transactions".to_string())
})?;
let payer_pubkey: Pubkey = fee_payer.pubkey();
let (recent_blockhash, last_valid_block_hash) = self
.connection()
.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())?;
let mut final_instructions: Vec<Instruction> = vec![];
let existing_compute_budget_instructions: bool = config.instructions.iter().any(|instruction| {
instruction.program_id == ComputeBudgetInstruction::set_compute_unit_limit(0).program_id
|| instruction.program_id == ComputeBudgetInstruction::set_compute_unit_price(0).program_id
});
if existing_compute_budget_instructions {
return Err(HeliusError::InvalidInput(
"Cannot provide instructions that set the compute unit price and/or limit".to_string(),
));
}
let is_versioned: bool = config.lookup_tables.is_some();
let preflight_bytes: Vec<u8> = Self::build_unsigned_preflight_tx(
&payer_pubkey,
&config.instructions,
config.lookup_tables.as_deref(),
recent_blockhash,
)?;
let transaction_base58: String = encode(&preflight_bytes).into_string();
let priority_fee_request = GetPriorityFeeEstimateRequest {
transaction: Some(transaction_base58),
account_keys: None,
options: Some(GetPriorityFeeEstimateOptions {
recommended: Some(true),
..Default::default()
}),
};
let priority_fee_estimate: GetPriorityFeeEstimateResponse =
self.rpc().get_priority_fee_estimate(priority_fee_request).await?;
let priority_fee_recommendation: u64 = priority_fee_estimate
.priority_fee_estimate
.ok_or_else(|| HeliusError::InvalidInput("Priority fee estimate not available".to_string()))?
as u64;
let priority_fee: u64 = if let Some(provided_fee) = config.priority_fee_cap {
std::cmp::min(priority_fee_recommendation, provided_fee)
} else {
priority_fee_recommendation
};
let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_price(priority_fee);
final_instructions.push(compute_budget_ix);
let units: Option<u64> = self
.get_compute_units(
config.instructions.clone(),
payer_pubkey,
config.lookup_tables.clone().unwrap_or_default(),
None,
)
.await?;
if units.is_none() {
return Err(HeliusError::InvalidInput(
"Error fetching compute units for the provided instructions".to_string(),
));
}
let compute_units: u64 = units.ok_or(HeliusError::InvalidInput(
"Error fetching compute units for the instructions provided".to_string(),
))?;
let multiplier: f32 = config.cu_buffer_multiplier.unwrap_or(CU_BUFFER_MULTIPLIER_DEFAULT);
let customers_cu: u32 = if compute_units < 1000 {
1000
} else {
(compute_units as f64 * multiplier as f64).ceil() as u32
};
let compute_units_ix = ComputeBudgetInstruction::set_compute_unit_limit(customers_cu);
final_instructions.push(compute_units_ix);
final_instructions.extend(config.instructions.clone());
if is_versioned {
let lookup_tables: &[AddressLookupTableAccount] = config.lookup_tables.as_deref().unwrap();
let v0_message: v0::Message =
v0::Message::try_compile(&payer_pubkey, &final_instructions, lookup_tables, recent_blockhash)?;
let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message);
let versioned_transaction: VersionedTransaction = VersionedTransaction {
signatures: vec![],
message: versioned_message,
};
Ok((
SmartTransaction::Versioned(versioned_transaction),
last_valid_block_hash,
))
} else {
let mut tx: Transaction = Transaction::new_with_payer(&final_instructions, Some(&payer_pubkey));
tx.message.recent_blockhash = recent_blockhash;
Ok((SmartTransaction::Legacy(tx), last_valid_block_hash))
}
}
pub async fn fetch_tip_floor_75th(&self) -> Result<Option<u64>> {
let res = reqwest::Client::new()
.get(TIP_FLOOR_URL)
.header("User-Agent", SDK_USER_AGENT)
.send()
.await
.map_err(|e| HeliusError::InvalidInput(format!("Tip floor fetch error: {e}")))?;
if !res.status().is_success() {
return Ok(None);
}
let json: serde_json::Value = res
.json()
.await
.map_err(|e| HeliusError::InvalidInput(format!("Tip floor JSON parse error: {e}")))?;
let val_sol = json
.get(0)
.and_then(|o| o.get("landed_tips_75th_percentile"))
.and_then(|v| v.as_f64());
Ok(val_sol.map(|sol| (sol * 1_000_000_000.0) as u64))
}
pub async fn determine_tip_lamports(&self, swqos_only: bool) -> Result<u64> {
let min_lamports: u64 = if swqos_only {
MIN_TIP_LAMPORTS_SWQOS
} else {
MIN_TIP_LAMPORTS_DUAL
};
let floor_lamports: u64 = self.fetch_tip_floor_75th().await?.unwrap_or(min_lamports);
Ok(floor_lamports.max(min_lamports))
}
pub async fn create_smart_transaction_with_tip_for_sender(
&self,
mut config: CreateSmartTransactionConfig,
tip_amount: u64,
) -> Result<(SmartTransaction, u64)> {
if config.signers.is_empty() {
return Err(HeliusError::InvalidInput(
"The fee payer must sign the transaction".to_string(),
));
}
let payer_pubkey: Pubkey = config
.fee_payer
.as_ref()
.map_or(config.signers[0].pubkey(), |signer| signer.pubkey());
if tip_amount > 0 {
let mut rng = rand::rng();
let idx = rng.random_range(0..SENDER_TIP_ACCOUNTS.len());
let tip_pubkey = Pubkey::from_str(SENDER_TIP_ACCOUNTS[idx])
.map_err(|e| HeliusError::InvalidInput(format!("Invalid tip account: {e}")))?;
let tip_ix = system_instruction::transfer(&payer_pubkey, &tip_pubkey, tip_amount);
config.instructions.push(tip_ix);
}
self.create_smart_transaction(&config).await
}
pub async fn warm_sender_connection(&self, region: &str) -> Result<()> {
let url = sender_ping_url(region);
let res = reqwest::Client::new()
.get(&url)
.header("User-Agent", SDK_USER_AGENT)
.send()
.await
.map_err(|e| HeliusError::InvalidInput(format!("Sender ping error: {e}")))?;
if !res.status().is_success() {
return Err(HeliusError::InvalidInput(format!("Sender ping HTTP {}", res.status())));
}
Ok(())
}
pub async fn send_and_confirm_via_sender<T>(
&self,
transaction: &T,
last_valid_block_height: u64,
opts: SenderSendOptions,
) -> Result<Signature>
where
T: SerializableTransaction + serde::Serialize + ?Sized,
{
let wire: Vec<u8> =
bincode::serialize(transaction).map_err(|e: Box<ErrorKind>| HeliusError::InvalidInput(e.to_string()))?;
let tx64: String = B64.encode(&wire);
let sig: Signature = post_to_sender(&tx64, &opts).await?;
let start: Instant = Instant::now();
let timeout: Duration = Duration::from_millis(opts.poll_timeout_ms);
let interval: Duration = Duration::from_millis(opts.poll_interval_ms);
loop {
if start.elapsed() >= timeout {
return Err(HeliusError::Timeout {
code: StatusCode::REQUEST_TIMEOUT,
text: format!("Transaction {}'s confirmation timed out", sig),
});
}
if self.connection().get_block_height()? > last_valid_block_height {
return Err(HeliusError::Timeout {
code: StatusCode::REQUEST_TIMEOUT,
text: format!(
"Transaction {} expired (last_valid_block_height={})",
sig, last_valid_block_height
),
});
}
match self.poll_transaction_confirmation(sig).await {
Ok(confirmed) => return Ok(confirmed),
Err(err) if is_retryable_confirmation_error(&err) => sleep(interval).await,
Err(err) => return Err(err),
}
}
}
pub async fn send_smart_transaction_with_sender(
&self,
config: SmartTransactionConfig,
sender_opts: SenderSendOptions,
) -> Result<Signature> {
if sender_opts.region.trim().is_empty() {
return Err(HeliusError::InvalidInput("Sender region must be specified".to_string()));
}
let mut tip_lamports = self.determine_tip_lamports(sender_opts.swqos_only).await?;
let floor = if sender_opts.swqos_only {
MIN_TIP_LAMPORTS_SWQOS
} else {
MIN_TIP_LAMPORTS_DUAL
};
if tip_lamports < floor {
tip_lamports = floor;
}
let create_cfg: CreateSmartTransactionConfig = config.create_config;
let (transaction, last_valid_block_height) = self
.create_smart_transaction_with_tip_for_sender(create_cfg, tip_lamports)
.await?;
match transaction {
SmartTransaction::Legacy(tx) => {
self.send_and_confirm_via_sender(&tx, last_valid_block_height, sender_opts)
.await
}
SmartTransaction::Versioned(tx) => {
self.send_and_confirm_via_sender(&tx, last_valid_block_height, sender_opts)
.await
}
}
}
}
#[cfg(test)]
mod tests {
use super::{collect_unique_keypair_refs, collect_unique_signers, is_retryable_confirmation_error};
use crate::error::HeliusError;
use reqwest::StatusCode;
use solana_sdk::{
hash::Hash,
instruction::{AccountMeta, Instruction, InstructionError},
message::{v0, VersionedMessage},
pubkey::Pubkey,
signature::{Keypair, Signature, Signer},
transaction::{Transaction, TransactionError, VersionedTransaction},
};
use std::sync::Arc;
fn build_versioned_message(
payer: &Keypair,
writable_signer: &Keypair,
readonly_signer: &Keypair,
) -> VersionedMessage {
let instruction = Instruction {
program_id: Pubkey::new_unique(),
accounts: vec![
AccountMeta::new(writable_signer.pubkey(), true),
AccountMeta::new_readonly(readonly_signer.pubkey(), true),
],
data: vec![],
};
VersionedMessage::V0(
v0::Message::try_compile(&payer.pubkey(), &[instruction], &[], Hash::new_unique()).unwrap(),
)
}
#[test]
fn collect_unique_signers_includes_fee_payer_once() {
let fee_payer: Arc<dyn Signer> = Arc::new(Keypair::new());
let signer: Arc<dyn Signer> = Arc::new(Keypair::new());
let signers: Vec<Arc<dyn Signer>> = vec![signer.clone(), fee_payer.clone(), signer.clone()];
let all_signers = collect_unique_signers(&signers, Some(&fee_payer));
let signer_pubkeys: Vec<Pubkey> = all_signers.iter().map(|signer| signer.pubkey()).collect();
assert_eq!(signer_pubkeys, vec![fee_payer.pubkey(), signer.pubkey()]);
}
#[test]
fn collect_unique_keypair_refs_includes_fee_payer_once() {
let fee_payer = Keypair::new();
let signer = Keypair::new();
let signers = vec![
signer.insecure_clone(),
fee_payer.insecure_clone(),
signer.insecure_clone(),
];
let all_signers = collect_unique_keypair_refs(&signers, &fee_payer);
let signer_pubkeys: Vec<Pubkey> = all_signers.iter().map(|signer| signer.pubkey()).collect();
assert_eq!(signer_pubkeys, vec![fee_payer.pubkey(), signer.pubkey()]);
}
#[test]
fn versioned_try_new_reorders_arc_signers_to_match_message() {
let fee_payer = Keypair::new();
let writable_signer = Keypair::new();
let readonly_signer = Keypair::new();
let fee_payer_signer: Arc<dyn Signer> = Arc::new(fee_payer.insecure_clone());
let writable_signer_arc: Arc<dyn Signer> = Arc::new(writable_signer.insecure_clone());
let readonly_signer_arc: Arc<dyn Signer> = Arc::new(readonly_signer.insecure_clone());
let message = build_versioned_message(&fee_payer, &writable_signer, &readonly_signer);
let signers: Vec<Arc<dyn Signer>> = vec![readonly_signer_arc.clone(), writable_signer_arc.clone()];
let all_signers = collect_unique_signers(&signers, Some(&fee_payer_signer));
let tx = VersionedTransaction::try_new(message.clone(), all_signers.as_slice()).unwrap();
let message_bytes = message.serialize();
assert_eq!(
tx.signatures,
vec![
Signature::from(fee_payer.sign_message(&message_bytes)),
Signature::from(writable_signer.sign_message(&message_bytes)),
Signature::from(readonly_signer.sign_message(&message_bytes)),
]
);
}
#[test]
fn manual_fee_payer_appended_signature_order_fails_verification() {
let fee_payer = Keypair::new();
let writable_signer = Keypair::new();
let readonly_signer = Keypair::new();
let message = build_versioned_message(&fee_payer, &writable_signer, &readonly_signer);
let message_bytes = message.serialize();
let manual_signatures = [
readonly_signer.sign_message(&message_bytes),
writable_signer.sign_message(&message_bytes),
fee_payer.sign_message(&message_bytes),
];
let verification_results: Vec<bool> = manual_signatures
.iter()
.zip(message.static_account_keys().iter())
.map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &message_bytes))
.collect();
assert_eq!(verification_results, vec![false, true, false]);
}
#[test]
fn manual_non_payer_caller_order_can_fail_verification() {
let fee_payer = Keypair::new();
let writable_signer = Keypair::new();
let readonly_signer = Keypair::new();
let message = build_versioned_message(&fee_payer, &writable_signer, &readonly_signer);
let message_bytes = message.serialize();
let manual_signatures = [
fee_payer.sign_message(&message_bytes),
readonly_signer.sign_message(&message_bytes),
writable_signer.sign_message(&message_bytes),
];
let verification_results: Vec<bool> = manual_signatures
.iter()
.zip(message.static_account_keys().iter())
.map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &message_bytes))
.collect();
assert_eq!(verification_results, vec![true, false, false]);
}
#[test]
fn versioned_try_new_reorders_keypair_signers_to_match_message() {
let fee_payer = Keypair::new();
let writable_signer = Keypair::new();
let readonly_signer = Keypair::new();
let message = build_versioned_message(&fee_payer, &writable_signer, &readonly_signer);
let signers = vec![readonly_signer.insecure_clone(), writable_signer.insecure_clone()];
let all_signers = collect_unique_keypair_refs(&signers, &fee_payer);
let tx = VersionedTransaction::try_new(message.clone(), all_signers.as_slice()).unwrap();
let message_bytes = message.serialize();
assert_eq!(
tx.signatures,
vec![
Signature::from(fee_payer.sign_message(&message_bytes)),
Signature::from(writable_signer.sign_message(&message_bytes)),
Signature::from(readonly_signer.sign_message(&message_bytes)),
]
);
}
#[test]
fn legacy_try_partial_sign_reorders_keypairs_to_match_message() {
let fee_payer = Keypair::new();
let writable_signer = Keypair::new();
let readonly_signer = Keypair::new();
let recent_blockhash = Hash::new_unique();
let instruction = Instruction {
program_id: Pubkey::new_unique(),
accounts: vec![
AccountMeta::new(writable_signer.pubkey(), true),
AccountMeta::new_readonly(readonly_signer.pubkey(), true),
],
data: vec![],
};
let mut tx = Transaction::new_with_payer(&[instruction], Some(&fee_payer.pubkey()));
let signers = vec![readonly_signer.insecure_clone(), writable_signer.insecure_clone()];
let all_signers = collect_unique_keypair_refs(&signers, &fee_payer);
tx.try_partial_sign(&all_signers, recent_blockhash).unwrap();
let message_bytes = tx.message_data();
assert_eq!(
tx.signatures,
vec![
fee_payer.sign_message(&message_bytes),
writable_signer.sign_message(&message_bytes),
readonly_signer.sign_message(&message_bytes),
]
);
}
#[test]
fn confirmation_retries_only_on_timeout() {
let timeout = HeliusError::Timeout {
code: StatusCode::REQUEST_TIMEOUT,
text: "pending".to_string(),
};
let tx_error =
HeliusError::TransactionError(TransactionError::InstructionError(0, InstructionError::Custom(1)));
let invalid_input = HeliusError::InvalidInput("bad config".to_string());
assert!(is_retryable_confirmation_error(&timeout));
assert!(!is_retryable_confirmation_error(&tx_error));
assert!(!is_retryable_confirmation_error(&invalid_input));
}
}