diff --git a/examples/get_parsed_transaction_history.rs b/examples/get_parsed_transaction_history.rs index 57d979b..77dec00 100644 --- a/examples/get_parsed_transaction_history.rs +++ b/examples/get_parsed_transaction_history.rs @@ -12,6 +12,11 @@ async fn main() -> Result<()> { let request: ParsedTransactionHistoryRequest = ParsedTransactionHistoryRequest { address: "2k5AXX4guW9XwRQ1AKCpAuUqgWDpQpwFfpVFh3hnm2Ha".to_string(), before: None, + until: None, + transaction_type: None, + commitment: None, + limit: None, + source: None, }; let response: Result> = helius.parsed_transaction_history(request).await; diff --git a/examples/send_smart_transaction_with_seeds.rs b/examples/send_smart_transaction_with_seeds.rs new file mode 100644 index 0000000..83d7eb8 --- /dev/null +++ b/examples/send_smart_transaction_with_seeds.rs @@ -0,0 +1,103 @@ +use helius::types::*; +use helius::Helius; +use solana_client::rpc_config::RpcSendTransactionConfig; +use solana_sdk::signature::Keypair; +use solana_sdk::signer::Signer; +use solana_sdk::{bs58, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, system_instruction}; +use std::{str::FromStr, time::Duration}; +use tokio::time::sleep; + +#[tokio::main] +async fn main() { + tokio::spawn(async { + let api_key: &str = "your_api_key"; + let cluster: Cluster = Cluster::MainnetBeta; + let helius: Helius = Helius::new(api_key, cluster).unwrap(); + + // Convert your base58 private key to a seed + let keypair_base58: &str = "your_keypair_as_base58"; + let keypair_bytes: Vec = bs58::decode(keypair_base58).into_vec().unwrap(); + + // Create the recipient address + let to_pubkey: Pubkey = Pubkey::from_str("recipient_address").unwrap(); + + // Get the sender's public key for balance checking + let from_pubkey: Pubkey = Keypair::from_bytes(&keypair_bytes).unwrap().pubkey(); + + println!("From wallet address: {}", from_pubkey); + println!("To wallet address: {}", to_pubkey); + + // Get initial balances + let balance_from: u64 = helius.connection().get_balance(&from_pubkey).unwrap_or(0); + let balance_to: u64 = helius.connection().get_balance(&to_pubkey).unwrap_or(0); + + println!( + "From wallet balance: {} SOL", + balance_from as f64 / LAMPORTS_PER_SOL as f64 + ); + println!("To wallet balance: {} SOL", balance_to as f64 / LAMPORTS_PER_SOL as f64); + + // Create the transfer instruction + let transfer_amount: u64 = (0.01 * LAMPORTS_PER_SOL as f64) as u64; + let instruction: solana_sdk::instruction::Instruction = + system_instruction::transfer(&from_pubkey, &to_pubkey, transfer_amount); + + // Convert keypair bytes to a 32-byte seed array + let mut seed: [u8; 32] = [0u8; 32]; + seed.copy_from_slice(&keypair_bytes[..32]); + + // For testing purposes. In a production setting, you'd actually create or pass in an existing ATL + let address_lut: Vec = vec![]; + + // Configure the smart transaction + let config: CreateSmartTransactionSeedConfig = CreateSmartTransactionSeedConfig { + instructions: vec![instruction], + signer_seeds: vec![seed], + fee_payer_seed: None, + lookup_tables: Some(address_lut), + priority_fee_cap: Some(100000), + }; + + // Configure send options (optional) + let send_options: Option = Some(RpcSendTransactionConfig { + skip_preflight: true, + preflight_commitment: None, + encoding: None, + max_retries: None, + min_context_slot: None, + }); + + // Set a timeout (optional) + let timeout: Option = Some(Timeout { + duration: Duration::from_secs(60), + }); + + // Send the transaction + match helius + .send_smart_transaction_with_seeds(config, send_options, timeout) + .await + { + Ok(signature) => { + println!("Transaction sent successfully: {}", signature); + sleep(Duration::from_secs(5)).await; + + // Get final balances + let balance_from: u64 = helius.connection().get_balance(&from_pubkey).unwrap_or(0); + println!( + "Final From Wallet Balance: {} SOL", + balance_from as f64 / LAMPORTS_PER_SOL as f64 + ); + let balance_to: u64 = helius.connection().get_balance(&to_pubkey).unwrap_or(0); + println!( + "Final To Wallet Balance: {} SOL", + balance_to as f64 / LAMPORTS_PER_SOL as f64 + ); + } + Err(e) => { + eprintln!("Failed to send transaction: {:?}", e); + } + } + }) + .await + .unwrap(); +} diff --git a/src/optimized_transaction.rs b/src/optimized_transaction.rs index 7cad1f4..850399e 100644 --- a/src/optimized_transaction.rs +++ b/src/optimized_transaction.rs @@ -24,6 +24,7 @@ use solana_sdk::{ message::{v0, VersionedMessage}, pubkey::Pubkey, signature::{Signature, Signer}, + signer::keypair::Keypair, transaction::{Transaction, VersionedTransaction}, }; use solana_transaction_status::TransactionConfirmationStatus; @@ -37,7 +38,7 @@ impl Helius { /// * `instructions` - The transaction instructions /// * `payer` - The public key of the payer /// * `lookup_tables` - The address lookup tables - /// * `from_keypair` - The keypair signing the transaction (needed to simulate the transaction) + /// * `signers` - The signers for the transaction /// /// # Returns /// The compute units consumed, or None if unsuccessful @@ -417,6 +418,63 @@ impl Helius { }) } + /// Thread safe version of get_compute_units to simulate a transaction to get the total compute units consumed + /// + /// # Arguments + /// * `instructions` - The transaction instructions + /// * `payer` - The public key of the payer + /// * `lookup_tables` - The address lookup tables + /// * `keypairs` - The keypairs for the transaction + /// + /// # Returns + /// The compute units consumed, or None if unsuccessful + pub async fn get_compute_units_thread_safe( + &self, + instructions: Vec, + payer: Pubkey, + lookup_tables: Vec, + keypairs: Option<&[&Keypair]>, + ) -> Result> { + let test_instructions: Vec = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)] + .into_iter() + .chain(instructions) + .collect::>(); + + 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 { + let mut tx = VersionedTransaction { + signatures: vec![Signature::default(); keypairs.len()], + message: versioned_message.clone(), + }; + + for (i, keypair) in keypairs.iter().enumerate() { + tx.signatures[i] = keypair.sign_message(&versioned_message.serialize()); + } + + tx + } else { + VersionedTransaction { + signatures: vec![], + message: versioned_message, + } + }; + + let config: RpcSimulateTransactionConfig = RpcSimulateTransactionConfig { + sig_verify: keypairs.is_some(), + ..Default::default() + }; + + let result: Response = self + .connection() + .simulate_transaction_with_config(&transaction, config)?; + + Ok(result.value.units_consumed) + } + /// Sends a smart transaction using seed bytes /// /// This method allows for sending smart transactions in asynchronous contexts @@ -439,7 +497,7 @@ impl Helius { /// /// # Errors /// - /// This function will return an error if keypair creation from seeds fails, the underlying `send_smart_transaction` call fails, + /// This function will return an error if keypair creation from seeds fails, the transaction sending fails, /// or no signer seeds are provided /// /// # Notes @@ -457,38 +515,146 @@ impl Helius { )); } - let mut signers: Vec> = create_config + let keypairs: Vec = create_config .signer_seeds .into_iter() - .map(|seed| { - Arc::new(keypair_from_seed(&seed).expect("Failed to create keypair from seed")) as Arc - }) + .map(|seed| keypair_from_seed(&seed).expect("Failed to create keypair from seed")) .collect(); - // Determine the fee payer - let fee_payer_index: usize = if let Some(fee_payer_seed) = create_config.fee_payer_seed { - let fee_payer = - Arc::new(keypair_from_seed(&fee_payer_seed).expect("Failed to create fee payer keypair from seed")); - signers.push(fee_payer); - signers.len() - 1 // Index of the last signer (fee payer) + // Create the fee payer keypair if provided. Otherwise, we default to the first signer + 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 { - 0 // Index of the first signer + Keypair::from_bytes(&keypairs[0].to_bytes()).unwrap() }; - let fee_payer = signers[fee_payer_index].clone(); - let create_smart_transaction_config: CreateSmartTransactionConfig = CreateSmartTransactionConfig { - instructions: create_config.instructions, - signers, - lookup_tables: create_config.lookup_tables, - fee_payer: Some(fee_payer), - priority_fee_cap: create_config.priority_fee_cap, + + let (recent_blockhash, last_valid_block_hash) = self + .connection() + .get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())?; + + let mut final_instructions: Vec = vec![]; + + // Get priority fee estimate + let transaction: Transaction = Transaction::new_signed_with_payer( + &create_config.instructions, + Some(&fee_payer.pubkey()), + &[&fee_payer], + recent_blockhash, + ); + + let serialized_tx: Vec = serialize(&transaction).map_err(|e| HeliusError::InvalidInput(e.to_string()))?; + let transaction_base58: String = encode(&serialized_tx).into_string(); + + let priority_fee_request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest { + transaction: Some(transaction_base58), + account_keys: None, + options: Some(GetPriorityFeeEstimateOptions { + recommended: Some(true), + ..Default::default() + }), }; - let smart_transaction_config: SmartTransactionConfig = SmartTransactionConfig { - create_config: create_smart_transaction_config, - send_options: send_options.unwrap_or_default(), - timeout: timeout.unwrap_or_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 + }; + + // Add compute budget instructions + final_instructions.push(ComputeBudgetInstruction::set_compute_unit_price(priority_fee)); + + // Get optimal compute units + let mut test_instructions: Vec = final_instructions.clone(); + test_instructions.extend(create_config.instructions.clone()); + + let units: Option = self + .get_compute_units_thread_safe( + test_instructions, + fee_payer.pubkey(), + create_config.lookup_tables.clone().unwrap_or_default(), + Some(&[&fee_payer]), + ) + .await?; + + let compute_units: u64 = units.ok_or(HeliusError::InvalidInput( + "Error fetching compute units for the instructions provided".to_string(), + ))?; + + let customers_cu: u32 = if compute_units < 1000 { + 1000 + } else { + (compute_units as f64 * 1.1).ceil() as u32 }; - self.send_smart_transaction(smart_transaction_config).await + final_instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(customers_cu)); + final_instructions.extend(create_config.instructions); + + // Create the final transaction + 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 fee_payer_copy: Keypair = Keypair::from_bytes(&fee_payer.to_bytes()).unwrap(); + let mut all_signers: Vec = vec![fee_payer_copy]; + all_signers.extend(keypairs.into_iter().filter(|k| k.pubkey() != fee_payer.pubkey())); + + let mut tx: VersionedTransaction = VersionedTransaction { + signatures: vec![Signature::default(); all_signers.len()], + message: versioned_message.clone(), + }; + + // Sign message with all keypairs + for (i, keypair) in all_signers.iter().enumerate() { + tx.signatures[i] = keypair.sign_message(&versioned_message.serialize()); + } + + SmartTransaction::Versioned(tx) + } else { + let mut tx: Transaction = Transaction::new_with_payer(&final_instructions, Some(&fee_payer.pubkey())); + + let mut signers: Vec<&Keypair> = vec![&fee_payer]; + signers.extend(keypairs.iter().filter(|k| k.pubkey() != fee_payer.pubkey())); + + tx.sign(&signers, recent_blockhash); + + SmartTransaction::Legacy(tx) + }; + + // Send and confirm the transaction + 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 + } + } } } diff --git a/tests/rpc/test_get_assets_by_authority.rs b/tests/rpc/test_get_assets_by_authority.rs index b4854fb..1439f62 100644 --- a/tests/rpc/test_get_assets_by_authority.rs +++ b/tests/rpc/test_get_assets_by_authority.rs @@ -204,6 +204,7 @@ async fn test_get_assets_by_authority_success() { }, ], errors: None, + native_balance: None, }, id: "1".to_string(), }; diff --git a/tests/rpc/test_get_assets_by_creator.rs b/tests/rpc/test_get_assets_by_creator.rs index f54023a..9ddb5cb 100644 --- a/tests/rpc/test_get_assets_by_creator.rs +++ b/tests/rpc/test_get_assets_by_creator.rs @@ -204,6 +204,7 @@ async fn test_get_assets_by_creator_success() { }, ], errors: None, + native_balance: None, }, id: "1".to_string(), }; diff --git a/tests/rpc/test_get_assets_by_group.rs b/tests/rpc/test_get_assets_by_group.rs index e2ce4ee..6d06f7f 100644 --- a/tests/rpc/test_get_assets_by_group.rs +++ b/tests/rpc/test_get_assets_by_group.rs @@ -119,6 +119,7 @@ async fn test_get_assets_by_group_success() { }, ], errors: None, + native_balance: None, }, id: "1".to_string(), }; diff --git a/tests/rpc/test_get_assets_by_owner.rs b/tests/rpc/test_get_assets_by_owner.rs index 1d3ee52..97ff97d 100644 --- a/tests/rpc/test_get_assets_by_owner.rs +++ b/tests/rpc/test_get_assets_by_owner.rs @@ -134,6 +134,7 @@ async fn test_get_assets_by_owner_success() { mpl_core_info: None, }], errors: None, + native_balance: None, }, id: "1".to_string(), }; diff --git a/tests/rpc/test_search_assets.rs b/tests/rpc/test_search_assets.rs index 812641d..522eb34 100644 --- a/tests/rpc/test_search_assets.rs +++ b/tests/rpc/test_search_assets.rs @@ -134,6 +134,7 @@ async fn test_search_assets_success() { mpl_core_info: None, }], errors: None, + native_balance: None, }, id: "1".to_string(), }; diff --git a/tests/test_enhanced_transactions.rs b/tests/test_enhanced_transactions.rs index 9c33326..663448f 100644 --- a/tests/test_enhanced_transactions.rs +++ b/tests/test_enhanced_transactions.rs @@ -218,6 +218,11 @@ async fn test_parse_transaction_history_success() { let request: ParsedTransactionHistoryRequest = ParsedTransactionHistoryRequest { address: "46tC8n6GyWvUjFxpTE9juG5WZ72RXADpPhY4S1d6wvTi".to_string(), before: None, + until: None, + transaction_type: None, + commitment: None, + limit: None, + source: None, }; let response: Result> = helius.parsed_transaction_history(request).await; @@ -244,6 +249,7 @@ async fn test_parse_transaction_history_failure() { rpc: url.to_string(), }, }); + let client: Client = Client::new(); let rpc_client: Arc = Arc::new(RpcClient::new(Arc::new(client.clone()), Arc::clone(&config)).unwrap()); let helius: Helius = Helius { @@ -253,10 +259,17 @@ async fn test_parse_transaction_history_failure() { async_rpc_client: None, ws_client: None, }; + let request: ParsedTransactionHistoryRequest = ParsedTransactionHistoryRequest { address: "46tC8n6GyWvUjFxpTE9juG5WZ72RXADpPhY4S1d6wvTi".to_string(), before: None, + until: None, + transaction_type: None, + commitment: None, + limit: None, + source: None, }; + server .mock( "GET",