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
24#[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
57pub 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 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
131pub 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
176pub(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
190pub(crate) async fn get_priority_fee_with_percentile(
192 client: &RpcClient,
193 writable_accounts: &[Pubkey],
194 percentile: Percentile,
195) -> Result<u64, String> {
196 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
227pub(crate) async fn get_priority_fee_legacy(
229 client: &RpcClient,
230 writable_accounts: &[Pubkey],
231 percentile: Percentile,
232) -> Result<u64, String> {
233 let recent_fees = client
235 .get_recent_prioritization_fees(writable_accounts)
236 .await
237 .map_err(|e| format!("RPC Error: {}", e))?;
238
239 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 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
259pub 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}