Created
September 21, 2025 03:42
-
-
Save benjiqq/d8af43ae3d4a18e6f507baac557d7f91 to your computer and use it in GitHub Desktop.
txconfirm
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use anyhow::{anyhow, Result}; | |
| use log::{error, info, warn}; | |
| use serde::{Deserialize, Serialize}; | |
| use solana_client::rpc_client::RpcClient; | |
| use solana_sdk::{ | |
| commitment_config::CommitmentConfig, signature::Signature, transaction::TransactionError, | |
| }; | |
| use std::time::Duration; | |
| use tokio::time::sleep; | |
| // Define commitment level constant | |
| #[allow(dead_code)] | |
| pub const COMMITMENT_LEVEL: CommitmentConfig = CommitmentConfig::confirmed(); | |
| // Error codes and texts | |
| #[allow(dead_code)] | |
| pub const ERR_1001: &str = "Unknown instruction error"; | |
| #[allow(dead_code)] | |
| pub const ERR_1002: &str = "Provided owner is not allowed"; | |
| #[allow(dead_code)] | |
| pub const ERR_1003: &str = "custom program error: insufficient funds"; | |
| #[allow(dead_code)] | |
| pub const ERR_1011: &str = "Not known Error"; | |
| pub const ERR_2006: &str = "custom program error: 2006 | A seeds constraint was violated"; | |
| pub const ERR_3007: &str = | |
| "custom program error: 3007 | The given account is owned by a different program than expected"; | |
| pub const ERR_3012: &str = | |
| "custom program error: 3012 | The program expected this account to be already initialized"; | |
| pub const ERR_6002: &str = "slippage: Too much SOL required to buy the given amount of tokens."; | |
| pub const ERR_6003: &str = "slippage: Too little SOL received to sell the given amount of tokens."; | |
| pub const ERR_6005: &str = "custom program error: 6005 | The bonding curve has completed and liquidity migrated to raydium"; | |
| //Program Error: "Insufficient Funds For Rent: A2WBBtwKEQduapSSmdeu4oYB1GxHewXHNs5YNNDkPhjB" | |
| // Helper function for sleeping | |
| async fn sleep_ms(ms: u64) { | |
| sleep(Duration::from_millis(ms)).await; | |
| } | |
| // Transaction confirmation result | |
| #[derive(Debug)] | |
| pub struct TransactionConfirmation { | |
| pub success: bool, | |
| #[allow(dead_code)] | |
| pub txsig: String, | |
| #[allow(dead_code)] | |
| pub confirmation_time: Option<chrono::DateTime<chrono::Utc>>, | |
| pub error: Option<String>, | |
| pub error_code: Option<u32>, | |
| } | |
| // Status poller for transaction confirmation | |
| pub async fn get_status_txn_retry( | |
| client: &RpcClient, | |
| txsig: &str, | |
| max_retries: u32, | |
| retry_sleep: u64, | |
| ) -> Result<TransactionConfirmation> { | |
| info!("try get_status_txn {txsig} (max {max_retries}, every {retry_sleep} ms)"); | |
| for retry in 0..max_retries { | |
| match client.get_signature_statuses(&[txsig.parse()?]) { | |
| Ok(signature_statuses) => { | |
| if let Some(status) = signature_statuses.value.first() { | |
| if let Some(status) = status { | |
| if status.err.is_none() { | |
| info!("Transaction confirmed {txsig}"); | |
| // Get confirmation time from block time | |
| let mut confirmation_time = None; | |
| let slot = status.slot; | |
| if slot > 0 { | |
| match client.get_block_time(slot) { | |
| Ok(block_time) => { | |
| confirmation_time = Some( | |
| chrono::DateTime::from_timestamp(block_time, 0) | |
| .unwrap_or_default(), | |
| ); | |
| info!("Block time for slot {slot}: {confirmation_time:?}"); | |
| } | |
| Err(e) => { | |
| warn!("Could not fetch block time for slot {slot}: {e}"); | |
| } | |
| } | |
| } | |
| return Ok(TransactionConfirmation { | |
| success: true, | |
| txsig: txsig.to_string(), | |
| confirmation_time, | |
| error: None, | |
| error_code: None, | |
| }); | |
| } | |
| // Decode InstructionError | |
| if let Some(err) = &status.err { | |
| match err { | |
| TransactionError::InstructionError(instruction_index, instruction_error) => { | |
| match instruction_error { | |
| solana_sdk::instruction::InstructionError::Custom(custom_code) => { | |
| let code_u32: u32 = *custom_code; | |
| let (error_msg, error_code): (String, u32) = match code_u32 { | |
| 1u32 => (ERR_1003.to_string(), 1003u32), | |
| 2006u32 => (ERR_2006.to_string(), 2006u32), | |
| 3007u32 => (ERR_3007.to_string(), 3007u32), | |
| 3012u32 => (ERR_3012.to_string(), 3012u32), | |
| 6002u32 => (ERR_6002.to_string(), 6002u32), | |
| 6003u32 => (ERR_6003.to_string(), 6003u32), | |
| 6005u32 => (ERR_6005.to_string(), 6005u32), | |
| _ => (format!( | |
| "Instruction #{} failed with custom program error: {}", | |
| instruction_index, code_u32 | |
| ), code_u32), | |
| }; | |
| error!("Custom error result: {error_msg} (code: {error_code})"); | |
| return Ok(TransactionConfirmation { | |
| success: false, | |
| txsig: txsig.to_string(), | |
| confirmation_time: None, | |
| error: Some(error_msg), | |
| error_code: Some(error_code), | |
| }); | |
| } | |
| solana_sdk::instruction::InstructionError::InvalidAccountOwner => { | |
| return Ok(TransactionConfirmation { | |
| success: false, | |
| txsig: txsig.to_string(), | |
| confirmation_time: None, | |
| error: Some(ERR_1002.to_string()), | |
| error_code: Some(1002), | |
| }); | |
| } | |
| other => { | |
| let full_msg = format!( | |
| "Instruction #{} failed: {:?}", | |
| instruction_index, other | |
| ); | |
| error!("{full_msg}"); | |
| return Ok(TransactionConfirmation { | |
| success: false, | |
| txsig: txsig.to_string(), | |
| confirmation_time: None, | |
| error: Some(full_msg), | |
| error_code: Some(1011), | |
| }); | |
| } | |
| } | |
| } | |
| _ => { | |
| error!("Unexpected tx error: {err:?}"); | |
| return Ok(TransactionConfirmation { | |
| success: false, | |
| txsig: txsig.to_string(), | |
| confirmation_time: None, | |
| error: Some(format!("Unexpected tx error: {err:?}")), | |
| error_code: Some(1001), | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Err(e) => { | |
| error!("confirmation attempt {retry}: {e}"); | |
| } | |
| } | |
| sleep_ms(retry_sleep).await; | |
| } | |
| error!("Max retries reached. Tx confirmation failed {txsig}"); | |
| Ok(TransactionConfirmation { | |
| success: false, | |
| txsig: txsig.to_string(), | |
| confirmation_time: None, | |
| error: Some("could not confirm in time".to_string()), | |
| error_code: Some(1003), | |
| }) | |
| } | |
| // Get transaction via RPC | |
| #[allow(dead_code)] | |
| pub async fn get_tx(url: &str, txsig: &str) -> Result<Option<serde_json::Value>> { | |
| let body = serde_json::json!({ | |
| "jsonrpc": "2.0", | |
| "id": 1, | |
| "method": "getTransaction", | |
| "params": [ | |
| txsig, | |
| { | |
| "commitment": "confirmed", | |
| "maxSupportedTransactionVersion": 0, | |
| "encoding": "json", | |
| }, | |
| ], | |
| }); | |
| let client = reqwest::Client::new(); | |
| let response = | |
| client.post(url).header("Content-Type", "application/json").json(&body).send().await?; | |
| if !response.status().is_success() { | |
| return Err(anyhow!("HTTP error! status: {}", response.status())); | |
| } | |
| let data: serde_json::Value = response.json().await?; | |
| Ok(data.get("result").cloned()) | |
| } | |
| // Transaction result structure | |
| #[derive(Debug, Serialize, Deserialize)] | |
| pub struct TransactionResult { | |
| pub signature: String, | |
| pub token_amount: Option<f64>, | |
| pub token_sol_price: Option<f64>, | |
| pub token_usd_price: Option<f64>, | |
| pub transaction_fee: Option<f64>, | |
| pub net_buy_sol_amount: Option<f64>, | |
| pub execution_price: Option<f64>, | |
| pub error: Option<String>, | |
| } | |
| // Parse transaction details (simplified version) | |
| pub async fn parse_transaction( | |
| signature: &Signature, | |
| _token_address: &str, | |
| _wallet_public_key: &str, | |
| _client: &RpcClient, | |
| ) -> Result<TransactionResult> { | |
| // This is a placeholder implementation | |
| // The actual transaction parsing would need to be implemented based on the specific requirements | |
| Ok(TransactionResult { | |
| signature: signature.to_string(), | |
| token_amount: None, | |
| token_sol_price: None, | |
| token_usd_price: None, | |
| transaction_fee: None, | |
| net_buy_sol_amount: None, | |
| execution_price: None, | |
| error: Some("Transaction parsing not yet implemented".to_string()), | |
| }) | |
| } | |
| // Test function to demonstrate the transaction confirmation functionality | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| #[tokio::test] | |
| async fn test_transaction_confirmation_structure() { | |
| let confirmation = TransactionConfirmation { | |
| success: true, | |
| txsig: "test_signature".to_string(), | |
| confirmation_time: None, | |
| error: None, | |
| error_code: None, | |
| }; | |
| assert!(confirmation.success); | |
| assert_eq!(confirmation.txsig, "test_signature"); | |
| assert!(confirmation.error.is_none()); | |
| assert!(confirmation.error_code.is_none()); | |
| } | |
| #[tokio::test] | |
| async fn test_transaction_result_structure() { | |
| let result = TransactionResult { | |
| signature: "test_signature".to_string(), | |
| token_amount: Some(100.0), | |
| token_sol_price: Some(0.1), | |
| token_usd_price: Some(10.0), | |
| transaction_fee: Some(0.000005), | |
| net_buy_sol_amount: Some(0.1), | |
| execution_price: Some(0.1), | |
| error: None, | |
| }; | |
| assert_eq!(result.signature, "test_signature"); | |
| assert_eq!(result.token_amount, Some(100.0)); | |
| assert_eq!(result.token_sol_price, Some(0.1)); | |
| assert_eq!(result.token_usd_price, Some(10.0)); | |
| assert_eq!(result.transaction_fee, Some(0.000005)); | |
| assert_eq!(result.net_buy_sol_amount, Some(0.1)); | |
| assert_eq!(result.execution_price, Some(0.1)); | |
| assert!(result.error.is_none()); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment