From 07a3dba11b65e7056afbd53cf9ffad6d5373efe5 Mon Sep 17 00:00:00 2001 From: Evan <0xIchigo@protonmail.com> Date: Tue, 21 May 2024 18:31:12 -0400 Subject: [PATCH 1/9] Implement New Errors and get_compute_units --- src/error.rs | 28 ++++++++++++++++++ src/lib.rs | 1 + src/optimized_transaction.rs | 55 ++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 src/optimized_transaction.rs diff --git a/src/error.rs b/src/error.rs index 3dcbaaa..52487dc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,9 @@ use reqwest::{Error as ReqwestError, StatusCode}; use serde_json::Error as SerdeJsonError; +use solana_client::client_error::ClientError; +use solana_sdk::sanitize::SanitizeError; +use solana_sdk::signature::SignerError; +use solana_sdk::message::CompileError; use thiserror::Error; /// Represents all possible errors returned by the `Helius` client @@ -14,6 +18,18 @@ pub enum HeliusError { #[error("Bad request to {path}: {text}")] BadRequest { path: String, text: String }, + /// Represents errors from the Solana client + /// + /// This captures errors from the Solana client library + #[error("Solana client error: {0}")] + ClientError(#[from] ClientError), + + /// Represents compile errors from the Solana SDK + /// + /// This captures all compile errors thrown by the Solana SDK + #[error("Compile error: {0}")] + CompileError(#[from] CompileError), + /// Represents errors that occur internally with Helius and our servers /// /// If the server encounters an unexpected condition that prevents it from fulfilling the request, this error is returned. @@ -60,6 +76,12 @@ pub enum HeliusError { #[error("Serialization / Deserialization error: {0}")] SerdeJson(SerdeJsonError), + /// Represents errors from the Solana SDK for signing operations + /// + /// This captures errors from the signing operations in the Solana SDK + #[error("Signer error: {0}")] + SignerError(#[from] SignerError), + /// Indicates the request lacked valid authentication credentials /// /// This error is returned in response to a missing, invalid, or expired API key @@ -98,5 +120,11 @@ impl From for HeliusError { } } +impl From for HeliusError { + fn from(err: SanitizeError) -> Self { + HeliusError::InvalidInput(err.to_string()) + } +} + /// A handy type alias for handling results across the Helius SDK pub type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs index 4e34a78..05eaa10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod enhanced_transactions; pub mod error; pub mod factory; pub mod mint_api; +pub mod optimized_transaction; pub mod request_handler; pub mod rpc_client; pub mod types; diff --git a/src/optimized_transaction.rs b/src/optimized_transaction.rs new file mode 100644 index 0000000..e66f711 --- /dev/null +++ b/src/optimized_transaction.rs @@ -0,0 +1,55 @@ +use crate::error::Result; +use crate::Helius; +use solana_client::rpc_response::{Response, RpcSimulateTransactionResult}; +use solana_sdk::{ + address_lookup_table::AddressLookupTableAccount, + compute_budget::ComputeBudgetInstruction, + hash::Hash, + instruction::Instruction, + message::{v0, VersionedMessage}, + pubkey::Pubkey, + transaction::VersionedTransaction, +}; + +impl Helius { + /// Simulates 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 + /// + /// # Returns + /// The compute units consumed, or None if unsuccessful + pub async fn get_compute_units( + &self, + instructions: Vec, + payer: Pubkey, + lookup_tables: Vec, + ) -> Result> { + // Set the compute budget limit + let test_instructions: Vec = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)] + .into_iter() + .chain(instructions) + .collect::>(); + + // Fetch the latest blockhash + let recent_blockhash: Hash = self.connection().get_latest_blockhash()?; + + // Create a v0::Message + let v0_message: v0::Message = v0::Message::try_compile(&payer, &test_instructions, &lookup_tables, recent_blockhash)?; + let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); + + // Create an unsigned VersionedTransaction + let transaction: VersionedTransaction = VersionedTransaction { + signatures: vec![], + message: versioned_message, + }; + + // Simulate the transaction + let result: Response = self.connection().simulate_transaction(&transaction)?; + + // Return the units consumed or None if not available + Ok(result.value.units_consumed) + } +} From 8320afe210fa6d5281fe5854dd144e1bece5d76a Mon Sep 17 00:00:00 2001 From: Evan <0xIchigo@protonmail.com> Date: Tue, 21 May 2024 18:33:37 -0400 Subject: [PATCH 2/9] Add include_vote Param --- examples/get_priority_fee_estimate.rs | 1 + src/types/types.rs | 1 + tests/rpc/test_get_priority_fee_estimate.rs | 2 ++ 3 files changed, 4 insertions(+) diff --git a/examples/get_priority_fee_estimate.rs b/examples/get_priority_fee_estimate.rs index 61b80b4..7c661db 100644 --- a/examples/get_priority_fee_estimate.rs +++ b/examples/get_priority_fee_estimate.rs @@ -18,6 +18,7 @@ async fn main() -> Result<(), HeliusError> { transaction_encoding: None, lookback_slots: None, recommended: None, + include_vote: None, }), }; diff --git a/src/types/types.rs b/src/types/types.rs index 8b8eea9..59f8ecb 100644 --- a/src/types/types.rs +++ b/src/types/types.rs @@ -778,6 +778,7 @@ pub struct GetPriorityFeeEstimateOptions { pub transaction_encoding: Option, pub lookback_slots: Option, pub recommended: Option, + pub include_vote: Option, } #[derive(Serialize, Deserialize, Debug, Default)] diff --git a/tests/rpc/test_get_priority_fee_estimate.rs b/tests/rpc/test_get_priority_fee_estimate.rs index d5d629c..1654cdb 100644 --- a/tests/rpc/test_get_priority_fee_estimate.rs +++ b/tests/rpc/test_get_priority_fee_estimate.rs @@ -63,6 +63,7 @@ async fn test_get_nft_editions_success() { transaction_encoding: None, lookback_slots: None, recommended: None, + include_vote: None, }), }; @@ -117,6 +118,7 @@ async fn test_get_nft_editions_failure() { transaction_encoding: None, lookback_slots: None, recommended: None, + include_vote: None, }), }; From c76a2114b6849dec26980c2224b0b0fb40eacba9 Mon Sep 17 00:00:00 2001 From: Evan <0xIchigo@protonmail.com> Date: Tue, 21 May 2024 18:33:52 -0400 Subject: [PATCH 3/9] Formatting --- src/error.rs | 4 ++-- src/optimized_transaction.rs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/error.rs b/src/error.rs index 52487dc..2492d72 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,9 @@ use reqwest::{Error as ReqwestError, StatusCode}; use serde_json::Error as SerdeJsonError; use solana_client::client_error::ClientError; +use solana_sdk::message::CompileError; use solana_sdk::sanitize::SanitizeError; use solana_sdk::signature::SignerError; -use solana_sdk::message::CompileError; use thiserror::Error; /// Represents all possible errors returned by the `Helius` client @@ -25,7 +25,7 @@ pub enum HeliusError { ClientError(#[from] ClientError), /// Represents compile errors from the Solana SDK - /// + /// /// This captures all compile errors thrown by the Solana SDK #[error("Compile error: {0}")] CompileError(#[from] CompileError), diff --git a/src/optimized_transaction.rs b/src/optimized_transaction.rs index e66f711..825f5ff 100644 --- a/src/optimized_transaction.rs +++ b/src/optimized_transaction.rs @@ -37,7 +37,8 @@ impl Helius { let recent_blockhash: Hash = self.connection().get_latest_blockhash()?; // Create a v0::Message - let v0_message: v0::Message = v0::Message::try_compile(&payer, &test_instructions, &lookup_tables, recent_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); // Create an unsigned VersionedTransaction From b9a13e3614d33c1e8148abef5916abccb3becffa Mon Sep 17 00:00:00 2001 From: Evan <0xIchigo@protonmail.com> Date: Tue, 21 May 2024 19:19:30 -0400 Subject: [PATCH 4/9] Add New Errors and poll_transaction_confirmation --- src/error.rs | 21 +++++++++++++++--- src/optimized_transaction.rs | 43 +++++++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/error.rs b/src/error.rs index 2492d72..fb649ff 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,12 @@ use reqwest::{Error as ReqwestError, StatusCode}; use serde_json::Error as SerdeJsonError; use solana_client::client_error::ClientError; -use solana_sdk::message::CompileError; -use solana_sdk::sanitize::SanitizeError; -use solana_sdk::signature::SignerError; +use solana_sdk::{ + message::CompileError, + sanitize::SanitizeError, + signature::SignerError, + transaction::TransactionError, +}; use thiserror::Error; /// Represents all possible errors returned by the `Helius` client @@ -82,6 +85,18 @@ pub enum HeliusError { #[error("Signer error: {0}")] SignerError(#[from] SignerError), + /// Indicates that the transaction confirmation timed out + /// + /// For polling a transaction's confirmation status + #[error("Transaction confirmation timed out with error code {code}: {text}")] + Timeout { code: StatusCode, text: String }, + + /// Represents transaction errors from the Solana SDK + /// + /// This captures errors that occur when processing transactions + #[error("Transaction error: {0}")] + TransactionError(#[from] TransactionError), + /// Indicates the request lacked valid authentication credentials /// /// This error is returned in response to a missing, invalid, or expired API key diff --git a/src/optimized_transaction.rs b/src/optimized_transaction.rs index 825f5ff..1f2d5ac 100644 --- a/src/optimized_transaction.rs +++ b/src/optimized_transaction.rs @@ -1,15 +1,21 @@ -use crate::error::Result; +use crate::error::{HeliusError, Result}; use crate::Helius; + +use reqwest::StatusCode; use solana_client::rpc_response::{Response, RpcSimulateTransactionResult}; use solana_sdk::{ address_lookup_table::AddressLookupTableAccount, + commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, hash::Hash, instruction::Instruction, message::{v0, VersionedMessage}, pubkey::Pubkey, + signature::Signature, transaction::VersionedTransaction, }; +use std::time::Duration; +use tokio::time::sleep; impl Helius { /// Simulates a transaction to get the total compute units consumed @@ -53,4 +59,39 @@ impl Helius { // Return the units consumed or None if not available Ok(result.value.units_consumed) } + + /// Poll a transaction to check whether it has been confirmed + /// + /// * `txt-sig` - The transaction signature to check + /// + /// # Returns + /// The confirmed transaction signature or an error if the confirmation times out + pub async fn poll_transaction_confirmation(&self, txt_sig: Signature) -> Result { + // 15 second timeout + let timeout: Duration = Duration::from_secs(15); + // 5 second retry interval + let interval: Duration = Duration::from_secs(5); + let mut elapsed = Duration::default(); + + let commitment_config: CommitmentConfig = CommitmentConfig::confirmed(); + + loop { + if elapsed >= timeout { + return Err(HeliusError::Timeout { + code: StatusCode::REQUEST_TIMEOUT, + text: format!("Transaction {}'s confirmation timed out", txt_sig), + }); + } + + match self.connection().get_signature_status_with_commitment(&txt_sig, commitment_config) { + Ok(Some(Ok(()))) => return Ok(txt_sig), + Ok(Some(Err(err))) => return Err(HeliusError::TransactionError(err)), + Ok(None) => { + sleep(interval).await; + elapsed += interval; + } + Err(err) => return Err(HeliusError::ClientError(err)), + } + } + } } From bf0ab252ff277f2061234e96eb1b3680f40e4195 Mon Sep 17 00:00:00 2001 From: Evan <0xIchigo@protonmail.com> Date: Tue, 21 May 2024 20:14:35 -0400 Subject: [PATCH 5/9] Implement send_smart_transaction Functionality --- Cargo.toml | 2 + src/error.rs | 5 +- src/optimized_transaction.rs | 118 ++++++++++++++++++++++++++++++++++- src/types/types.rs | 21 +++++++ 4 files changed, 139 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8db55cb..b627664 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ chrono = { version = "0.4.11", features = ["serde"] } solana-client = "1.18.12" solana-program = "1.18.12" serde-enum-str = "0.4.0" +bincode = "1.3.3" +base64 = "0.22.1" [dev-dependencies] mockito = "1.4.0" diff --git a/src/error.rs b/src/error.rs index fb649ff..0b65d37 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,10 +2,7 @@ use reqwest::{Error as ReqwestError, StatusCode}; use serde_json::Error as SerdeJsonError; use solana_client::client_error::ClientError; use solana_sdk::{ - message::CompileError, - sanitize::SanitizeError, - signature::SignerError, - transaction::TransactionError, + message::CompileError, sanitize::SanitizeError, signature::SignerError, transaction::TransactionError, }; use thiserror::Error; diff --git a/src/optimized_transaction.rs b/src/optimized_transaction.rs index 1f2d5ac..bc02d1a 100644 --- a/src/optimized_transaction.rs +++ b/src/optimized_transaction.rs @@ -1,7 +1,12 @@ use crate::error::{HeliusError, Result}; +use crate::types::{GetPriorityFeeEstimateRequest, GetPriorityFeeEstimateResponse, SmartTransactionConfig}; use crate::Helius; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use bincode::{ErrorKind, serialize}; use reqwest::StatusCode; +use solana_client::rpc_config::RpcSendTransactionConfig; use solana_client::rpc_response::{Response, RpcSimulateTransactionResult}; use solana_sdk::{ address_lookup_table::AddressLookupTableAccount, @@ -11,8 +16,8 @@ use solana_sdk::{ instruction::Instruction, message::{v0, VersionedMessage}, pubkey::Pubkey, - signature::Signature, - transaction::VersionedTransaction, + signature::{Signature, Signer}, + transaction::{Transaction, VersionedTransaction}, }; use std::time::Duration; use tokio::time::sleep; @@ -83,7 +88,10 @@ impl Helius { }); } - match self.connection().get_signature_status_with_commitment(&txt_sig, commitment_config) { + match self + .connection() + .get_signature_status_with_commitment(&txt_sig, commitment_config) + { Ok(Some(Ok(()))) => return Ok(txt_sig), Ok(Some(Err(err))) => return Err(HeliusError::TransactionError(err)), Ok(None) => { @@ -94,4 +102,108 @@ impl Helius { } } } + + /// Builds and sends an optimized transaction, and handles its confirmation status + /// + /// # Arguments + /// * `config` - The configuration for the smart transaction, which includes the transaction's instructions, the user's keypair, whether preflight checks + /// should be skipped, and how many times to retry the transaction, if provided + /// + /// # Returns + /// 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()?; + + // Build the initial transaction and estimate the priority fee + let mut transaction: Transaction = Transaction::new_with_payer(&config.instructions, Some(&pubkey)); + transaction.try_sign(&[config.from_keypair], recent_blockhash)?; + + // Serialize the transaction + let serialized_transaction: Vec = serialize(&transaction).map_err(|e: Box| HeliusError::InvalidInput(e.to_string()))?; + + // Convert the serialized transaction to a Base64 string + let transaction_base64: String = STANDARD.encode(&serialized_transaction); + + // Get the priority fee estimate based on the serialized transaction + let priority_fee_request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest { + transaction: Some(transaction_base64), + account_keys: None, + options: None, + }; + + let priority_fee_estimate: GetPriorityFeeEstimateResponse = + self.rpc().get_priority_fee_estimate(priority_fee_request).await?; + + let priority_fee = priority_fee_estimate + .priority_fee_estimate + .ok_or(HeliusError::InvalidInput( + "Priority fee estimate not available".to_string(), + ))? + .to_bits(); + + // Add the compute unit price instruction with the estimated fee + let compute_budget_ix: Instruction = ComputeBudgetInstruction::set_compute_unit_price(priority_fee); + let mut final_instructions: Vec = vec![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, vec![]) + .await? + { + // Add some margin to the compute units + 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); + } + + // Build the optimized transaction + let mut optimized_transaction: Transaction = Transaction::new_with_payer(&final_instructions, Some(&pubkey)); + optimized_transaction.try_sign(&[config.from_keypair], recent_blockhash)?; + + // Re-fetch the blockhash every 4 retries, or roughly once every minute + let blockhash_validity_threshold: usize = 4; + + let mut retry_count: usize = 0; + let txt_sig: Signature; + + let skip_preflight_checks: bool = config.skip_preflight_checks.unwrap_or(true); + let send_transaction_config: RpcSendTransactionConfig = RpcSendTransactionConfig { + skip_preflight: skip_preflight_checks, + ..Default::default() + }; + let max_retries: usize = config.max_retries.unwrap_or(6); + + // Send the transaction with configurable retries and preflight checks + while retry_count <= max_retries { + if retry_count > 0 && retry_count % blockhash_validity_threshold == 0 { + recent_blockhash = self.connection().get_latest_blockhash()?; + optimized_transaction.try_sign(&[config.from_keypair], recent_blockhash)?; + } + + match self + .connection() + .send_transaction_with_config(&optimized_transaction, send_transaction_config) + { + Ok(signature) => { + txt_sig = signature; + return self.poll_transaction_confirmation(txt_sig).await; + } + Err(error) => { + if retry_count == max_retries { + return Err(HeliusError::ClientError(error)); + } + retry_count += 1; + } + } + + sleep(Duration::from_secs(5)).await; + } + + Err(HeliusError::Timeout { + code: StatusCode::REQUEST_TIMEOUT, + text: "Reached an unexpected point in send_smart_transaction".to_string(), + }) + } } diff --git a/src/types/types.rs b/src/types/types.rs index 59f8ecb..856fc94 100644 --- a/src/types/types.rs +++ b/src/types/types.rs @@ -8,6 +8,9 @@ use crate::types::{DisplayOptions, GetAssetOptions}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use solana_sdk::instruction::Instruction; +use solana_sdk::signature::Keypair; + /// Defines the available clusters supported by Helius #[derive(Debug, Clone, PartialEq)] pub enum Cluster { @@ -940,3 +943,21 @@ pub struct EditWebhookRequest { #[serde(default)] pub encoding: AccountWebhookEncoding, } + +pub struct SmartTransactionConfig<'a> { + pub instructions: Vec, + pub from_keypair: &'a Keypair, + pub skip_preflight_checks: Option, + pub max_retries: Option, +} + +impl<'a> SmartTransactionConfig<'a> { + pub fn new(instructions: Vec, from_keypair: &'a Keypair) -> Self { + Self { + instructions, + from_keypair, + skip_preflight_checks: None, + max_retries: None, + } + } +} From 83d29f9b87e8ac611dbc1763e71a60c2e0e273a7 Mon Sep 17 00:00:00 2001 From: Evan <0xIchigo@protonmail.com> Date: Tue, 21 May 2024 20:20:03 -0400 Subject: [PATCH 6/9] Formatting --- src/optimized_transaction.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/optimized_transaction.rs b/src/optimized_transaction.rs index bc02d1a..c4d736b 100644 --- a/src/optimized_transaction.rs +++ b/src/optimized_transaction.rs @@ -4,7 +4,7 @@ use crate::Helius; use base64::engine::general_purpose::STANDARD; use base64::Engine; -use bincode::{ErrorKind, serialize}; +use bincode::{serialize, ErrorKind}; use reqwest::StatusCode; use solana_client::rpc_config::RpcSendTransactionConfig; use solana_client::rpc_response::{Response, RpcSimulateTransactionResult}; @@ -120,7 +120,8 @@ impl Helius { transaction.try_sign(&[config.from_keypair], recent_blockhash)?; // Serialize the transaction - let serialized_transaction: Vec = serialize(&transaction).map_err(|e: Box| HeliusError::InvalidInput(e.to_string()))?; + let serialized_transaction: Vec = + serialize(&transaction).map_err(|e: Box| HeliusError::InvalidInput(e.to_string()))?; // Convert the serialized transaction to a Base64 string let transaction_base64: String = STANDARD.encode(&serialized_transaction); From 3f2a27f01e6b02e6ab5cf8ae08f2e26f565057e3 Mon Sep 17 00:00:00 2001 From: Evan <0xIchigo@protonmail.com> Date: Wed, 22 May 2024 13:42:38 -0400 Subject: [PATCH 7/9] Start Addressing Comments --- src/optimized_transaction.rs | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/optimized_transaction.rs b/src/optimized_transaction.rs index c4d736b..84fb363 100644 --- a/src/optimized_transaction.rs +++ b/src/optimized_transaction.rs @@ -1,5 +1,8 @@ use crate::error::{HeliusError, Result}; -use crate::types::{GetPriorityFeeEstimateRequest, GetPriorityFeeEstimateResponse, SmartTransactionConfig}; +use crate::types::{ + GetPriorityFeeEstimateOptions, GetPriorityFeeEstimateRequest, GetPriorityFeeEstimateResponse, + SmartTransactionConfig, +}; use crate::Helius; use base64::engine::general_purpose::STANDARD; @@ -19,7 +22,7 @@ use solana_sdk::{ signature::{Signature, Signer}, transaction::{Transaction, VersionedTransaction}, }; -use std::time::Duration; +use std::time::{Duration, Instant}; use tokio::time::sleep; impl Helius { @@ -76,12 +79,12 @@ impl Helius { let timeout: Duration = Duration::from_secs(15); // 5 second retry interval let interval: Duration = Duration::from_secs(5); - let mut elapsed = Duration::default(); + let start: Instant = Instant::now(); let commitment_config: CommitmentConfig = CommitmentConfig::confirmed(); loop { - if elapsed >= timeout { + if start.elapsed() >= timeout { return Err(HeliusError::Timeout { code: StatusCode::REQUEST_TIMEOUT, text: format!("Transaction {}'s confirmation timed out", txt_sig), @@ -96,7 +99,6 @@ impl Helius { Ok(Some(Err(err))) => return Err(HeliusError::TransactionError(err)), Ok(None) => { sleep(interval).await; - elapsed += interval; } Err(err) => return Err(HeliusError::ClientError(err)), } @@ -130,18 +132,23 @@ impl Helius { let priority_fee_request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest { transaction: Some(transaction_base64), account_keys: None, - options: 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 = priority_fee_estimate + let priority_fee_f64 = priority_fee_estimate .priority_fee_estimate .ok_or(HeliusError::InvalidInput( "Priority fee estimate not available".to_string(), - ))? - .to_bits(); + ))?; + + // Directly cast as u64 + let priority_fee: u64 = priority_fee_f64 as u64; // Add the compute unit price instruction with the estimated fee let compute_budget_ix: Instruction = ComputeBudgetInstruction::set_compute_unit_price(priority_fee); @@ -153,7 +160,7 @@ impl Helius { .get_compute_units(final_instructions.clone(), pubkey, vec![]) .await? { - // Add some margin to the compute units + // 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); @@ -192,14 +199,15 @@ impl Helius { return self.poll_transaction_confirmation(txt_sig).await; } Err(error) => { - if retry_count == max_retries { + retry_count += 1; + + if retry_count > max_retries { return Err(HeliusError::ClientError(error)); } - retry_count += 1; + + continue; } } - - sleep(Duration::from_secs(5)).await; } Err(HeliusError::Timeout { From 4c7b50b248af48cffbf8d44f7537c320f0263cb5 Mon Sep 17 00:00:00 2001 From: Evan <0xIchigo@protonmail.com> Date: Wed, 22 May 2024 16:23:14 -0400 Subject: [PATCH 8/9] Handle Versioned Transactions Properly --- src/optimized_transaction.rs | 206 +++++++++++++++++++++++++---------- src/types/types.rs | 5 +- 2 files changed, 154 insertions(+), 57 deletions(-) diff --git a/src/optimized_transaction.rs b/src/optimized_transaction.rs index 84fb363..905589d 100644 --- a/src/optimized_transaction.rs +++ b/src/optimized_transaction.rs @@ -108,103 +108,199 @@ impl Helius { /// Builds and sends an optimized transaction, and handles its confirmation status /// /// # Arguments - /// * `config` - The configuration for the smart transaction, which includes the transaction's instructions, the user's keypair, whether preflight checks - /// should be skipped, and how many times to retry the transaction, if provided + /// * `config` - The configuration for the smart transaction, which includes the transaction's instructions, and the user's keypair. If provided, it also + /// includes whether preflight checks should be skipped, how many times to retry the transaction, and any address lookup tables to be included in the transaction /// /// # Returns /// 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 mut final_instructions: Vec = vec![]; - // Build the initial transaction and estimate the priority fee - let mut transaction: Transaction = Transaction::new_with_payer(&config.instructions, Some(&pubkey)); - transaction.try_sign(&[config.from_keypair], recent_blockhash)?; - - // Serialize the transaction - let serialized_transaction: Vec = - serialize(&transaction).map_err(|e: Box| HeliusError::InvalidInput(e.to_string()))?; - - // Convert the serialized transaction to a Base64 string - let transaction_base64: String = STANDARD.encode(&serialized_transaction); - - // 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() - }), - }; + // Determine if we need to use a versioned transaction + let is_versioned: bool = 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(); + 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)?; + let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); + + versioned_transaction = Some(VersionedTransaction { + signatures: vec![], + message: versioned_message, + }); + } else { + // If no lookup tables are present, we build a regular transaction + let mut tx: Transaction = Transaction::new_with_payer(&config.instructions, Some(&pubkey)); + tx.try_sign(&[config.from_keypair], recent_blockhash)?; + 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_estimate: GetPriorityFeeEstimateResponse = + self.rpc().get_priority_fee_estimate(priority_fee_request).await?; - let priority_fee_f64 = priority_fee_estimate - .priority_fee_estimate - .ok_or(HeliusError::InvalidInput( - "Priority fee estimate not available".to_string(), - ))?; + let priority_fee_f64: f64 = + priority_fee_estimate + .priority_fee_estimate + .ok_or(HeliusError::InvalidInput( + "Priority fee estimate not available".to_string(), + ))?; - // Directly cast as u64 - let priority_fee: u64 = priority_fee_f64 as u64; + priority_fee_f64 as u64 + } 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 + } else { + return Err(HeliusError::InvalidInput("No transaction available".to_string())); + }; // Add the compute unit price instruction with the estimated fee let compute_budget_ix: Instruction = ComputeBudgetInstruction::set_compute_unit_price(priority_fee); - let mut final_instructions: Vec = vec![compute_budget_ix]; + 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, vec![]) + .get_compute_units( + final_instructions.clone(), + pubkey, + config.lookup_tables.clone().unwrap_or_default(), + ) .await? { + println!("Compute units: {}", units); // 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); } - // Build the optimized transaction - let mut optimized_transaction: Transaction = Transaction::new_with_payer(&final_instructions, Some(&pubkey)); - optimized_transaction.try_sign(&[config.from_keypair], recent_blockhash)?; + // Rebuild the transaction with the final instructions + if is_versioned { + let lookup_tables: &[AddressLookupTableAccount] = config.lookup_tables.as_deref().unwrap(); + let v0_message: v0::Message = + v0::Message::try_compile(&pubkey, &final_instructions, lookup_tables, recent_blockhash)?; + let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); - // Re-fetch the blockhash every 4 retries, or roughly once every minute - let blockhash_validity_threshold: usize = 4; + versioned_transaction = Some(VersionedTransaction { + signatures: vec![], + message: versioned_message, + }); + } else { + let mut tx = Transaction::new_with_payer(&final_instructions, Some(&pubkey)); + tx.try_sign(&[config.from_keypair], recent_blockhash)?; + legacy_transaction = Some(tx); + } - let mut retry_count: usize = 0; - let txt_sig: Signature; + // Re-fetch interval of 60 seconds + let blockhash_refetch_interval: Duration = Duration::from_secs(60); + let mut last_blockhash_refetch: Instant = Instant::now(); - let skip_preflight_checks: bool = config.skip_preflight_checks.unwrap_or(true); let send_transaction_config: RpcSendTransactionConfig = RpcSendTransactionConfig { - skip_preflight: skip_preflight_checks, + skip_preflight: config.skip_preflight_checks.unwrap_or(true), ..Default::default() }; + + // Common logic for sending transactions + let send_result = |transaction: &Transaction| { + self.connection() + .send_transaction_with_config(transaction, send_transaction_config) + }; + let send_versioned_result = |transaction: &VersionedTransaction| { + self.connection() + .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); - // Send the transaction with configurable retries and preflight checks while retry_count <= max_retries { - if retry_count > 0 && retry_count % blockhash_validity_threshold == 0 { + if last_blockhash_refetch.elapsed() >= blockhash_refetch_interval { recent_blockhash = self.connection().get_latest_blockhash()?; - optimized_transaction.try_sign(&[config.from_keypair], recent_blockhash)?; + if is_versioned { + let signers: Vec<&dyn Signer> = vec![config.from_keypair]; + let signed_message = 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(); } - match self - .connection() - .send_transaction_with_config(&optimized_transaction, send_transaction_config) - { - Ok(signature) => { - txt_sig = signature; - return self.poll_transaction_confirmation(txt_sig).await; - } + let result = if is_versioned { + send_versioned_result(versioned_transaction.as_ref().unwrap()) + } else { + send_result(legacy_transaction.as_ref().unwrap()) + }; + + 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; } } diff --git a/src/types/types.rs b/src/types/types.rs index 856fc94..8ecb7a6 100644 --- a/src/types/types.rs +++ b/src/types/types.rs @@ -8,8 +8,7 @@ use crate::types::{DisplayOptions, GetAssetOptions}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use solana_sdk::instruction::Instruction; -use solana_sdk::signature::Keypair; +use solana_sdk::{address_lookup_table::AddressLookupTableAccount, instruction::Instruction, signature::Keypair}; /// Defines the available clusters supported by Helius #[derive(Debug, Clone, PartialEq)] @@ -949,6 +948,7 @@ pub struct SmartTransactionConfig<'a> { pub from_keypair: &'a Keypair, pub skip_preflight_checks: Option, pub max_retries: Option, + pub lookup_tables: Option>, } impl<'a> SmartTransactionConfig<'a> { @@ -958,6 +958,7 @@ impl<'a> SmartTransactionConfig<'a> { from_keypair, skip_preflight_checks: None, max_retries: None, + lookup_tables: None, } } } From 514de035817650828713108b27fedd649099651d Mon Sep 17 00:00:00 2001 From: Evan <0xIchigo@protonmail.com> Date: Thu, 23 May 2024 15:26:45 -0400 Subject: [PATCH 9/9] Support For Versioned Transactions --- src/optimized_transaction.rs | 44 +++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/optimized_transaction.rs b/src/optimized_transaction.rs index 905589d..b384993 100644 --- a/src/optimized_transaction.rs +++ b/src/optimized_transaction.rs @@ -9,7 +9,7 @@ use base64::engine::general_purpose::STANDARD; use base64::Engine; use bincode::{serialize, ErrorKind}; use reqwest::StatusCode; -use solana_client::rpc_config::RpcSendTransactionConfig; +use solana_client::rpc_config::{RpcSendTransactionConfig, RpcSimulateTransactionConfig}; use solana_client::rpc_response::{Response, RpcSimulateTransactionResult}; use solana_sdk::{ address_lookup_table::AddressLookupTableAccount, @@ -19,7 +19,7 @@ use solana_sdk::{ instruction::Instruction, message::{v0, VersionedMessage}, pubkey::Pubkey, - signature::{Signature, Signer}, + signature::{Keypair, Signature, Signer}, transaction::{Transaction, VersionedTransaction}, }; use std::time::{Duration, Instant}; @@ -32,6 +32,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) /// /// # Returns /// The compute units consumed, or None if unsuccessful @@ -40,6 +41,7 @@ impl Helius { instructions: Vec, payer: Pubkey, lookup_tables: Vec, + from_keypair: &Keypair, ) -> Result> { // Set the compute budget limit let test_instructions: Vec = vec![ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)] @@ -55,14 +57,18 @@ impl Helius { v0::Message::try_compile(&payer, &test_instructions, &lookup_tables, recent_blockhash)?; let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); - // Create an unsigned VersionedTransaction - let transaction: VersionedTransaction = VersionedTransaction { - signatures: vec![], - message: versioned_message, - }; + // Create a signed VersionedTransaction + let transaction: VersionedTransaction = VersionedTransaction::try_new(versioned_message, &[from_keypair]) + .map_err(|e| HeliusError::InvalidInput(format!("Signing error: {:?}", e)))?; // Simulate the transaction - let result: Response = self.connection().simulate_transaction(&transaction)?; + let config: RpcSimulateTransactionConfig = RpcSimulateTransactionConfig { + sig_verify: true, + ..Default::default() + }; + let result: Response = self + .connection() + .simulate_transaction_with_config(&transaction, config)?; // Return the units consumed or None if not available Ok(result.value.units_consumed) @@ -126,7 +132,7 @@ impl Helius { // 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(); + 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()); @@ -134,8 +140,15 @@ impl Helius { v0::Message::try_compile(&pubkey, &instructions, lookup_tables, recent_blockhash)?; let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); + // Sign the versioned transaction + let signers: Vec<&dyn Signer> = vec![config.from_keypair]; + let signatures: Vec = signers + .iter() + .map(|signer| signer.try_sign_message(versioned_message.serialize().as_slice())) + .collect::, _>>()?; + versioned_transaction = Some(VersionedTransaction { - signatures: vec![], + signatures, message: versioned_message, }); } else { @@ -213,10 +226,10 @@ impl Helius { final_instructions.clone(), pubkey, config.lookup_tables.clone().unwrap_or_default(), + &config.from_keypair, ) .await? { - println!("Compute units: {}", units); // 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); @@ -229,9 +242,14 @@ impl Helius { let v0_message: v0::Message = v0::Message::try_compile(&pubkey, &final_instructions, lookup_tables, recent_blockhash)?; let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); + let signers: Vec<&dyn Signer> = vec![config.from_keypair]; + let signatures: Vec = signers + .iter() + .map(|signer| signer.try_sign_message(versioned_message.serialize().as_slice())) + .collect::, _>>()?; versioned_transaction = Some(VersionedTransaction { - signatures: vec![], + signatures, message: versioned_message, }); } else { @@ -268,7 +286,7 @@ impl Helius { recent_blockhash = self.connection().get_latest_blockhash()?; if is_versioned { let signers: Vec<&dyn Signer> = vec![config.from_keypair]; - let signed_message = signers + let signed_message: Vec = signers .iter() .map(|signer| { signer.try_sign_message(