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] 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, + } + } +}