orca_tx_sender/
lib.rs

1mod compute_budget;
2mod config;
3mod fee_config;
4mod jito;
5mod rpc_config;
6mod signer;
7
8pub use compute_budget::*;
9pub use config::*;
10pub use fee_config::*;
11pub use jito::*;
12pub use rpc_config::*;
13pub use signer::*;
14
15use solana_client::nonblocking::rpc_client::RpcClient;
16
17/// Build and send a transaction using the supplied configuration
18///
19/// This function:
20/// 1. Builds an unsigned transaction with all necessary instructions
21/// 2. Signs the transaction with all provided signers
22/// 3. Sends the transaction and waits for confirmation
23/// 4. Optionally uses address lookup tables for account compression
24pub async fn build_and_send_transaction_with_config<S: Signer>(
25    instructions: Vec<Instruction>,
26    signers: &[&S],
27    commitment: Option<CommitmentLevel>,
28    address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
29    rpc_client: &RpcClient,
30    rpc_config: &RpcConfig,
31    fee_config: &FeeConfig,
32) -> Result<Signature, String> {
33    // Get the payer (first signer)
34    let payer = signers
35        .first()
36        .ok_or_else(|| "At least one signer is required".to_string())?;
37
38    // Build transaction with compute budget and priority fees
39    let mut tx = build_transaction_with_config(
40        instructions,
41        &payer.pubkey(),
42        address_lookup_tables,
43        rpc_client,
44        rpc_config,
45        fee_config,
46    )
47    .await?;
48    // Serialize the message once instead of for each signer
49    let serialized_message = tx.message.serialize();
50    tx.signatures = signers
51        .iter()
52        .map(|signer| signer.sign_message(&serialized_message))
53        .collect();
54    // Send with retry logic
55    send_transaction_with_config(tx, commitment, rpc_client).await
56}
57
58/// Build and send a transaction using the global configuration
59///
60/// This function:
61/// 1. Builds an unsigned transaction with all necessary instructions
62/// 2. Signs the transaction with all provided signers
63/// 3. Sends the transaction and waits for confirmation
64/// 4. Optionally uses address lookup tables for account compression
65pub async fn build_and_send_transaction<S: Signer>(
66    instructions: Vec<Instruction>,
67    signers: &[&S],
68    commitment: Option<CommitmentLevel>,
69    address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
70) -> Result<Signature, String> {
71    let config = config::get_global_config()
72        .read()
73        .map_err(|e| format!("Lock error: {}", e))?;
74    let rpc_client = config::get_rpc_client()?;
75    let rpc_config = config
76        .rpc_config
77        .as_ref()
78        .ok_or("RPC config not set".to_string())?;
79    let fee_config = &config.fee_config;
80    build_and_send_transaction_with_config(
81        instructions,
82        signers,
83        commitment,
84        address_lookup_tables,
85        &rpc_client,
86        rpc_config,
87        fee_config,
88    )
89    .await
90}
91
92#[derive(Debug, Default)]
93pub struct BuildTransactionConfig {
94    pub rpc_config: RpcConfig,
95    pub fee_config: FeeConfig,
96    pub compute_config: ComputeConfig,
97}
98
99/// Build a transaction with compute budget and priority fees from the supplied configuration
100///
101/// This function handles:
102/// 1. Building a transaction message with all instructions
103/// 2. Adding compute budget instructions
104/// 3. Adding any Jito tip instructions
105/// 4. Supporting address lookup tables for account compression
106pub async fn build_transaction_with_config_obj(
107    mut instructions: Vec<Instruction>,
108    payer: &Pubkey,
109    address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
110    rpc_client: &RpcClient,
111    config: &BuildTransactionConfig,
112) -> Result<VersionedTransaction, String> {
113    let recent_blockhash = rpc_client
114        .get_latest_blockhash()
115        .await
116        .map_err(|e| format!("RPC Error: {}", e))?;
117
118    let writable_accounts = compute_budget::get_writable_accounts(&instructions);
119
120    let address_lookup_tables_clone = address_lookup_tables.clone();
121
122    let compute_units = match config.compute_config.unit_limit {
123        ComputeUnitLimitStrategy::Dynamic => {
124            compute_budget::estimate_compute_units(
125                rpc_client,
126                &instructions,
127                payer,
128                address_lookup_tables_clone,
129            )
130            .await?
131        }
132        ComputeUnitLimitStrategy::Exact(units) => units,
133    };
134
135    let rpc_config = &config.rpc_config;
136    let fee_config = &config.fee_config;
137    let budget_instructions = compute_budget::get_compute_budget_instruction(
138        rpc_client,
139        compute_units,
140        payer,
141        rpc_config,
142        fee_config,
143        &writable_accounts,
144    )
145    .await?;
146    for (i, budget_ix) in budget_instructions.into_iter().enumerate() {
147        instructions.insert(i, budget_ix);
148    }
149    // Check if network is mainnet before adding Jito tip
150    if fee_config.jito != JitoFeeStrategy::Disabled {
151        if !rpc_config.is_mainnet() {
152            println!("Warning: Jito tips are only supported on mainnet. Skipping Jito tip.");
153        } else if let Some(jito_tip_ix) = jito::add_jito_tip_instruction(fee_config, payer).await? {
154            instructions.insert(0, jito_tip_ix);
155        }
156    }
157    // Create versioned transaction message based on whether ALTs are provided
158    let message = if let Some(address_lookup_tables_clone) = address_lookup_tables {
159        Message::try_compile(
160            payer,
161            &instructions,
162            &address_lookup_tables_clone,
163            recent_blockhash,
164        )
165        .map_err(|e| format!("Failed to compile message with ALTs: {}", e))?
166    } else {
167        Message::try_compile(payer, &instructions, &[], recent_blockhash)
168            .map_err(|e| format!("Failed to compile message: {}", e))?
169    };
170
171    // Provide the correct number of signatures for the transaction, otherwise (de)serialization can fail
172    Ok(VersionedTransaction {
173        signatures: vec![
174            solana_signature::Signature::default();
175            message.header.num_required_signatures.into()
176        ],
177        message: VersionedMessage::V0(message),
178    })
179}
180
181/// Build a transaction with compute budget and priority fees from the supplied configuration
182///
183/// This function handles:
184/// 1. Building a transaction message with all instructions
185/// 2. Adding compute budget instructions
186/// 3. Adding any Jito tip instructions
187/// 4. Supporting address lookup tables for account compression
188pub async fn build_transaction_with_config(
189    instructions: Vec<Instruction>,
190    payer: &Pubkey,
191    address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
192    rpc_client: &RpcClient,
193    rpc_config: &RpcConfig,
194    fee_config: &FeeConfig,
195) -> Result<VersionedTransaction, String> {
196    build_transaction_with_config_obj(
197        instructions,
198        payer,
199        address_lookup_tables,
200        rpc_client,
201        &BuildTransactionConfig {
202            rpc_config: (*rpc_config).clone(),
203            fee_config: (*fee_config).clone(),
204            ..Default::default()
205        },
206    )
207    .await
208}
209
210/// Build a transaction with compute budget and priority fees from the global configuration
211///
212/// This function handles:
213/// 1. Building a transaction message with all instructions
214/// 2. Adding compute budget instructions
215/// 3. Adding any Jito tip instructions
216/// 4. Supporting address lookup tables for account compression
217pub async fn build_transaction(
218    instructions: Vec<Instruction>,
219    payer: &Pubkey,
220    address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
221) -> Result<VersionedTransaction, String> {
222    let config = config::get_global_config()
223        .read()
224        .map_err(|e| format!("Lock error: {}", e))?;
225    let rpc_client = config::get_rpc_client()?;
226    let rpc_config = config
227        .rpc_config
228        .as_ref()
229        .ok_or("RPC config not set".to_string())?;
230    let fee_config = &config.fee_config;
231    build_transaction_with_config(
232        instructions,
233        payer,
234        address_lookup_tables,
235        &rpc_client,
236        rpc_config,
237        fee_config,
238    )
239    .await
240}
241
242/// Send a transaction with retry logic using the supplied configuration
243///
244/// This function handles:
245/// 1. Sending the transaction to the network
246/// 2. Implementing retry logic with exponential backoff
247/// 3. Waiting for transaction confirmation
248pub async fn send_transaction_with_config(
249    transaction: VersionedTransaction,
250    commitment: Option<CommitmentLevel>,
251    rpc_client: &RpcClient,
252) -> Result<Signature, String> {
253    let sim_result = rpc_client
254        .simulate_transaction(&transaction)
255        .await
256        .map_err(|e| format!("Transaction simulation failed: {}", e))?;
257
258    if let Some(err) = sim_result.value.err {
259        return Err(compute_budget::format_simulation_error(
260            err,
261            sim_result.value.logs,
262        ));
263    }
264
265    let commitment_level = commitment.unwrap_or(CommitmentLevel::Confirmed);
266    let expiry_time = Instant::now() + Duration::from_millis(90_000);
267    let mut retries = 0;
268    let signature = transaction.signatures[0];
269
270    while Instant::now() < expiry_time {
271        // Check if the transaction has been confirmed
272        let status = rpc_client
273            .get_signature_status_with_commitment(
274                &signature,
275                CommitmentConfig {
276                    commitment: commitment_level,
277                },
278            )
279            .await
280            .map_err(|e| format!("Failed to get signature status: {}", e))?;
281
282        match status {
283            // Transaction confirmed
284            Some(Ok(())) => {
285                return Ok(signature);
286            }
287            // Transaction failed
288            Some(Err(err)) => {
289                return Err(format!("Transaction failed: {}", err));
290            }
291            // Transaction not found or still processing
292            None => {
293                // Try to send the transaction if not found
294                println!("sending {}...", signature);
295                match rpc_client
296                    .send_transaction_with_config(
297                        &transaction,
298                        RpcSendTransactionConfig {
299                            skip_preflight: true,
300                            preflight_commitment: Some(commitment_level),
301                            max_retries: Some(0), // We handle retries ourselves
302                            ..RpcSendTransactionConfig::default()
303                        },
304                    )
305                    .await
306                {
307                    Ok(_) => {
308                        retries += 1;
309                    }
310                    Err(err) => {
311                        println!("Transaction send failed (attempt {}): {}", retries, err);
312                    }
313                }
314            }
315        }
316        // Always wait 1 second between loop iterations
317        sleep(Duration::from_secs(1)).await;
318    }
319
320    println!("Transaction send timeout: {}", signature);
321    Ok(signature)
322}
323
324/// Send a transaction with retry logic using the global configuration
325///
326/// This function handles:
327/// 1. Sending the transaction to the network
328/// 2. Implementing retry logic with exponential backoff
329/// 3. Waiting for transaction confirmation
330pub async fn send_transaction(
331    transaction: VersionedTransaction,
332    commitment: Option<CommitmentLevel>,
333) -> Result<Signature, String> {
334    let rpc_client = config::get_rpc_client()?;
335    send_transaction_with_config(transaction, commitment, &rpc_client).await
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341    use crate::compute_budget;
342    use solana_keypair::{Keypair, Signer};
343    use solana_system_interface::instruction::transfer;
344
345    #[test]
346    fn test_get_writable_accounts() {
347        let keypair = Keypair::new();
348        let recipient = Keypair::new().pubkey();
349
350        let instructions = vec![transfer(&keypair.pubkey(), &recipient, 1_000_000)];
351
352        let writable_accounts = compute_budget::get_writable_accounts(&instructions);
353        assert_eq!(writable_accounts.len(), 2);
354        assert!(writable_accounts.contains(&keypair.pubkey()));
355        assert!(writable_accounts.contains(&recipient));
356    }
357
358    #[test]
359    fn test_fee_config_default() {
360        let config = FeeConfig::default();
361        assert_eq!(config.compute_unit_margin_multiplier, 1.1);
362        assert_eq!(config.jito_block_engine_url, "https://bundles.jito.wtf");
363    }
364
365    #[test]
366    fn test_format_simulation_error_includes_logs() {
367        let err = compute_budget::format_simulation_error(
368            "InstructionError",
369            Some(vec![
370                "Program Foo invoke [1]".to_string(),
371                "Program log: failed here".to_string(),
372            ]),
373        );
374
375        assert!(err.contains("Transaction simulation failed: InstructionError"));
376        assert!(err.contains("Simulation logs:"));
377        assert!(err.contains("Program log: failed here"));
378    }
379}