diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d523893..f8ffb22 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -24,22 +24,28 @@ jobs: fetch-depth: '0' - name: Cache cargo registry - uses: actions/cache@v3 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + run: | + timeout 10m bash -c "actions/cache@v3 \ + with: \ + path: ~/.cargo/registry \ + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} \ + restore-keys: ${{ runner.os }}-cargo-registry- || echo 'Cache registry step failed or timed out.'" - name: Cache cargo index - uses: actions/cache@v3 - with: - path: ~/.cargo/git - key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + run: | + timeout 10m bash -c "actions/cache@v3 \ + with: \ + path: ~/.cargo/git \ + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} \ + restore-keys: ${{ runner.os }}-cargo-index- || echo 'Cache index step failed or timed out.'" - name: Cache target directory - uses: actions/cache@v3 - with: - path: target - key: ${{ runner.os }}-target-${{ hashFiles('**/Cargo.lock') }} + run: | + timeout 10m bash -c "actions/cache@v3 \ + with: \ + path: target \ + key: ${{ runner.os }}-target-${{ hashFiles('**/Cargo.lock') }} \ + restore-keys: ${{ runner.os }}-target- || echo 'Cache target step failed or timed out.'" - name: Set up Rust uses: actions-rs/toolchain@v1 @@ -48,21 +54,42 @@ jobs: profile: minimal override: true components: clippy - + - name: Install Protobuf Compiler run: sudo apt-get update && sudo apt-get install -y protobuf-compiler - + + - name: Clean up disk space before build + run: | + sudo apt-get clean + sudo rm -rf /var/lib/apt/lists/* + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + df -h + - name: Check formatting run: cargo fmt --all -- --check - + - name: rust-clippy-check uses: giraffate/clippy-action@v1 with: - reporter: 'github-pr-review' - github_token: ${{ secrets.GITHUB_TOKEN }} + reporter: 'github-pr-review' + github_token: ${{ secrets.GITHUB_TOKEN }} - name: Build run: cargo build --verbose - + + - name: Save build artifacts + uses: actions/upload-artifact@v2 + with: + name: build + path: target + + - name: Show disk usage after build + run: df -h + - name: Run tests run: cargo test --verbose + + - name: Show disk usage after tests + run: df -h diff --git a/Cargo.toml b/Cargo.toml index 741f853..738f57f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helius" -version = "0.2.0" +version = "0.2.1" edition = "2021" description = "An asynchronous Helius Rust SDK for building the future of Solana" keywords = ["helius", "solana", "asynchronous-sdk", "das", "cryptocurrency"] @@ -17,6 +17,8 @@ bincode = "1.3.3" chrono = { version = "0.4.11", features = ["serde"] } futures = "0.3.30" futures-util = "0.3.30" +phf = { version = "0.11.2", features = ["macros"] } +rand = "0.8.5" reqwest = { version = "0.12.3", features = ["json"] } semver = "1.0.23" serde = "1.0.198" diff --git a/README.md b/README.md index e2d684e..a8d40d7 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Remember to run `cargo update` regularly to fetch the latest version of the SDK. ## Usage ### `Helius` -The SDK provides a [`Helius`](https://github.com/helius-labs/helius-rust-sdk/blob/dev/src/client.rs) instance that can be configured with an API key and a given Solana cluster. Developers can generate a new API key on the [Helius Developer Dashboard](https://dev.helius.xyz/dashboard/app). This instance acts as the main entry point for interacting with the SDK by providing methods to access different Solana and RPC client functionalities. The following code is an example of how to use the SDK to fetch info on [Mad Lad #8420](https://xray.helius.xyz/token/F9Lw3ki3hJ7PF9HQXsBzoY8GyE6sPoEZZdXJBsTTD2rk?network=mainnet): +The SDK provides a [`Helius`](https://github.com/helius-labs/helius-rust-sdk/blob/dev/src/client.rs) instance that can be configured with an API key and a given Solana cluster. Developers can generate a new API key on the [Helius Developer Dashboard](https://dev.helius.xyz/dashboard/app). This instance acts as the main entry point for interacting with the SDK by providing methods to access different Solana and RPC client functionalities. The following code is an example of how to use the SDK to fetch info on [Mad Lad #8420](https://explorer.solana.com/address/F9Lw3ki3hJ7PF9HQXsBzoY8GyE6sPoEZZdXJBsTTD2rk?network=mainnet): ```rust use helius::error::Result; use helius::types::{Cluster, DisplayOptions, GetAssetRequest, GetAssetResponseForAsset}; @@ -32,12 +32,7 @@ async fn main() -> Result<()> { let request: GetAssetRequest = GetAssetRequest { id: "F9Lw3ki3hJ7PF9HQXsBzoY8GyE6sPoEZZdXJBsTTD2rk".to_string(), - display_options: Some(DisplayOptions { - show_unverified_collections: false, - show_collection_metadata: false, - show_fungible: false, - show_inscription: false, - }), + display_options: None, }; let response: Result> = helius.rpc().get_asset(request).await; @@ -123,9 +118,17 @@ Our SDK is designed to provide a seamless developer experience when building on - [`remove_addresses_from_webhook`](https://github.com/helius-labs/helius-rust-sdk/blob/bf24259e3333ae93126bb65b342c2c63e80e07a6/src/webhook.rs#L75-L105) - Removes a list of addresses from an existing webhook by its ID ### Smart Transactions +- [`create_smart_transaction`](https://github.com/helius-labs/helius-rust-sdk/blob/705d66fb7d4004fc32c2a5f0d6ca4a1f2a7b175d/src/optimized_transaction.rs#L113-L312) - Creates an optimized transaction based on the provided configuration - [`get_compute_units`](https://github.com/helius-labs/helius-rust-sdk/blob/a79a751e1a064125010bdb359068a366d635d005/src/optimized_transaction.rs#L29-L75) - Simulates a transaction to get the total compute units consumed - [`poll_transaction_confirmation`](https://github.com/helius-labs/helius-rust-sdk/blob/a79a751e1a064125010bdb359068a366d635d005/src/optimized_transaction.rs#L77-L112) - Polls a transaction to check whether it has been confirmed in 5 second intervals with a 15 second timeout -- [`send_smart_transaction`](https://github.com/helius-labs/helius-rust-sdk/blob/a79a751e1a064125010bdb359068a366d635d005/src/optimized_transaction.rs#L114-L332) - Builds and sends an optimized transaction, and handles its confirmation status +- [`send_smart_transaction`](https://github.com/helius-labs/helius-rust-sdk/blob/705d66fb7d4004fc32c2a5f0d6ca4a1f2a7b175d/src/optimized_transaction.rs#L314-L374) - Builds and sends an optimized transaction, and handles its confirmation status + +### Jito Smart Transactions and Helper Methods +- [`add_tip_instruction`](https://github.com/helius-labs/helius-rust-sdk/blob/02b351a5ee3fe16a36078b40f92dc72d0ad077ed/src/jito.rs#L66-L83) - Adds a tip instruction to the instructions provided +- [`create_smart_transaction_with_tip`](https://github.com/helius-labs/helius-rust-sdk/blob/02b351a5ee3fe16a36078b40f92dc72d0ad077ed/src/jito.rs#L85-L124) - Creates a smart transaction with a Jito tip +- [`get_bundle_statuses`](https://github.com/helius-labs/helius-rust-sdk/blob/02b351a5ee3fe16a36078b40f92dc72d0ad077ed/src/jito.rs#L169-L202) - Get the status of Jito bundles +- [`send_jito_bundle`](https://github.com/helius-labs/helius-rust-sdk/blob/02b351a5ee3fe16a36078b40f92dc72d0ad077ed/src/jito.rs#L126-L167) - Sends a bundle of transactions to the Jito Block Engine +- [`send_smart_transaction_with_tip`](https://github.com/helius-labs/helius-rust-sdk/blob/02b351a5ee3fe16a36078b40f92dc72d0ad077ed/src/jito.rs#L204-L269) - Sends a smart transaction as a Jito bundle with a tip ### Helper Methods - [`get_priority_fee_estimate`](https://docs.helius.dev/solana-rpc-nodes/alpha-priority-fee-api) - Gets an estimate of the priority fees required for a transaction to be processed more quickly diff --git a/examples/send_smart_transaction_with_tip.rs b/examples/send_smart_transaction_with_tip.rs new file mode 100644 index 0000000..2f6fa76 --- /dev/null +++ b/examples/send_smart_transaction_with_tip.rs @@ -0,0 +1,70 @@ +use helius::types::*; +use helius::Helius; +use solana_client::rpc_config::RpcSendTransactionConfig; +use solana_sdk::{ + instruction::Instruction, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, signature::Keypair, signer::Signer, + system_instruction::transfer, +}; +use std::str::FromStr; +use std::time::Duration; +use tokio::time::sleep; + +#[tokio::main] +async fn main() { + let api_key: &str = "YOUR_API_KEY"; + let cluster: Cluster = Cluster::MainnetBeta; + let helius: Helius = Helius::new(api_key, cluster).unwrap(); + + // Replace with your actual keypair + let from_keypair: Keypair = Keypair::new(); + let from_pubkey: Pubkey = from_keypair.pubkey(); + + // Replace with the recipient's public key + let to_pubkey: Pubkey = Pubkey::from_str("RecipientPublicKeyHere").unwrap(); + + // Create a simple instruction (transfer 0.01 SOL from from_pubkey to to_pubkey) + let transfer_amount: u64 = 100_000; // 0.01 SOL in lamports + let instructions: Vec = vec![transfer(&from_pubkey, &to_pubkey, transfer_amount)]; + + let create_config: CreateSmartTransactionConfig = CreateSmartTransactionConfig { + instructions, + signers: vec![&from_keypair], + lookup_tables: None, + fee_payer: None, + }; + + let config: SmartTransactionConfig = SmartTransactionConfig { + create_config, + send_options: RpcSendTransactionConfig { + skip_preflight: true, + preflight_commitment: None, + encoding: None, + max_retries: None, + min_context_slot: None, + }, + }; + + // Send the optimized transaction with a 10k lamport tip using the New York region's API URL + match helius + .send_smart_transaction_with_tip(config, Some(10000), Some("NY")) + .await + { + Ok(bundle_id) => { + println!("Transaction sent successfully: {}", bundle_id); + sleep(Duration::from_secs(5)).await; + + // Get final balances + let balance_from = helius.connection().get_balance(&from_pubkey).unwrap_or(0); + println!( + "From Wallet Balance: {} SOL", + balance_from as f64 / LAMPORTS_PER_SOL as f64 + ); + + let balance_to = helius.connection().get_balance(&to_pubkey).unwrap_or(0); + println!("To Wallet Balance: {} SOL", balance_to as f64 / LAMPORTS_PER_SOL as f64); + } + Err(e) => { + eprintln!("Failed to send transaction: {:?}", e); + } + } +} diff --git a/src/jito.rs b/src/jito.rs new file mode 100644 index 0000000..1534f0c --- /dev/null +++ b/src/jito.rs @@ -0,0 +1,269 @@ +#![allow(unused_imports)] +/// Jito Smart Transactions +/// +/// This module allows the creation and sending of smart transactions with Jito tips. +/// It includes methods to add tips to transactions, create smart transactions with tips, and +/// send these smart transactions as bundles. Additionally, it provides the ability to check +/// the status of sent bundles +use crate::error::{HeliusError, Result}; +use crate::types::{ + BasicRequest, CreateSmartTransactionConfig, GetPriorityFeeEstimateOptions, GetPriorityFeeEstimateRequest, + GetPriorityFeeEstimateResponse, SmartTransaction, SmartTransactionConfig, +}; +use crate::Helius; + +use bincode::{serialize, ErrorKind}; +use chrono::format::parse; +use phf::phf_map; +use rand::seq::SliceRandom; +use reqwest::{Method, StatusCode, Url}; +use serde::Serialize; +use serde_json::Value; +use solana_client::rpc_config::{RpcSendTransactionConfig, RpcSimulateTransactionConfig}; +use solana_client::rpc_response::{Response, RpcSimulateTransactionResult}; +use solana_sdk::system_instruction; +use solana_sdk::{ + address_lookup_table::AddressLookupTableAccount, + bs58::encode, + commitment_config::CommitmentConfig, + compute_budget::ComputeBudgetInstruction, + hash::Hash, + instruction::Instruction, + message::{v0, VersionedMessage}, + pubkey::Pubkey, + signature::{Signature, Signer}, + transaction::{Transaction, VersionedTransaction}, +}; +use std::str::FromStr; +use std::time::{Duration, Instant}; +use tokio::time::{sleep, timeout_at}; + +/// Jito tip accounts +pub const JITO_TIP_ACCOUNTS: [&str; 8] = [ + "96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5", + "HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe", + "Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY", + "ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49", + "DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh", + "ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt", + "DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL", + "3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT", +]; + +/// Jito API URLs for different regions +pub static JITO_API_URLS: phf::Map<&'static str, &'static str> = phf_map! { + "Default" => "https://mainnet.block-engine.jito.wtf", + "NY" => "https://ny.mainnet.block-engine.jito.wtf", + "Amsterdam" => "https://amsterdam.mainnet.block-engine.jito.wtf", + "Frankfurt" => "https://frankfurt.mainnet.block-engine.jito.wtf", + "Tokyo" => "https://tokyo.mainnet.block-engine.jito.wtf", +}; + +/// Type alias for Jito regions +pub type JitoRegion = &'static str; + +impl Helius { + /// Adds a tip instruction to the provided instructions + /// + /// # Arguments + /// * `instructions` - The transaction instructions to which the tip_instruction will be added + /// * `fee_payer` - The public key of the fee payer + /// * `tip_account` - The public key of the tip account as a string + /// * `tip_amount` - The amount of lamports to tip + pub fn add_tip_instruction( + &self, + instructions: &mut Vec, + fee_payer: Pubkey, + tip_account: &str, + tip_amount: u64, + ) { + let tip_instruction: Instruction = + system_instruction::transfer(&fee_payer, &Pubkey::from_str(tip_account).unwrap(), tip_amount); + instructions.push(tip_instruction); + } + + /// Creates a smart transaction with a Jito tip + /// + /// # Arguments + /// * `config` - The configuration for creating the smart transaction + /// * `tip_amount` - The amount of lamports to tip. Defaults to `1000` + /// + /// # Returns + /// A `Result` containing the serialized transaction as a base58-encoded string and the last valid block height + pub async fn create_smart_transaction_with_tip( + &self, + mut config: CreateSmartTransactionConfig<'_>, + tip_amount: Option, + ) -> Result<(String, u64)> { + if config.signers.is_empty() { + return Err(HeliusError::InvalidInput( + "The transaction must have at least one signer".to_string(), + )); + } + + let tip_amount: u64 = tip_amount.unwrap_or(1000); + let random_tip_account: &str = *JITO_TIP_ACCOUNTS.choose(&mut rand::thread_rng()).unwrap(); + let payer_key: Pubkey = config + .fee_payer + .map_or_else(|| config.signers[0].pubkey(), |signer| signer.pubkey()); + + self.add_tip_instruction(&mut config.instructions, payer_key, random_tip_account, tip_amount); + + let (smart_transaction, last_valid_block_height) = self.create_smart_transaction(&config).await?; + let serialized_transaction: Vec = match smart_transaction { + SmartTransaction::Legacy(tx) => { + serialize(&tx).map_err(|e: Box| HeliusError::InvalidInput(e.to_string()))? + } + SmartTransaction::Versioned(tx) => { + serialize(&tx).map_err(|e: Box| HeliusError::InvalidInput(e.to_string()))? + } + }; + let transaction_base58: String = encode(&serialized_transaction).into_string(); + + Ok((transaction_base58, last_valid_block_height)) + } + + /// Sends a bundle of transactions to the Jito Block Engine + /// + /// # Arguments + /// * `serialized_transactions` - The serialized transactions in the bundle + /// * `jito_api_url` - The Jito Block Engine API URL + /// + /// # Returns + /// A `Result` containing the bundle ID + pub async fn send_jito_bundle(&self, serialized_transactions: Vec, jito_api_url: &str) -> Result { + let request: BasicRequest = BasicRequest { + jsonrpc: "2.0".to_string(), + id: 1, + method: "sendBundle".to_string(), + params: vec![serialized_transactions], + }; + + let parsed_url: Url = Url::parse(jito_api_url).expect("Failed to parse URL"); + + let response: Value = self + .rpc_client + .handler + .send(Method::POST, parsed_url, Some(&request)) + .await?; + + if let Some(error) = response.get("error") { + return Err(HeliusError::BadRequest { + path: jito_api_url.to_string(), + text: format!("Error sending bundles: {:?}", error), + }); + } + + if let Some(result) = response.get("result") { + if let Some(bundle_id) = result.as_str() { + return Ok(bundle_id.to_string()); + } + } + + Err(HeliusError::Unknown { + code: StatusCode::INTERNAL_SERVER_ERROR, + text: "Unexpected response format".to_string(), + }) + } + + /// Get the status of Jito bundles + /// + /// # Arguments + /// * `bundle_ids` - An array of bundle IDs to check the status for + /// * `jito_api_url` - The Jito Block Engine API URL + /// + /// # Returns + /// A `Result` containing the status of the bundles as a `serde_json::Value` + pub async fn get_bundle_statuses(&self, bundle_ids: Vec, jito_api_url: &str) -> Result { + let request: BasicRequest = BasicRequest { + jsonrpc: "2.0".to_string(), + id: 1, + method: "getBundleStatuses".to_string(), + params: vec![bundle_ids], + }; + + let parsed_url: Url = Url::parse(jito_api_url).expect("Failed to parse URL"); + + let response: Value = self + .rpc_client + .handler + .send(Method::POST, parsed_url, Some(&request)) + .await?; + + if let Some(error) = response.get("error") { + return Err(HeliusError::BadRequest { + path: jito_api_url.to_string(), + text: format!("Error getting bundle statuses: {:?}", error), + }); + } + + // Return the response value + Ok(response) + } + + /// Sends a smart transaction as a Jito bundle with a tip + /// + /// # Arguments + /// * `config` - The configuration for sending the smart transaction + /// * `tip_amount` - The amount of lamports tp tip. Defaults to `1000` + /// * `region` - The Jito Block Engine region. Defaults to `"Default"` + /// + /// # Returns + /// A `Result` containing the bundle IDc + pub async fn send_smart_transaction_with_tip( + &self, + config: SmartTransactionConfig<'_>, + tip_amount: Option, + region: Option, + ) -> Result { + if config.create_config.signers.is_empty() { + return Err(HeliusError::InvalidInput( + "The transaction must have at least one signer".to_string(), + )); + } + + let tip: u64 = tip_amount.unwrap_or(1000); + let user_provided_region: &str = region.unwrap_or("Default"); + let jito_region: &str = *JITO_API_URLS + .get(user_provided_region) + .ok_or_else(|| HeliusError::InvalidInput("Invalid Jito region".to_string()))?; + let jito_api_url_string: String = format!("{}/api/v1/bundles", jito_region); + let jito_api_url: &str = jito_api_url_string.as_str(); + + // Create the smart transaction with tip + let (serialized_transaction, last_valid_block_height) = self + .create_smart_transaction_with_tip(config.create_config, Some(tip)) + .await?; + + // Send the transaction as a Jito bundle + let bundle_id: String = self + .send_jito_bundle(vec![serialized_transaction], jito_api_url) + .await?; + + // Poll for confirmation status + let timeout: Duration = Duration::from_secs(60); + let interval: Duration = Duration::from_secs(5); + let start: tokio::time::Instant = tokio::time::Instant::now(); + + while start.elapsed() < timeout || self.connection().get_block_height()? <= last_valid_block_height { + let bundle_statuses: Value = self.get_bundle_statuses(vec![bundle_id.clone()], jito_api_url).await?; + + if let Some(values) = bundle_statuses["result"]["value"].as_array() { + if !values.is_empty() { + if let Some(status) = values[0]["confirmation_status"].as_str() { + if status == "confirmed" { + return Ok(values[0]["transactions"][0].as_str().unwrap().to_string()); + } + } + } + } + + sleep(interval).await; + } + + Err(HeliusError::Timeout { + code: StatusCode::REQUEST_TIMEOUT, + text: "Bundle failed to confirm within the timeout period".to_string(), + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index c75b226..e3dbbde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod config; pub mod enhanced_transactions; pub mod error; pub mod factory; +pub mod jito; pub mod mint_api; pub mod optimized_transaction; pub mod request_handler; diff --git a/src/optimized_transaction.rs b/src/optimized_transaction.rs index 265f723..53f3fd1 100644 --- a/src/optimized_transaction.rs +++ b/src/optimized_transaction.rs @@ -1,7 +1,7 @@ use crate::error::{HeliusError, Result}; use crate::types::{ - GetPriorityFeeEstimateOptions, GetPriorityFeeEstimateRequest, GetPriorityFeeEstimateResponse, SmartTransaction, - SmartTransactionConfig, + CreateSmartTransactionConfig, GetPriorityFeeEstimateOptions, GetPriorityFeeEstimateRequest, + GetPriorityFeeEstimateResponse, SmartTransaction, SmartTransactionConfig, }; use crate::Helius; @@ -117,16 +117,23 @@ impl Helius { /// whether it's a legacy or versioned smart transaction. The transaction's send configuration can also be changed, if provided /// /// # Returns - /// An optimized `Transaction` or `VersionedTransaction` - pub async fn create_smart_transaction(&self, config: &SmartTransactionConfig<'_>) -> Result { + /// An optimized `SmartTransaction` (i.e., `Transaction` or `VersionedTransaction`) and the `last_valid_block_height` + 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.signers[0].pubkey(); - let recent_blockhash: Hash = self.connection().get_latest_blockhash()?; + let payer_pubkey: Pubkey = config + .fee_payer + .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 = vec![]; // Check if any of the instructions provided set the compute unit price and/or limit, and throw an error if `true` @@ -153,9 +160,19 @@ impl Helius { v0::Message::try_compile(&payer_pubkey, &config.instructions, lookup_tables, recent_blockhash)?; let versioned_message: VersionedMessage = VersionedMessage::V0(v0_message); + let all_signers = if let Some(fee_payer) = config.fee_payer { + let mut all_signers: Vec<&dyn Signer> = config.signers.clone(); + if !all_signers.iter().any(|signer| signer.pubkey() == fee_payer.pubkey()) { + all_signers.push(fee_payer); + } + + all_signers + } else { + config.signers.clone() + }; + // Sign the versioned transaction - let signatures: Vec = config - .signers + let signatures: Vec = all_signers .iter() .map(|signer| signer.try_sign_message(versioned_message.serialize().as_slice())) .collect::, _>>()?; @@ -167,7 +184,12 @@ impl Helius { } else { // If no lookup tables are present, we build a regular transaction let mut tx: Transaction = Transaction::new_with_payer(&config.instructions, Some(&payer_pubkey)); - tx.try_sign(&config.signers, recent_blockhash)?; + tx.try_partial_sign(&config.signers, recent_blockhash)?; + + if let Some(fee_payer) = config.fee_payer { + tx.try_partial_sign(&[fee_payer], recent_blockhash)?; + } + legacy_transaction = Some(tx); } @@ -247,8 +269,18 @@ impl Helius { 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 signatures: Vec = config - .signers + + let all_signers: Vec<&dyn Signer> = if let Some(fee_payer) = config.fee_payer { + let mut all_signers = config.signers.clone(); + if !all_signers.iter().any(|signer| signer.pubkey() == fee_payer.pubkey()) { + all_signers.push(fee_payer); + } + all_signers + } else { + config.signers.clone() + }; + + let signatures: Vec = all_signers .iter() .map(|signer| signer.try_sign_message(versioned_message.serialize().as_slice())) .collect::, _>>()?; @@ -258,13 +290,24 @@ impl Helius { message: versioned_message, }); - Ok(SmartTransaction::Versioned(versioned_transaction.unwrap())) + Ok(( + SmartTransaction::Versioned(versioned_transaction.unwrap()), + last_valid_block_hash, + )) } else { let mut tx: Transaction = Transaction::new_with_payer(&final_instructions, Some(&payer_pubkey)); - tx.try_sign(&config.signers, recent_blockhash)?; + tx.try_partial_sign(&config.signers, recent_blockhash)?; + + if let Some(fee_payer) = config.fee_payer { + tx.try_partial_sign(&[fee_payer], recent_blockhash)?; + } + legacy_transaction = Some(tx); - Ok(SmartTransaction::Legacy(legacy_transaction.unwrap())) + Ok(( + SmartTransaction::Legacy(legacy_transaction.unwrap()), + last_valid_block_hash, + )) } } @@ -277,7 +320,7 @@ impl Helius { /// # Returns /// The transaction signature, if successful pub async fn send_smart_transaction(&self, config: SmartTransactionConfig<'_>) -> Result { - let transaction: SmartTransaction = self.create_smart_transaction(&config).await?; + let (transaction, last_valid_block_height) = self.create_smart_transaction(&config.create_config).await?; // Common logic for sending transactions let send_transaction_config: RpcSendTransactionConfig = RpcSendTransactionConfig { @@ -301,14 +344,24 @@ impl Helius { let timeout: Duration = Duration::from_secs(60); let start_time: Instant = Instant::now(); - while Instant::now().duration_since(start_time) < timeout { + while Instant::now().duration_since(start_time) < timeout + || self.connection().get_block_height()? <= last_valid_block_height + { let result = match &transaction { SmartTransaction::Legacy(tx) => send_result(tx), SmartTransaction::Versioned(tx) => send_versioned_result(tx), }; match result { - Ok(signature) => return self.poll_transaction_confirmation(signature).await, + Ok(signature) => { + // Poll for transaction confirmation + match self.poll_transaction_confirmation(signature).await { + Ok(sig) => return Ok(sig), + // Retry on polling failure + Err(_) => continue, + } + } + // Retry on send failure Err(_) => continue, } } diff --git a/src/request_handler.rs b/src/request_handler.rs index a540f2f..12466ba 100644 --- a/src/request_handler.rs +++ b/src/request_handler.rs @@ -1,6 +1,7 @@ use crate::error::{HeliusError, Result}; use reqwest::{Client, Method, RequestBuilder, Response, StatusCode, Url}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::fmt::Debug; use std::sync::Arc; @@ -100,10 +101,19 @@ impl RequestHandler { } } } else { - let body_json: serde_json::Result = serde_json::from_str(&body_text); + let body_json: serde_json::Result = serde_json::from_str(&body_text); match body_json { Ok(body) => { - let error_message: String = body["message"].as_str().unwrap_or("Unknown error").to_string(); + let error_message = match body["error"].clone() { + Value::Object(error_value) => error_value + .into_iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join(", ") + .to_string(), + Value::String(error_value) => error_value, + _ => "Unknown error".to_string(), + }; Err(HeliusError::from_response_status(status, path, error_message)) } Err(_) => Err(HeliusError::from_response_status(status, path, body_text)), diff --git a/src/types/types.rs b/src/types/types.rs index 291353d..a6aa909 100644 --- a/src/types/types.rs +++ b/src/types/types.rs @@ -411,7 +411,7 @@ pub struct TransferFeeConfig { pub withdraw_withheld_authority: String, pub withheld_amount: i32, pub older_transfer_fee: OlderTransferFee, - pub new_trasfer_fee: NewTransferFee, + pub new_transfer_fee: NewTransferFee, } #[derive(Serialize, Deserialize, Debug)] @@ -927,8 +927,9 @@ pub struct CreateCollectionWebhookRequest { } #[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] pub struct EditWebhookRequest { - #[serde(rename = "webhookID")] + #[serde(skip_serializing)] pub webhook_id: String, #[serde(rename = "webhookURL")] pub webhook_url: String, @@ -944,20 +945,42 @@ pub struct EditWebhookRequest { pub encoding: AccountWebhookEncoding, } -pub struct SmartTransactionConfig<'a> { +pub struct CreateSmartTransactionConfig<'a> { pub instructions: Vec, pub signers: Vec<&'a dyn Signer>, - pub send_options: RpcSendTransactionConfig, pub lookup_tables: Option>, + pub fee_payer: Option<&'a dyn Signer>, } -impl<'a> SmartTransactionConfig<'a> { +impl<'a> CreateSmartTransactionConfig<'a> { pub fn new(instructions: Vec, signers: Vec<&'a dyn Signer>) -> Self { Self { instructions, signers, - send_options: RpcSendTransactionConfig::default(), lookup_tables: None, + fee_payer: None, + } + } +} + +pub struct SmartTransactionConfig<'a> { + pub create_config: CreateSmartTransactionConfig<'a>, + pub send_options: RpcSendTransactionConfig, +} + +impl<'a> SmartTransactionConfig<'a> { + pub fn new(instructions: Vec, signers: Vec<&'a dyn Signer>) -> Self { + Self { + create_config: CreateSmartTransactionConfig::new(instructions, signers), + send_options: RpcSendTransactionConfig::default(), } } } + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct BasicRequest { + pub jsonrpc: String, + pub id: u32, + pub method: String, + pub params: Vec>, +} diff --git a/tests/test_request_handler.rs b/tests/test_request_handler.rs index d80ebbc..28f6e13 100644 --- a/tests/test_request_handler.rs +++ b/tests/test_request_handler.rs @@ -45,7 +45,7 @@ async fn test_bad_request_error() { .mock("GET", "/") .with_status(400) .with_header("content-type", "application/json") - .with_body(r#"{"message": "bad request"}"#) + .with_body(r#"{"error": "bad request"}"#) .create(); let client: Arc = Arc::new(Client::new()); @@ -63,3 +63,43 @@ async fn test_bad_request_error() { server.reset(); } + +#[tokio::test] +async fn test_bad_request_with_json_rpc_error() { + let mut server: Server = Server::new_with_opts_async(mockito::ServerOpts::default()).await; + let url: String = server.url(); + + server + .mock("GET", "/") + .with_status(400) + .with_header("content-type", "application/json") + .with_body( + r#" + { + "jsonrpc": "2.0", + "error": { + "code": -32603, + "message": "internal error: please contact Helius support if this persists" + } + }"#, + ) + .create(); + + let client: Arc = Arc::new(Client::new()); + let handler: RequestHandler = RequestHandler::new(client).unwrap(); + + let response: Result = handler + .send::<(), MockResponse>(Method::GET, url.parse().unwrap(), None) + .await; + + assert!(response.is_err()); + match response { + Err(HeliusError::BadRequest { text, .. }) => assert_eq!( + text, + "code: -32603, message: \"internal error: please contact Helius support if this persists\"" + ), + _ => panic!("Expected BadRequest error"), + } + + server.reset(); +}