orca_tx_sender/
compute_budget.rs

1use crate::fee_config::{FeeConfig, Percentile, PriorityFeeStrategy};
2use crate::rpc_config::RpcConfig;
3use solana_client::nonblocking::rpc_client::RpcClient;
4use solana_client::rpc_config::RpcSimulateTransactionConfig;
5use solana_compute_budget_interface::ComputeBudgetInstruction;
6use solana_instruction::Instruction;
7use solana_message::AddressLookupTableAccount;
8use solana_message::{v0::Message, VersionedMessage};
9use solana_pubkey::Pubkey;
10use solana_rpc_client_api::response::RpcPrioritizationFee;
11use solana_transaction::versioned::VersionedTransaction;
12
13/// Compute unit limit strategy to apply when building a transaction.
14/// - Dynamic: Estimate compute units by simulating the transaction.
15///            If the simulation fails, the transaction will not build.
16/// - Exact: Directly use the provided compute unit limit.
17#[derive(Debug, Default)]
18pub enum ComputeUnitLimitStrategy {
19    #[default]
20    Dynamic,
21    Exact(u32),
22}
23
24#[derive(Debug, Default)]
25pub struct ComputeConfig {
26    pub unit_limit: ComputeUnitLimitStrategy,
27}
28
29/// Estimate compute units by simulating a transaction
30pub async fn estimate_compute_units(
31    rpc_client: &RpcClient,
32    instructions: &[Instruction],
33    payer: &Pubkey,
34    alts: Option<Vec<AddressLookupTableAccount>>,
35) -> Result<u32, String> {
36    let alt_accounts = alts.unwrap_or_default();
37    let blockhash = rpc_client
38        .get_latest_blockhash()
39        .await
40        .map_err(|e| format!("Failed to get recent blockhash: {}", e))?;
41
42    // Add max compute unit limit instruction so that the simulation does not fail
43    let mut simulation_instructions =
44        vec![ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)];
45    simulation_instructions.extend_from_slice(instructions);
46
47    let message = Message::try_compile(payer, &simulation_instructions, &alt_accounts, blockhash)
48        .map_err(|e| format!("Failed to compile message: {}", e))?;
49
50    let transaction = VersionedTransaction {
51        signatures: vec![
52            solana_signature::Signature::default();
53            message.header.num_required_signatures.into()
54        ],
55        message: VersionedMessage::V0(message),
56    };
57
58    let result = rpc_client
59        .simulate_transaction_with_config(
60            &transaction,
61            RpcSimulateTransactionConfig {
62                sig_verify: false,
63                replace_recent_blockhash: true,
64                ..Default::default()
65            },
66        )
67        .await;
68
69    match result {
70        Ok(simulation_result) => {
71            if let Some(err) = simulation_result.value.err {
72                return Err(format!("Transaction simulation failed: {}", err));
73            }
74            match simulation_result.value.units_consumed {
75                Some(units) => Ok(units as u32),
76                None => Err("Transaction simulation didn't return consumed units".to_string()),
77            }
78        }
79        Err(e) => Err(format!("Transaction simulation failed: {}", e)),
80    }
81}
82
83/// Calculate and return compute budget instructions for a transaction
84pub async fn get_compute_budget_instruction(
85    client: &RpcClient,
86    compute_units: u32,
87    _payer: &Pubkey,
88    rpc_config: &RpcConfig,
89    fee_config: &FeeConfig,
90    writable_accounts: &[Pubkey],
91) -> Result<Vec<Instruction>, String> {
92    let mut budget_instructions = Vec::new();
93    let compute_units_with_margin =
94        (compute_units as f64 * (fee_config.compute_unit_margin_multiplier)) as u32;
95
96    budget_instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(
97        compute_units_with_margin,
98    ));
99
100    match &fee_config.priority_fee {
101        PriorityFeeStrategy::Dynamic {
102            percentile,
103            max_lamports,
104        } => {
105            let fee =
106                calculate_dynamic_priority_fee(client, rpc_config, writable_accounts, *percentile)
107                    .await?;
108            let clamped_fee = std::cmp::min(fee, *max_lamports);
109
110            if clamped_fee > 0 {
111                budget_instructions.push(ComputeBudgetInstruction::set_compute_unit_price(
112                    clamped_fee,
113                ));
114            }
115        }
116        PriorityFeeStrategy::Exact(lamports) => {
117            if *lamports > 0 {
118                budget_instructions
119                    .push(ComputeBudgetInstruction::set_compute_unit_price(*lamports));
120            }
121        }
122        PriorityFeeStrategy::Disabled => {}
123    }
124
125    Ok(budget_instructions)
126}
127
128/// Calculate dynamic priority fee based on recent fees
129pub(crate) async fn calculate_dynamic_priority_fee(
130    client: &RpcClient,
131    rpc_config: &RpcConfig,
132    writable_accounts: &[Pubkey],
133    percentile: Percentile,
134) -> Result<u64, String> {
135    if rpc_config.supports_priority_fee_percentile {
136        get_priority_fee_with_percentile(client, writable_accounts, percentile).await
137    } else {
138        get_priority_fee_legacy(client, writable_accounts, percentile).await
139    }
140}
141
142/// Get priority fee using the getRecentPrioritizationFees endpoint with percentile parameter
143pub(crate) async fn get_priority_fee_with_percentile(
144    client: &RpcClient,
145    writable_accounts: &[Pubkey],
146    percentile: Percentile,
147) -> Result<u64, String> {
148    // This is a direct RPC call using reqwest since the Solana client doesn't support
149    // the percentile parameter yet
150    let rpc_url = client.url();
151
152    let response = reqwest::Client::new()
153        .post(rpc_url)
154        .json(&serde_json::json!({
155            "jsonrpc": "2.0",
156            "id": 1,
157            "method": "getRecentPrioritizationFees",
158            "params": [{
159                "lockedWritableAccounts": writable_accounts.iter().map(|p| p.to_string()).collect::<Vec<String>>(),
160                "percentile": percentile.as_value() * 100
161            }]
162        }))
163        .send()
164        .await
165        .map_err(|e| format!("RPC Error: {}", e))?;
166
167    #[derive(serde::Deserialize)]
168    struct Response {
169        result: RpcPrioritizationFee,
170    }
171
172    response
173        .json::<Response>()
174        .await
175        .map(|resp| resp.result.prioritization_fee)
176        .map_err(|e| format!("Failed to parse prioritization fee response: {}", e))
177}
178
179/// Get priority fee using the legacy getRecentPrioritizationFees endpoint
180pub(crate) async fn get_priority_fee_legacy(
181    client: &RpcClient,
182    writable_accounts: &[Pubkey],
183    percentile: Percentile,
184) -> Result<u64, String> {
185    // This uses the built-in method that returns Vec<RpcPrioritizationFee>
186    let recent_fees = client
187        .get_recent_prioritization_fees(writable_accounts)
188        .await
189        .map_err(|e| format!("RPC Error: {}", e))?;
190
191    // Filter out zero fees and sort
192    let mut non_zero_fees: Vec<u64> = recent_fees
193        .iter()
194        .filter(|fee| fee.prioritization_fee > 0)
195        .map(|fee| fee.prioritization_fee)
196        .collect();
197
198    non_zero_fees.sort_unstable();
199
200    if non_zero_fees.is_empty() {
201        return Ok(0);
202    }
203
204    // Calculate percentile
205    let index = (non_zero_fees.len() as f64 * (percentile.as_value() as f64 / 100.0)) as usize;
206    let index = std::cmp::min(index, non_zero_fees.len() - 1);
207
208    Ok(non_zero_fees[index])
209}
210
211/// Get writable accounts from a list of instructions
212pub fn get_writable_accounts(instructions: &[Instruction]) -> Vec<Pubkey> {
213    let mut writable = std::collections::HashSet::new();
214
215    for ix in instructions {
216        for meta in &ix.accounts {
217            if meta.is_writable {
218                writable.insert(meta.pubkey);
219            }
220        }
221    }
222
223    writable.into_iter().collect()
224}