orca_tx_sender/
compute_budget.rs1use 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#[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 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 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
83pub 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
128pub(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
142pub(crate) async fn get_priority_fee_with_percentile(
144 client: &RpcClient,
145 writable_accounts: &[Pubkey],
146 percentile: Percentile,
147) -> Result<u64, String> {
148 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
179pub(crate) async fn get_priority_fee_legacy(
181 client: &RpcClient,
182 writable_accounts: &[Pubkey],
183 percentile: Percentile,
184) -> Result<u64, String> {
185 let recent_fees = client
187 .get_recent_prioritization_fees(writable_accounts)
188 .await
189 .map_err(|e| format!("RPC Error: {}", e))?;
190
191 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 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
211pub 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}