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