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(format!("Transaction simulation failed: {}", err));
260    }
261
262    let commitment_level = commitment.unwrap_or(CommitmentLevel::Confirmed);
263    let expiry_time = Instant::now() + Duration::from_millis(90_000);
264    let mut retries = 0;
265    let signature = transaction.signatures[0];
266
267    while Instant::now() < expiry_time {
268        // Check if the transaction has been confirmed
269        let status = rpc_client
270            .get_signature_status_with_commitment(
271                &signature,
272                CommitmentConfig {
273                    commitment: commitment_level,
274                },
275            )
276            .await
277            .map_err(|e| format!("Failed to get signature status: {}", e))?;
278
279        match status {
280            // Transaction confirmed
281            Some(Ok(())) => {
282                return Ok(signature);
283            }
284            // Transaction failed
285            Some(Err(err)) => {
286                return Err(format!("Transaction failed: {}", err));
287            }
288            // Transaction not found or still processing
289            None => {
290                // Try to send the transaction if not found
291                println!("sending {}...", signature);
292                match rpc_client
293                    .send_transaction_with_config(
294                        &transaction,
295                        RpcSendTransactionConfig {
296                            skip_preflight: true,
297                            preflight_commitment: Some(commitment_level),
298                            max_retries: Some(0), // We handle retries ourselves
299                            ..RpcSendTransactionConfig::default()
300                        },
301                    )
302                    .await
303                {
304                    Ok(_) => {
305                        retries += 1;
306                    }
307                    Err(err) => {
308                        println!("Transaction send failed (attempt {}): {}", retries, err);
309                    }
310                }
311            }
312        }
313        // Always wait 1 second between loop iterations
314        sleep(Duration::from_secs(1)).await;
315    }
316
317    println!("Transaction send timeout: {}", signature);
318    Ok(signature)
319}
320
321/// Send a transaction with retry logic using the global configuration
322///
323/// This function handles:
324/// 1. Sending the transaction to the network
325/// 2. Implementing retry logic with exponential backoff
326/// 3. Waiting for transaction confirmation
327pub async fn send_transaction(
328    transaction: VersionedTransaction,
329    commitment: Option<CommitmentLevel>,
330) -> Result<Signature, String> {
331    let rpc_client = config::get_rpc_client()?;
332    send_transaction_with_config(transaction, commitment, &rpc_client).await
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use crate::compute_budget;
339    use solana_keypair::{Keypair, Signer};
340    use solana_system_interface::instruction::transfer;
341
342    #[test]
343    fn test_get_writable_accounts() {
344        let keypair = Keypair::new();
345        let recipient = Keypair::new().pubkey();
346
347        let instructions = vec![transfer(&keypair.pubkey(), &recipient, 1_000_000)];
348
349        let writable_accounts = compute_budget::get_writable_accounts(&instructions);
350        assert_eq!(writable_accounts.len(), 2);
351        assert!(writable_accounts.contains(&keypair.pubkey()));
352        assert!(writable_accounts.contains(&recipient));
353    }
354
355    #[test]
356    fn test_fee_config_default() {
357        let config = FeeConfig::default();
358        assert_eq!(config.compute_unit_margin_multiplier, 1.1);
359        assert_eq!(config.jito_block_engine_url, "https://bundles.jito.wtf");
360    }
361}