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 address_lookup_tables_clone = address_lookup_tables.clone();
157
158 let rpc_config = &config.rpc_config;
159 let fee_config = &config.fee_config;
160 let budget_instructions = compute_budget::build_compute_budget_instructions(
161 rpc_client,
162 &instructions,
163 payer,
164 address_lookup_tables_clone,
165 &config.compute_config,
166 fee_config,
167 rpc_config,
168 config.min_context_slot,
169 )
170 .await?;
171
172 let recent_blockhash = rpc_client
173 .get_latest_blockhash()
174 .await
175 .map_err(|e| format!("RPC Error: {}", e))?;
176
177 for (i, budget_ix) in budget_instructions.into_iter().enumerate() {
178 instructions.insert(i, budget_ix);
179 }
180 if fee_config.jito != JitoFeeStrategy::Disabled {
182 if !rpc_config.is_mainnet() {
183 println!("Warning: Jito tips are only supported on mainnet. Skipping Jito tip.");
184 } else if let Some(jito_tip_ix) = jito::add_jito_tip_instruction(fee_config, payer).await? {
185 instructions.insert(0, jito_tip_ix);
186 }
187 }
188 let message = if let Some(address_lookup_tables_clone) = address_lookup_tables {
190 Message::try_compile(
191 payer,
192 &instructions,
193 &address_lookup_tables_clone,
194 recent_blockhash,
195 )
196 .map_err(|e| format!("Failed to compile message with ALTs: {}", e))?
197 } else {
198 Message::try_compile(payer, &instructions, &[], recent_blockhash)
199 .map_err(|e| format!("Failed to compile message: {}", e))?
200 };
201
202 Ok(VersionedTransaction {
204 signatures: vec![
205 solana_signature::Signature::default();
206 message.header.num_required_signatures.into()
207 ],
208 message: VersionedMessage::V0(message),
209 })
210}
211
212pub async fn build_transaction_with_config(
220 instructions: Vec<Instruction>,
221 payer: &Pubkey,
222 address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
223 rpc_client: &RpcClient,
224 rpc_config: &RpcConfig,
225 fee_config: &FeeConfig,
226) -> Result<VersionedTransaction, String> {
227 build_transaction_with_config_obj(
228 instructions,
229 payer,
230 address_lookup_tables,
231 rpc_client,
232 &BuildTransactionConfig::default()
233 .with_rpc_config((*rpc_config).clone())
234 .with_fee_config((*fee_config).clone()),
235 )
236 .await
237}
238
239pub async fn build_transaction(
247 instructions: Vec<Instruction>,
248 payer: &Pubkey,
249 address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
250) -> Result<VersionedTransaction, String> {
251 let config = config::get_global_config()
252 .read()
253 .map_err(|e| format!("Lock error: {}", e))?;
254 let rpc_client = config::get_rpc_client()?;
255 let rpc_config = config
256 .rpc_config
257 .as_ref()
258 .ok_or("RPC config not set".to_string())?;
259 let fee_config = &config.fee_config;
260 build_transaction_with_config(
261 instructions,
262 payer,
263 address_lookup_tables,
264 &rpc_client,
265 rpc_config,
266 fee_config,
267 )
268 .await
269}
270
271pub async fn send_transaction_with_config(
278 transaction: VersionedTransaction,
279 commitment: Option<CommitmentLevel>,
280 rpc_client: &RpcClient,
281) -> Result<Signature, String> {
282 let sim_result = rpc_client
283 .simulate_transaction(&transaction)
284 .await
285 .map_err(|e| format!("Transaction simulation failed: {}", e))?;
286
287 if let Some(err) = sim_result.value.err {
288 return Err(compute_budget::format_simulation_error(
289 err,
290 sim_result.value.logs,
291 ));
292 }
293
294 let commitment_level = commitment.unwrap_or(CommitmentLevel::Confirmed);
295 let expiry_time = Instant::now() + Duration::from_millis(90_000);
296 let mut retries = 0;
297 let signature = transaction.signatures[0];
298
299 while Instant::now() < expiry_time {
300 let status = rpc_client
302 .get_signature_status_with_commitment(
303 &signature,
304 CommitmentConfig {
305 commitment: commitment_level,
306 },
307 )
308 .await
309 .map_err(|e| format!("Failed to get signature status: {}", e))?;
310
311 match status {
312 Some(Ok(())) => {
314 return Ok(signature);
315 }
316 Some(Err(err)) => {
318 return Err(format!("Transaction failed: {}", err));
319 }
320 None => {
322 println!("sending {}...", signature);
324 match rpc_client
325 .send_transaction_with_config(
326 &transaction,
327 RpcSendTransactionConfig {
328 skip_preflight: true,
329 preflight_commitment: Some(commitment_level),
330 max_retries: Some(0), ..RpcSendTransactionConfig::default()
332 },
333 )
334 .await
335 {
336 Ok(_) => {
337 retries += 1;
338 }
339 Err(err) => {
340 println!("Transaction send failed (attempt {}): {}", retries, err);
341 }
342 }
343 }
344 }
345 sleep(Duration::from_secs(1)).await;
347 }
348
349 println!("Transaction send timeout: {}", signature);
350 Ok(signature)
351}
352
353pub async fn send_transaction(
360 transaction: VersionedTransaction,
361 commitment: Option<CommitmentLevel>,
362) -> Result<Signature, String> {
363 let rpc_client = config::get_rpc_client()?;
364 send_transaction_with_config(transaction, commitment, &rpc_client).await
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use crate::compute_budget;
371 use solana_keypair::{Keypair, Signer};
372 use solana_system_interface::instruction::transfer;
373
374 #[test]
375 fn test_get_writable_accounts() {
376 let keypair = Keypair::new();
377 let recipient = Keypair::new().pubkey();
378
379 let instructions = vec![transfer(&keypair.pubkey(), &recipient, 1_000_000)];
380
381 let writable_accounts = compute_budget::get_writable_accounts(&instructions);
382 assert_eq!(writable_accounts.len(), 2);
383 assert!(writable_accounts.contains(&keypair.pubkey()));
384 assert!(writable_accounts.contains(&recipient));
385 }
386
387 #[test]
388 fn test_fee_config_default() {
389 let config = FeeConfig::default();
390 assert_eq!(config.compute_unit_margin_multiplier, 1.1);
391 assert_eq!(config.jito_block_engine_url, "https://bundles.jito.wtf");
392 }
393
394 #[test]
395 fn test_build_transaction_config_has_no_minimum_context_slot_by_default() {
396 let config = BuildTransactionConfig::default();
397 assert_eq!(config.min_context_slot, None);
398 }
399
400 #[test]
401 fn test_build_transaction_config_builder_sets_minimum_context_slot() {
402 let config = BuildTransactionConfig::default().with_min_context_slot(123);
403 assert_eq!(config.min_context_slot, Some(123));
404 }
405
406 #[test]
407 fn test_format_simulation_error_includes_logs() {
408 let err = compute_budget::format_simulation_error(
409 "InstructionError",
410 Some(vec![
411 "Program Foo invoke [1]".to_string(),
412 "Program log: failed here".to_string(),
413 ]),
414 );
415
416 assert!(err.contains("Transaction simulation failed: InstructionError"));
417 assert!(err.contains("Simulation logs:"));
418 assert!(err.contains("Program log: failed here"));
419 }
420}