1mod compute_budget;
2mod config;
3mod fee_config;
4mod jito;
5mod rpc_config;
6mod signer;
7
8pub use compute_budget::*;
9pub use config::*;
10pub use fee_config::*;
11pub use jito::*;
12pub use rpc_config::*;
13pub use signer::*;
14
15use solana_client::nonblocking::rpc_client::RpcClient;
16
17pub async fn build_and_send_transaction_with_config<S: Signer>(
25 instructions: Vec<Instruction>,
26 signers: &[&S],
27 commitment: Option<CommitmentLevel>,
28 address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
29 rpc_client: &RpcClient,
30 rpc_config: &RpcConfig,
31 fee_config: &FeeConfig,
32) -> Result<Signature, String> {
33 let payer = signers
35 .first()
36 .ok_or_else(|| "At least one signer is required".to_string())?;
37
38 let mut tx = build_transaction_with_config(
40 instructions,
41 &payer.pubkey(),
42 address_lookup_tables,
43 rpc_client,
44 rpc_config,
45 fee_config,
46 )
47 .await?;
48 let serialized_message = tx.message.serialize();
50 tx.signatures = signers
51 .iter()
52 .map(|signer| signer.sign_message(&serialized_message))
53 .collect();
54 send_transaction_with_config(tx, commitment, rpc_client).await
56}
57
58pub async fn build_and_send_transaction<S: Signer>(
66 instructions: Vec<Instruction>,
67 signers: &[&S],
68 commitment: Option<CommitmentLevel>,
69 address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
70) -> Result<Signature, String> {
71 let config = config::get_global_config()
72 .read()
73 .map_err(|e| format!("Lock error: {}", e))?;
74 let rpc_client = config::get_rpc_client()?;
75 let rpc_config = config
76 .rpc_config
77 .as_ref()
78 .ok_or("RPC config not set".to_string())?;
79 let fee_config = &config.fee_config;
80 build_and_send_transaction_with_config(
81 instructions,
82 signers,
83 commitment,
84 address_lookup_tables,
85 &rpc_client,
86 rpc_config,
87 fee_config,
88 )
89 .await
90}
91
92#[non_exhaustive]
105#[derive(Debug, Default)]
106pub struct BuildTransactionConfig {
107 pub rpc_config: RpcConfig,
108 pub fee_config: FeeConfig,
109 pub compute_config: ComputeConfig,
110 pub min_context_slot: Option<u64>,
116}
117
118impl BuildTransactionConfig {
119 pub fn with_rpc_config(mut self, rpc_config: RpcConfig) -> Self {
120 self.rpc_config = rpc_config;
121 self
122 }
123
124 pub fn with_fee_config(mut self, fee_config: FeeConfig) -> Self {
125 self.fee_config = fee_config;
126 self
127 }
128
129 pub fn with_compute_config(mut self, compute_config: ComputeConfig) -> Self {
130 self.compute_config = compute_config;
131 self
132 }
133
134 pub fn with_min_context_slot(mut self, min_context_slot: u64) -> Self {
137 self.min_context_slot = Some(min_context_slot);
138 self
139 }
140}
141
142pub async fn build_transaction_with_config_obj(
150 mut instructions: Vec<Instruction>,
151 payer: &Pubkey,
152 address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
153 rpc_client: &RpcClient,
154 config: &BuildTransactionConfig,
155) -> Result<VersionedTransaction, String> {
156 let recent_blockhash = rpc_client
157 .get_latest_blockhash()
158 .await
159 .map_err(|e| format!("RPC Error: {}", e))?;
160
161 let writable_accounts = compute_budget::get_writable_accounts(&instructions);
162
163 let address_lookup_tables_clone = address_lookup_tables.clone();
164
165 let compute_units = match config.compute_config.unit_limit {
166 ComputeUnitLimitStrategy::Dynamic => {
167 compute_budget::estimate_compute_units_at_commitment(
168 rpc_client,
169 &instructions,
170 payer,
171 address_lookup_tables_clone,
172 Some(rpc_client.commitment()),
173 config.min_context_slot,
174 )
175 .await?
176 }
177 ComputeUnitLimitStrategy::Exact(units) => units,
178 };
179
180 let rpc_config = &config.rpc_config;
181 let fee_config = &config.fee_config;
182 let budget_instructions = compute_budget::get_compute_budget_instruction(
183 rpc_client,
184 compute_units,
185 payer,
186 rpc_config,
187 fee_config,
188 &writable_accounts,
189 )
190 .await?;
191 for (i, budget_ix) in budget_instructions.into_iter().enumerate() {
192 instructions.insert(i, budget_ix);
193 }
194 if fee_config.jito != JitoFeeStrategy::Disabled {
196 if !rpc_config.is_mainnet() {
197 println!("Warning: Jito tips are only supported on mainnet. Skipping Jito tip.");
198 } else if let Some(jito_tip_ix) = jito::add_jito_tip_instruction(fee_config, payer).await? {
199 instructions.insert(0, jito_tip_ix);
200 }
201 }
202 let message = if let Some(address_lookup_tables_clone) = address_lookup_tables {
204 Message::try_compile(
205 payer,
206 &instructions,
207 &address_lookup_tables_clone,
208 recent_blockhash,
209 )
210 .map_err(|e| format!("Failed to compile message with ALTs: {}", e))?
211 } else {
212 Message::try_compile(payer, &instructions, &[], recent_blockhash)
213 .map_err(|e| format!("Failed to compile message: {}", e))?
214 };
215
216 Ok(VersionedTransaction {
218 signatures: vec![
219 solana_signature::Signature::default();
220 message.header.num_required_signatures.into()
221 ],
222 message: VersionedMessage::V0(message),
223 })
224}
225
226pub async fn build_transaction_with_config(
234 instructions: Vec<Instruction>,
235 payer: &Pubkey,
236 address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
237 rpc_client: &RpcClient,
238 rpc_config: &RpcConfig,
239 fee_config: &FeeConfig,
240) -> Result<VersionedTransaction, String> {
241 build_transaction_with_config_obj(
242 instructions,
243 payer,
244 address_lookup_tables,
245 rpc_client,
246 &BuildTransactionConfig::default()
247 .with_rpc_config((*rpc_config).clone())
248 .with_fee_config((*fee_config).clone()),
249 )
250 .await
251}
252
253pub async fn build_transaction(
261 instructions: Vec<Instruction>,
262 payer: &Pubkey,
263 address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
264) -> Result<VersionedTransaction, String> {
265 let config = config::get_global_config()
266 .read()
267 .map_err(|e| format!("Lock error: {}", e))?;
268 let rpc_client = config::get_rpc_client()?;
269 let rpc_config = config
270 .rpc_config
271 .as_ref()
272 .ok_or("RPC config not set".to_string())?;
273 let fee_config = &config.fee_config;
274 build_transaction_with_config(
275 instructions,
276 payer,
277 address_lookup_tables,
278 &rpc_client,
279 rpc_config,
280 fee_config,
281 )
282 .await
283}
284
285pub async fn send_transaction_with_config(
292 transaction: VersionedTransaction,
293 commitment: Option<CommitmentLevel>,
294 rpc_client: &RpcClient,
295) -> Result<Signature, String> {
296 let sim_result = rpc_client
297 .simulate_transaction(&transaction)
298 .await
299 .map_err(|e| format!("Transaction simulation failed: {}", e))?;
300
301 if let Some(err) = sim_result.value.err {
302 return Err(compute_budget::format_simulation_error(
303 err,
304 sim_result.value.logs,
305 ));
306 }
307
308 let commitment_level = commitment.unwrap_or(CommitmentLevel::Confirmed);
309 let expiry_time = Instant::now() + Duration::from_millis(90_000);
310 let mut retries = 0;
311 let signature = transaction.signatures[0];
312
313 while Instant::now() < expiry_time {
314 let status = rpc_client
316 .get_signature_status_with_commitment(
317 &signature,
318 CommitmentConfig {
319 commitment: commitment_level,
320 },
321 )
322 .await
323 .map_err(|e| format!("Failed to get signature status: {}", e))?;
324
325 match status {
326 Some(Ok(())) => {
328 return Ok(signature);
329 }
330 Some(Err(err)) => {
332 return Err(format!("Transaction failed: {}", err));
333 }
334 None => {
336 println!("sending {}...", signature);
338 match rpc_client
339 .send_transaction_with_config(
340 &transaction,
341 RpcSendTransactionConfig {
342 skip_preflight: true,
343 preflight_commitment: Some(commitment_level),
344 max_retries: Some(0), ..RpcSendTransactionConfig::default()
346 },
347 )
348 .await
349 {
350 Ok(_) => {
351 retries += 1;
352 }
353 Err(err) => {
354 println!("Transaction send failed (attempt {}): {}", retries, err);
355 }
356 }
357 }
358 }
359 sleep(Duration::from_secs(1)).await;
361 }
362
363 println!("Transaction send timeout: {}", signature);
364 Ok(signature)
365}
366
367pub async fn send_transaction(
374 transaction: VersionedTransaction,
375 commitment: Option<CommitmentLevel>,
376) -> Result<Signature, String> {
377 let rpc_client = config::get_rpc_client()?;
378 send_transaction_with_config(transaction, commitment, &rpc_client).await
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use crate::compute_budget;
385 use solana_keypair::{Keypair, Signer};
386 use solana_system_interface::instruction::transfer;
387
388 #[test]
389 fn test_get_writable_accounts() {
390 let keypair = Keypair::new();
391 let recipient = Keypair::new().pubkey();
392
393 let instructions = vec![transfer(&keypair.pubkey(), &recipient, 1_000_000)];
394
395 let writable_accounts = compute_budget::get_writable_accounts(&instructions);
396 assert_eq!(writable_accounts.len(), 2);
397 assert!(writable_accounts.contains(&keypair.pubkey()));
398 assert!(writable_accounts.contains(&recipient));
399 }
400
401 #[test]
402 fn test_fee_config_default() {
403 let config = FeeConfig::default();
404 assert_eq!(config.compute_unit_margin_multiplier, 1.1);
405 assert_eq!(config.jito_block_engine_url, "https://bundles.jito.wtf");
406 }
407
408 #[test]
409 fn test_build_transaction_config_has_no_minimum_context_slot_by_default() {
410 let config = BuildTransactionConfig::default();
411 assert_eq!(config.min_context_slot, None);
412 }
413
414 #[test]
415 fn test_build_transaction_config_builder_sets_minimum_context_slot() {
416 let config = BuildTransactionConfig::default().with_min_context_slot(123);
417 assert_eq!(config.min_context_slot, Some(123));
418 }
419
420 #[test]
421 fn test_format_simulation_error_includes_logs() {
422 let err = compute_budget::format_simulation_error(
423 "InstructionError",
424 Some(vec![
425 "Program Foo invoke [1]".to_string(),
426 "Program log: failed here".to_string(),
427 ]),
428 );
429
430 assert!(err.contains("Transaction simulation failed: InstructionError"));
431 assert!(err.contains("Simulation logs:"));
432 assert!(err.contains("Program log: failed here"));
433 }
434}