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