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#[derive(Debug, Default)]
18pub enum ComputeUnitLimitStrategy {
19 #[default]
20 Dynamic,
21 Exact(u32),
22}
23
24pub const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000;
25
26pub(crate) fn apply_compute_unit_margin(compute_units: u32, multiplier: f64) -> u32 {
27 ((compute_units as f64 * multiplier) as u32).min(MAX_COMPUTE_UNIT_LIMIT)
28}
29
30pub(crate) fn validate_compute_unit_limit_strategy(
31 strategy: &ComputeUnitLimitStrategy,
32) -> Result<(), String> {
33 if let ComputeUnitLimitStrategy::Exact(units) = strategy {
34 if !(1..=MAX_COMPUTE_UNIT_LIMIT).contains(units) {
35 return Err(format!(
36 "Exact compute unit limit must be between 1 and 1,400,000; received {units}"
37 ));
38 }
39 }
40 Ok(())
41}
42
43#[non_exhaustive]
48#[derive(Debug, Default)]
49pub struct ComputeConfig {
50 pub unit_limit: ComputeUnitLimitStrategy,
51}
52
53impl ComputeConfig {
54 pub fn with_unit_limit(mut self, unit_limit: ComputeUnitLimitStrategy) -> Self {
55 self.unit_limit = unit_limit;
56 self
57 }
58}
59
60pub(crate) fn format_simulation_error(
61 err: impl std::fmt::Display,
62 logs: Option<Vec<String>>,
63) -> String {
64 let mut message = format!("Transaction simulation failed: {}", err);
65
66 if let Some(logs) = logs {
67 if !logs.is_empty() {
68 message.push_str("\nSimulation logs:\n");
69 message.push_str(&logs.join("\n"));
70 }
71 }
72
73 message
74}
75
76pub async fn estimate_compute_units(
78 rpc_client: &RpcClient,
79 instructions: &[Instruction],
80 payer: &Pubkey,
81 alts: Option<Vec<AddressLookupTableAccount>>,
82) -> Result<u32, String> {
83 estimate_compute_units_at_commitment(rpc_client, instructions, payer, alts, None, None).await
84}
85
86fn simulation_config(
87 commitment: Option<solana_commitment_config::CommitmentConfig>,
88 min_context_slot: Option<u64>,
89) -> RpcSimulateTransactionConfig {
90 RpcSimulateTransactionConfig {
91 sig_verify: false,
92 replace_recent_blockhash: true,
93 commitment,
94 min_context_slot,
95 ..Default::default()
96 }
97}
98
99pub(crate) async fn estimate_compute_units_at_commitment(
100 rpc_client: &RpcClient,
101 instructions: &[Instruction],
102 payer: &Pubkey,
103 alts: Option<Vec<AddressLookupTableAccount>>,
104 commitment: Option<solana_commitment_config::CommitmentConfig>,
105 min_context_slot: Option<u64>,
106) -> Result<u32, String> {
107 let alt_accounts = alts.unwrap_or_default();
108 let blockhash = rpc_client
109 .get_latest_blockhash()
110 .await
111 .map_err(|e| format!("Failed to get recent blockhash: {}", e))?;
112
113 let mut simulation_instructions =
115 vec![ComputeBudgetInstruction::set_compute_unit_limit(1_400_000)];
116 simulation_instructions.extend_from_slice(instructions);
117
118 let message = Message::try_compile(payer, &simulation_instructions, &alt_accounts, blockhash)
119 .map_err(|e| format!("Failed to compile message: {}", e))?;
120
121 let transaction = VersionedTransaction {
122 signatures: vec![
123 solana_signature::Signature::default();
124 message.header.num_required_signatures.into()
125 ],
126 message: VersionedMessage::V0(message),
127 };
128
129 let result = rpc_client
130 .simulate_transaction_with_config(
131 &transaction,
132 simulation_config(commitment, min_context_slot),
133 )
134 .await;
135
136 match result {
137 Ok(simulation_result) => {
138 if let Some(err) = simulation_result.value.err {
139 return Err(format_simulation_error(err, simulation_result.value.logs));
140 }
141 match simulation_result.value.units_consumed {
142 Some(units) => Ok(units as u32),
143 None => Err("Transaction simulation didn't return consumed units".to_string()),
144 }
145 }
146 Err(e) => Err(format!("Transaction simulation failed: {}", e)),
147 }
148}
149
150pub(crate) async fn build_compute_budget_instructions(
151 rpc_client: &RpcClient,
152 instructions: &[Instruction],
153 payer: &Pubkey,
154 address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
155 compute_config: &ComputeConfig,
156 fee_config: &FeeConfig,
157 rpc_config: &RpcConfig,
158 min_context_slot: Option<u64>,
159) -> Result<Vec<Instruction>, String> {
160 let writable_accounts = get_writable_accounts(instructions);
161
162 let compute_units = match &compute_config.unit_limit {
163 ComputeUnitLimitStrategy::Dynamic => {
164 let estimated_compute_units = estimate_compute_units_at_commitment(
165 rpc_client,
166 instructions,
167 payer,
168 address_lookup_tables,
169 Some(rpc_client.commitment()),
170 min_context_slot,
171 )
172 .await?;
173
174 apply_compute_unit_margin(
175 estimated_compute_units,
176 fee_config.compute_unit_margin_multiplier,
177 )
178 }
179 ComputeUnitLimitStrategy::Exact(units) => {
180 validate_compute_unit_limit_strategy(&compute_config.unit_limit)?;
181 *units
182 }
183 };
184
185 get_compute_budget_instruction(
186 rpc_client,
187 compute_units,
188 payer,
189 rpc_config,
190 fee_config,
191 &writable_accounts,
192 )
193 .await
194}
195
196pub async fn get_compute_budget_instruction(
198 client: &RpcClient,
199 compute_units: u32,
200 _payer: &Pubkey,
201 rpc_config: &RpcConfig,
202 fee_config: &FeeConfig,
203 writable_accounts: &[Pubkey],
204) -> Result<Vec<Instruction>, String> {
205 let mut budget_instructions = Vec::new();
206 budget_instructions.push(ComputeBudgetInstruction::set_compute_unit_limit(
207 compute_units,
208 ));
209
210 match &fee_config.priority_fee {
211 PriorityFeeStrategy::Dynamic {
212 percentile,
213 max_lamports,
214 } => {
215 let fee =
216 calculate_dynamic_priority_fee(client, rpc_config, writable_accounts, *percentile)
217 .await?;
218 let clamped_fee = std::cmp::min(fee, *max_lamports);
219
220 if clamped_fee > 0 {
221 budget_instructions.push(ComputeBudgetInstruction::set_compute_unit_price(
222 clamped_fee,
223 ));
224 }
225 }
226 PriorityFeeStrategy::Exact(lamports) => {
227 if *lamports > 0 {
228 budget_instructions
229 .push(ComputeBudgetInstruction::set_compute_unit_price(*lamports));
230 }
231 }
232 PriorityFeeStrategy::Disabled => {}
233 }
234
235 Ok(budget_instructions)
236}
237
238pub(crate) async fn calculate_dynamic_priority_fee(
240 client: &RpcClient,
241 rpc_config: &RpcConfig,
242 writable_accounts: &[Pubkey],
243 percentile: Percentile,
244) -> Result<u64, String> {
245 if rpc_config.supports_priority_fee_percentile {
246 get_priority_fee_with_percentile(client, writable_accounts, percentile).await
247 } else {
248 get_priority_fee_legacy(client, writable_accounts, percentile).await
249 }
250}
251
252pub(crate) async fn get_priority_fee_with_percentile(
254 client: &RpcClient,
255 writable_accounts: &[Pubkey],
256 percentile: Percentile,
257) -> Result<u64, String> {
258 let rpc_url = client.url();
261
262 let response = reqwest::Client::new()
263 .post(rpc_url)
264 .json(&serde_json::json!({
265 "jsonrpc": "2.0",
266 "id": 1,
267 "method": "getRecentPrioritizationFees",
268 "params": [{
269 "lockedWritableAccounts": writable_accounts.iter().map(|p| p.to_string()).collect::<Vec<String>>(),
270 "percentile": percentile.as_value() * 100
271 }]
272 }))
273 .send()
274 .await
275 .map_err(|e| format!("RPC Error: {}", e))?;
276
277 #[derive(serde::Deserialize)]
278 struct Response {
279 result: RpcPrioritizationFee,
280 }
281
282 response
283 .json::<Response>()
284 .await
285 .map(|resp| resp.result.prioritization_fee)
286 .map_err(|e| format!("Failed to parse prioritization fee response: {}", e))
287}
288
289pub(crate) async fn get_priority_fee_legacy(
291 client: &RpcClient,
292 writable_accounts: &[Pubkey],
293 percentile: Percentile,
294) -> Result<u64, String> {
295 let recent_fees = client
297 .get_recent_prioritization_fees(writable_accounts)
298 .await
299 .map_err(|e| format!("RPC Error: {}", e))?;
300
301 let mut non_zero_fees: Vec<u64> = recent_fees
303 .iter()
304 .filter(|fee| fee.prioritization_fee > 0)
305 .map(|fee| fee.prioritization_fee)
306 .collect();
307
308 non_zero_fees.sort_unstable();
309
310 if non_zero_fees.is_empty() {
311 return Ok(0);
312 }
313
314 let index = (non_zero_fees.len() as f64 * (percentile.as_value() as f64 / 100.0)) as usize;
316 let index = std::cmp::min(index, non_zero_fees.len() - 1);
317
318 Ok(non_zero_fees[index])
319}
320
321pub fn get_writable_accounts(instructions: &[Instruction]) -> Vec<Pubkey> {
323 let mut writable = std::collections::HashSet::new();
324
325 for ix in instructions {
326 for meta in &ix.accounts {
327 if meta.is_writable {
328 writable.insert(meta.pubkey);
329 }
330 }
331 }
332
333 writable.into_iter().collect()
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 use solana_commitment_config::CommitmentConfig;
340
341 #[test]
342 fn simulation_config_preserves_commitment_and_min_context_slot() {
343 let config = simulation_config(Some(CommitmentConfig::confirmed()), Some(123));
344
345 assert_eq!(config.commitment, Some(CommitmentConfig::confirmed()));
346 assert_eq!(config.min_context_slot, Some(123));
347 assert!(!config.sig_verify);
348 assert!(config.replace_recent_blockhash);
349 }
350
351 #[test]
352 fn builder_sets_compute_unit_limit() {
353 let config = ComputeConfig::default().with_unit_limit(ComputeUnitLimitStrategy::Exact(321));
354 assert!(matches!(
355 config.unit_limit,
356 ComputeUnitLimitStrategy::Exact(321)
357 ));
358 }
359
360 #[test]
361 fn dynamic_compute_unit_limit_applies_margin_and_clamps() {
362 assert_eq!(apply_compute_unit_margin(100_000, 1.1), 110_000);
363 assert_eq!(apply_compute_unit_margin(1_350_000, 1.1), 1_400_000);
364 }
365
366 #[tokio::test]
367 async fn exact_compute_unit_limit_is_used_unchanged() {
368 let rpc_client = RpcClient::new("http://127.0.0.1:1".to_string());
369 let compute_config =
370 ComputeConfig::default().with_unit_limit(ComputeUnitLimitStrategy::Exact(200_000));
371 let fee_config = FeeConfig::default();
372 let rpc_config = RpcConfig::default();
373
374 let instructions = build_compute_budget_instructions(
375 &rpc_client,
376 &[],
377 &Pubkey::default(),
378 None,
379 &compute_config,
380 &fee_config,
381 &rpc_config,
382 None,
383 )
384 .await
385 .unwrap();
386
387 assert_eq!(instructions.len(), 1);
388 assert_eq!(
389 u32::from_le_bytes(instructions[0].data[1..5].try_into().unwrap()),
390 200_000
391 );
392 }
393
394 #[tokio::test]
395 async fn invalid_exact_compute_unit_limits_are_rejected() {
396 let rpc_client = RpcClient::new("http://127.0.0.1:1".to_string());
397 let too_large_config =
398 ComputeConfig::default().with_unit_limit(ComputeUnitLimitStrategy::Exact(1_400_001));
399 let fee_config = FeeConfig::default();
400 let rpc_config = RpcConfig::default();
401 let too_large = build_compute_budget_instructions(
402 &rpc_client,
403 &[],
404 &Pubkey::default(),
405 None,
406 &too_large_config,
407 &fee_config,
408 &rpc_config,
409 None,
410 )
411 .await
412 .unwrap_err();
413 assert!(too_large.contains("between 1 and 1,400,000"));
414
415 let zero_config =
416 ComputeConfig::default().with_unit_limit(ComputeUnitLimitStrategy::Exact(0));
417 let zero = build_compute_budget_instructions(
418 &rpc_client,
419 &[],
420 &Pubkey::default(),
421 None,
422 &zero_config,
423 &fee_config,
424 &rpc_config,
425 None,
426 )
427 .await
428 .unwrap_err();
429 assert!(zero.contains("between 1 and 1,400,000"));
430 }
431}