diff --git a/Cargo.toml b/Cargo.toml index 4568125..58cabf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helius" -version = "0.1.4" +version = "0.1.5" edition = "2021" description = "An asynchronous Helius Rust SDK for building the future of Solana" keywords = ["helius", "solana", "asynchronous-sdk", "das", "cryptocurrency"] diff --git a/src/optimized_transaction.rs b/src/optimized_transaction.rs index b384993..ba53156 100644 --- a/src/optimized_transaction.rs +++ b/src/optimized_transaction.rs @@ -5,14 +5,13 @@ use crate::types::{ }; use crate::Helius; -use base64::engine::general_purpose::STANDARD; -use base64::Engine; use bincode::{serialize, ErrorKind}; use reqwest::StatusCode; use solana_client::rpc_config::{RpcSendTransactionConfig, RpcSimulateTransactionConfig}; use solana_client::rpc_response::{Response, RpcSimulateTransactionResult}; use solana_sdk::{ address_lookup_table::AddressLookupTableAccount, + bs58::encode, commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, hash::Hash, @@ -121,23 +120,58 @@ impl Helius { /// The transaction signature, if successful pub async fn send_smart_transaction(&self, config: SmartTransactionConfig<'_>) -> Result { let pubkey: Pubkey = config.from_keypair.pubkey(); - let mut recent_blockhash: Hash = self.connection().get_latest_blockhash()?; + let recent_blockhash: Hash = self.connection().get_latest_blockhash()?; let mut final_instructions: Vec = vec![]; + // Check if any of the instructions provided set the compute unit price and/or limit, and throw an error if `true` + 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(), + )); + } + + // Get the optimal compute units + let units: Option = self + .get_compute_units( + config.instructions.clone(), + pubkey, + config.lookup_tables.clone().unwrap_or_default(), + &config.from_keypair, + ) + .await?; + + if units.is_none() { + return Err(HeliusError::InvalidInput( + "Error fetching compute units for the instructions provided".to_string(), + )); + } + + let compute_units: u64 = units.unwrap(); + let customers_cu: u32 = if compute_units < 1000 { + 1000 + } else { + (compute_units as f64 * 1.5).ceil() as u32 + }; + + // Add the compute unit limit instruction with a margin + let compute_units_ix: Instruction = ComputeBudgetInstruction::set_compute_unit_limit(customers_cu); + final_instructions.push(compute_units_ix); + // Determine if we need to use a versioned transaction - let is_versioned: bool = config.lookup_tables.is_some(); + let is_versioned: bool = false; //config.lookup_tables.is_some(); let mut legacy_transaction: Option = None; let mut versioned_transaction: Option = None; // Build the initial transaction based on whether lookup tables are present if is_versioned { - // If lookup tables are present, we build a versioned transaction let lookup_tables: &[AddressLookupTableAccount] = config.lookup_tables.as_deref().unwrap_or_default(); - let mut instructions: Vec = vec![ComputeBudgetInstruction::set_compute_unit_price(1)]; - instructions.extend(config.instructions.clone()); - let v0_message: v0::Message = - v0::Message::try_compile(&pubkey, &instructions, lookup_tables, recent_blockhash)?; + v0::Message::try_compile(&pubkey, &config.instructions, lookup_tables, recent_blockhash)?; let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); // Sign the versioned transaction @@ -158,83 +192,52 @@ impl Helius { legacy_transaction = Some(tx); } - let priority_fee: u64 = if let Some(tx) = &legacy_transaction { - // Serialize the transaction - let serialized_tx: Vec = - serialize(&tx).map_err(|e: Box| HeliusError::InvalidInput(e.to_string()))?; - let transaction_base64: String = STANDARD.encode(&serialized_tx); - - // Get the priority fee estimate based on the serialized transaction - let priority_fee_request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest { - transaction: Some(transaction_base64), - 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_f64: f64 = - priority_fee_estimate - .priority_fee_estimate - .ok_or(HeliusError::InvalidInput( - "Priority fee estimate not available".to_string(), - ))?; - - priority_fee_f64 as u64 + // Serialize the transaction + let serialized_tx: Vec = if let Some(tx) = &legacy_transaction { + serialize(&tx).map_err(|e: Box| HeliusError::InvalidInput(e.to_string()))? } else if let Some(tx) = &versioned_transaction { - // Serialize the transaction - let serialized_tx: Vec = - serialize(&tx).map_err(|e: Box| HeliusError::InvalidInput(e.to_string()))?; - let transaction_base64: String = STANDARD.encode(&serialized_tx); - - // Get the priority fee estimate based on the serialized transaction - let priority_fee_request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest { - transaction: Some(transaction_base64), - 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_f64: f64 = - priority_fee_estimate - .priority_fee_estimate - .ok_or(HeliusError::InvalidInput( - "Priority fee estimate not available".to_string(), - ))?; - priority_fee_f64 as u64 + serialize(&tx).map_err(|e: Box| HeliusError::InvalidInput(e.to_string()))? } else { return Err(HeliusError::InvalidInput("No transaction available".to_string())); }; + // Encode the transaction + let transaction_base58: String = encode(&serialized_tx).into_string(); + + // Get the priority fee estimate based on the serialized transaction + let priority_fee_request: GetPriorityFeeEstimateRequest = 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(HeliusError::InvalidInput( + "Priority fee estimate not available".to_string(), + ))? as u64; + + let lamports_to_micro_lamports: u64 = 10_u64.pow(6); + let minimum_total_pfee_lamports: u64 = 10_000; + let microlamports_per_cu: u64 = std::cmp::max( + priority_fee_recommendation, + ((minimum_total_pfee_lamports as f64 / customers_cu as f64) * lamports_to_micro_lamports as f64).round() + as u64, + ); + // Add the compute unit price instruction with the estimated fee - let compute_budget_ix: Instruction = ComputeBudgetInstruction::set_compute_unit_price(priority_fee); + let compute_budget_ix: Instruction = ComputeBudgetInstruction::set_compute_unit_price(microlamports_per_cu); final_instructions.push(compute_budget_ix); - final_instructions.extend(config.instructions.clone()); - // Get the optimal compute units - if let Some(units) = self - .get_compute_units( - final_instructions.clone(), - pubkey, - config.lookup_tables.clone().unwrap_or_default(), - &config.from_keypair, - ) - .await? - { - // Add some margin to the compute units to ensure the transaction does not fail - let compute_units_ix: Instruction = - ComputeBudgetInstruction::set_compute_unit_limit((units as f64 * 1.1).ceil() as u32); - final_instructions.insert(0, compute_units_ix); - } + // Add the original instructions back + final_instructions.extend(config.instructions.clone()); // Rebuild the transaction with the final instructions if is_versioned { @@ -253,21 +256,20 @@ impl Helius { message: versioned_message, }); } else { - let mut tx = Transaction::new_with_payer(&final_instructions, Some(&pubkey)); + let mut tx: Transaction = Transaction::new_with_payer(&final_instructions, Some(&pubkey)); tx.try_sign(&[config.from_keypair], recent_blockhash)?; legacy_transaction = Some(tx); } - // Re-fetch interval of 60 seconds - let blockhash_refetch_interval: Duration = Duration::from_secs(60); - let mut last_blockhash_refetch: Instant = Instant::now(); - + // Common logic for sending transactions let send_transaction_config: RpcSendTransactionConfig = RpcSendTransactionConfig { - skip_preflight: config.skip_preflight_checks.unwrap_or(true), - ..Default::default() + skip_preflight: config.send_options.skip_preflight, + preflight_commitment: config.send_options.preflight_commitment, + encoding: config.send_options.encoding, + max_retries: config.send_options.max_retries, + min_context_slot: config.send_options.min_context_slot, }; - // Common logic for sending transactions let send_result = |transaction: &Transaction| { self.connection() .send_transaction_with_config(transaction, send_transaction_config) @@ -277,35 +279,11 @@ impl Helius { .send_transaction_with_config(transaction, send_transaction_config) }; - // Send the transaction with retries and preflight checks - let mut retry_count: usize = 0; - let max_retries: usize = config.max_retries.unwrap_or(6); - - while retry_count <= max_retries { - if last_blockhash_refetch.elapsed() >= blockhash_refetch_interval { - recent_blockhash = self.connection().get_latest_blockhash()?; - if is_versioned { - let signers: Vec<&dyn Signer> = vec![config.from_keypair]; - let signed_message: Vec = signers - .iter() - .map(|signer| { - signer.try_sign_message( - versioned_transaction.as_ref().unwrap().message.serialize().as_slice(), - ) - }) - .collect::, _>>()?; - versioned_transaction.as_mut().unwrap().signatures = signed_message; - } else { - legacy_transaction - .as_mut() - .unwrap() - .try_sign(&[config.from_keypair], recent_blockhash)?; - } - - // Update the last refetch time - last_blockhash_refetch = Instant::now(); - } + // Retry logic with a timeout of 60 seconds + let timeout: Duration = Duration::from_secs(60); + let start_time: Instant = Instant::now(); + while Instant::now().duration_since(start_time) < timeout { let result = if is_versioned { send_versioned_result(versioned_transaction.as_ref().unwrap()) } else { @@ -314,19 +292,13 @@ impl Helius { match result { Ok(signature) => return self.poll_transaction_confirmation(signature).await, - Err(error) => { - retry_count += 1; - if retry_count > max_retries { - return Err(HeliusError::ClientError(error)); - } - continue; - } + Err(_) => continue, } } Err(HeliusError::Timeout { code: StatusCode::REQUEST_TIMEOUT, - text: "Reached an unexpected point in send_smart_transaction".to_string(), + text: "Transaction failed to confirm in 60s".to_string(), }) } } diff --git a/src/types/options.rs b/src/types/options.rs index f8511dc..9384afc 100644 --- a/src/types/options.rs +++ b/src/types/options.rs @@ -47,4 +47,6 @@ pub struct SearchAssetsOptions { pub show_zero_balance: bool, #[serde(default)] pub show_closed_accounts: bool, + #[serde(default)] + pub show_native_balance: bool, } diff --git a/src/types/types.rs b/src/types/types.rs index 8ecb7a6..abb6925 100644 --- a/src/types/types.rs +++ b/src/types/types.rs @@ -8,6 +8,7 @@ use crate::types::{DisplayOptions, GetAssetOptions}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use solana_client::rpc_config::RpcSendTransactionConfig; use solana_sdk::{address_lookup_table::AddressLookupTableAccount, instruction::Instruction, signature::Keypair}; /// Defines the available clusters supported by Helius @@ -572,8 +573,8 @@ pub struct FileQuality { pub struct Metadata { pub attributes: Option>, pub description: Option, - pub name: String, - pub symbol: String, + pub name: Option, + pub symbol: Option, } #[derive(Serialize, Deserialize, Debug)] @@ -946,8 +947,7 @@ pub struct EditWebhookRequest { pub struct SmartTransactionConfig<'a> { pub instructions: Vec, pub from_keypair: &'a Keypair, - pub skip_preflight_checks: Option, - pub max_retries: Option, + pub send_options: RpcSendTransactionConfig, pub lookup_tables: Option>, } @@ -956,8 +956,7 @@ impl<'a> SmartTransactionConfig<'a> { Self { instructions, from_keypair, - skip_preflight_checks: None, - max_retries: None, + send_options: RpcSendTransactionConfig::default(), lookup_tables: None, } } diff --git a/tests/rpc/test_get_asset.rs b/tests/rpc/test_get_asset.rs index a5655f6..8ac82d5 100644 --- a/tests/rpc/test_get_asset.rs +++ b/tests/rpc/test_get_asset.rs @@ -61,8 +61,8 @@ async fn test_get_asset_success() { description: Some( "Apt323 the 36 page Collectors Edition.".to_string(), ), - name: "Apt323 Collectors Edition #72".to_string(), - symbol: "".to_string(), + name: Some("Apt323 Collectors Edition #72".to_string()), + symbol: Some("".to_string()), }, links: Some( Links { diff --git a/tests/rpc/test_get_asset_batch.rs b/tests/rpc/test_get_asset_batch.rs index 902f50b..98296e4 100644 --- a/tests/rpc/test_get_asset_batch.rs +++ b/tests/rpc/test_get_asset_batch.rs @@ -47,8 +47,8 @@ async fn test_get_asset_batch_success() { } ]), description: Some("A hotspot NFT on Helium".to_string()), - name: "gentle-mandarin-ferret".to_string(), - symbol: "HOTSPOT".to_string(), + name: Some("gentle-mandarin-ferret".to_string()), + symbol: Some("HOTSPOT".to_string()), }, links: Some(Links { external_url: None, @@ -156,8 +156,8 @@ async fn test_get_asset_batch_success() { } ]), description: Some("Aerial photograph of a parking structure in LA depicting the photographer, Andrew Mason, \"sliding\" through the image.".to_string()), - name: "Slide".to_string(), - symbol: "".to_string(), + name: Some("Slide".to_string()), + symbol: Some("".to_string()), }, links: Some(Links { external_url: Some("".to_string()), diff --git a/tests/rpc/test_get_assets_by_authority.rs b/tests/rpc/test_get_assets_by_authority.rs index b699795..881c493 100644 --- a/tests/rpc/test_get_assets_by_authority.rs +++ b/tests/rpc/test_get_assets_by_authority.rs @@ -100,8 +100,8 @@ async fn test_get_assets_by_authority_success() { description: Some( "Fock it.".to_string(), ), - name: "Mad Lads #6867".to_string(), - symbol: "MAD".to_string(), + name: Some("Mad Lads #6867".to_string()), + symbol: Some("MAD".to_string()), }, links: Some( Links { diff --git a/tests/rpc/test_get_assets_by_creator.rs b/tests/rpc/test_get_assets_by_creator.rs index 4fed6f7..2d46427 100644 --- a/tests/rpc/test_get_assets_by_creator.rs +++ b/tests/rpc/test_get_assets_by_creator.rs @@ -100,8 +100,8 @@ async fn test_get_assets_by_creator_success() { description: Some( "Fock it.".to_string(), ), - name: "Mad Lads #6867".to_string(), - symbol: "MAD".to_string(), + name: Some("Mad Lads #6867".to_string()), + symbol: Some("MAD".to_string()), }, links: Some( Links { diff --git a/tests/rpc/test_get_assets_by_group.rs b/tests/rpc/test_get_assets_by_group.rs index d00522e..255baed 100644 --- a/tests/rpc/test_get_assets_by_group.rs +++ b/tests/rpc/test_get_assets_by_group.rs @@ -51,8 +51,8 @@ async fn test_get_assets_by_group_success() { trait_type: "Affiliation".to_string(), }]), description: Some("Obi-Wan Kenobi was a legendary Force-sensitive human male Jedi Master who served on the Jedi High Council during the final years of the Republic Era".to_string()), - name: "Obi-Wan Kenobi".to_string(), - symbol:"Guiding Light".to_string(), + name: Some("Obi-Wan Kenobi".to_string()), + symbol: Some("Guiding Light".to_string()), }, links: Some(Links { external_url: Some("https://example.com".to_string()), @@ -169,7 +169,10 @@ async fn test_get_assets_by_group_success() { let asset: AssetList = response.unwrap(); assert_eq!(asset.total, 1); - assert_eq!(asset.items[0].content.as_ref().unwrap().metadata.name, "Obi-Wan Kenobi"); + assert_eq!( + asset.items[0].content.as_ref().unwrap().metadata.name, + Some("Obi-Wan Kenobi".to_string()) + ); } #[tokio::test] diff --git a/tests/rpc/test_get_assets_by_owner.rs b/tests/rpc/test_get_assets_by_owner.rs index 94d0fc6..f5c5987 100644 --- a/tests/rpc/test_get_assets_by_owner.rs +++ b/tests/rpc/test_get_assets_by_owner.rs @@ -67,8 +67,8 @@ async fn test_get_assets_by_owner_success() { "Visit the domain shown in the picture and claim your exclusive voucher 3000jup.com" .to_string(), ), - name: "3000Jup For You 3000Jup.com".to_string(), - symbol: "JFY".to_string(), + name: Some("3000Jup For You 3000Jup.com".to_string()), + symbol: Some("JFY".to_string()), }, links: Some(Links { external_url: Some("https://3000jup.com".to_string()), diff --git a/tests/rpc/test_search_assets.rs b/tests/rpc/test_search_assets.rs index 2863dfc..a9e69e2 100644 --- a/tests/rpc/test_search_assets.rs +++ b/tests/rpc/test_search_assets.rs @@ -67,8 +67,8 @@ async fn test_search_assets_success() { "Visit the domain shown in the picture and claim your exclusive voucher 3000jup.com" .to_string(), ), - name: "3000Jup For You 3000Jup.com".to_string(), - symbol: "JFY".to_string(), + name: Some("3000Jup For You 3000Jup.com".to_string()), + symbol: Some("JFY".to_string()), }, links: Some(Links { external_url: Some("https://3000jup.com".to_string()),