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#[derive(Debug, Default)]
93pub struct BuildTransactionConfig {
94 pub rpc_config: RpcConfig,
95 pub fee_config: FeeConfig,
96 pub compute_config: ComputeConfig,
97}
98
99pub async fn build_transaction_with_config_obj(
107 mut instructions: Vec<Instruction>,
108 payer: &Pubkey,
109 address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
110 rpc_client: &RpcClient,
111 config: &BuildTransactionConfig,
112) -> Result<VersionedTransaction, String> {
113 let recent_blockhash = rpc_client
114 .get_latest_blockhash()
115 .await
116 .map_err(|e| format!("RPC Error: {}", e))?;
117
118 let writable_accounts = compute_budget::get_writable_accounts(&instructions);
119
120 let address_lookup_tables_clone = address_lookup_tables.clone();
121
122 let compute_units = match config.compute_config.unit_limit {
123 ComputeUnitLimitStrategy::Dynamic => {
124 compute_budget::estimate_compute_units(
125 rpc_client,
126 &instructions,
127 payer,
128 address_lookup_tables_clone,
129 )
130 .await?
131 }
132 ComputeUnitLimitStrategy::Exact(units) => units,
133 };
134
135 let rpc_config = &config.rpc_config;
136 let fee_config = &config.fee_config;
137 let budget_instructions = compute_budget::get_compute_budget_instruction(
138 rpc_client,
139 compute_units,
140 payer,
141 rpc_config,
142 fee_config,
143 &writable_accounts,
144 )
145 .await?;
146 for (i, budget_ix) in budget_instructions.into_iter().enumerate() {
147 instructions.insert(i, budget_ix);
148 }
149 if fee_config.jito != JitoFeeStrategy::Disabled {
151 if !rpc_config.is_mainnet() {
152 println!("Warning: Jito tips are only supported on mainnet. Skipping Jito tip.");
153 } else if let Some(jito_tip_ix) = jito::add_jito_tip_instruction(fee_config, payer).await? {
154 instructions.insert(0, jito_tip_ix);
155 }
156 }
157 let message = if let Some(address_lookup_tables_clone) = address_lookup_tables {
159 Message::try_compile(
160 payer,
161 &instructions,
162 &address_lookup_tables_clone,
163 recent_blockhash,
164 )
165 .map_err(|e| format!("Failed to compile message with ALTs: {}", e))?
166 } else {
167 Message::try_compile(payer, &instructions, &[], recent_blockhash)
168 .map_err(|e| format!("Failed to compile message: {}", e))?
169 };
170
171 Ok(VersionedTransaction {
173 signatures: vec![
174 solana_signature::Signature::default();
175 message.header.num_required_signatures.into()
176 ],
177 message: VersionedMessage::V0(message),
178 })
179}
180
181pub async fn build_transaction_with_config(
189 instructions: Vec<Instruction>,
190 payer: &Pubkey,
191 address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
192 rpc_client: &RpcClient,
193 rpc_config: &RpcConfig,
194 fee_config: &FeeConfig,
195) -> Result<VersionedTransaction, String> {
196 build_transaction_with_config_obj(
197 instructions,
198 payer,
199 address_lookup_tables,
200 rpc_client,
201 &BuildTransactionConfig {
202 rpc_config: (*rpc_config).clone(),
203 fee_config: (*fee_config).clone(),
204 ..Default::default()
205 },
206 )
207 .await
208}
209
210pub async fn build_transaction(
218 instructions: Vec<Instruction>,
219 payer: &Pubkey,
220 address_lookup_tables: Option<Vec<AddressLookupTableAccount>>,
221) -> Result<VersionedTransaction, String> {
222 let config = config::get_global_config()
223 .read()
224 .map_err(|e| format!("Lock error: {}", e))?;
225 let rpc_client = config::get_rpc_client()?;
226 let rpc_config = config
227 .rpc_config
228 .as_ref()
229 .ok_or("RPC config not set".to_string())?;
230 let fee_config = &config.fee_config;
231 build_transaction_with_config(
232 instructions,
233 payer,
234 address_lookup_tables,
235 &rpc_client,
236 rpc_config,
237 fee_config,
238 )
239 .await
240}
241
242pub async fn send_transaction_with_config(
249 transaction: VersionedTransaction,
250 commitment: Option<CommitmentLevel>,
251 rpc_client: &RpcClient,
252) -> Result<Signature, String> {
253 let sim_result = rpc_client
254 .simulate_transaction(&transaction)
255 .await
256 .map_err(|e| format!("Transaction simulation failed: {}", e))?;
257
258 if let Some(err) = sim_result.value.err {
259 return Err(compute_budget::format_simulation_error(
260 err,
261 sim_result.value.logs,
262 ));
263 }
264
265 let commitment_level = commitment.unwrap_or(CommitmentLevel::Confirmed);
266 let expiry_time = Instant::now() + Duration::from_millis(90_000);
267 let mut retries = 0;
268 let signature = transaction.signatures[0];
269
270 while Instant::now() < expiry_time {
271 let status = rpc_client
273 .get_signature_status_with_commitment(
274 &signature,
275 CommitmentConfig {
276 commitment: commitment_level,
277 },
278 )
279 .await
280 .map_err(|e| format!("Failed to get signature status: {}", e))?;
281
282 match status {
283 Some(Ok(())) => {
285 return Ok(signature);
286 }
287 Some(Err(err)) => {
289 return Err(format!("Transaction failed: {}", err));
290 }
291 None => {
293 println!("sending {}...", signature);
295 match rpc_client
296 .send_transaction_with_config(
297 &transaction,
298 RpcSendTransactionConfig {
299 skip_preflight: true,
300 preflight_commitment: Some(commitment_level),
301 max_retries: Some(0), ..RpcSendTransactionConfig::default()
303 },
304 )
305 .await
306 {
307 Ok(_) => {
308 retries += 1;
309 }
310 Err(err) => {
311 println!("Transaction send failed (attempt {}): {}", retries, err);
312 }
313 }
314 }
315 }
316 sleep(Duration::from_secs(1)).await;
318 }
319
320 println!("Transaction send timeout: {}", signature);
321 Ok(signature)
322}
323
324pub async fn send_transaction(
331 transaction: VersionedTransaction,
332 commitment: Option<CommitmentLevel>,
333) -> Result<Signature, String> {
334 let rpc_client = config::get_rpc_client()?;
335 send_transaction_with_config(transaction, commitment, &rpc_client).await
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::compute_budget;
342 use solana_keypair::{Keypair, Signer};
343 use solana_system_interface::instruction::transfer;
344
345 #[test]
346 fn test_get_writable_accounts() {
347 let keypair = Keypair::new();
348 let recipient = Keypair::new().pubkey();
349
350 let instructions = vec![transfer(&keypair.pubkey(), &recipient, 1_000_000)];
351
352 let writable_accounts = compute_budget::get_writable_accounts(&instructions);
353 assert_eq!(writable_accounts.len(), 2);
354 assert!(writable_accounts.contains(&keypair.pubkey()));
355 assert!(writable_accounts.contains(&recipient));
356 }
357
358 #[test]
359 fn test_fee_config_default() {
360 let config = FeeConfig::default();
361 assert_eq!(config.compute_unit_margin_multiplier, 1.1);
362 assert_eq!(config.jito_block_engine_url, "https://bundles.jito.wtf");
363 }
364
365 #[test]
366 fn test_format_simulation_error_includes_logs() {
367 let err = compute_budget::format_simulation_error(
368 "InstructionError",
369 Some(vec![
370 "Program Foo invoke [1]".to_string(),
371 "Program log: failed here".to_string(),
372 ]),
373 );
374
375 assert!(err.contains("Transaction simulation failed: InstructionError"));
376 assert!(err.contains("Simulation logs:"));
377 assert!(err.contains("Program log: failed here"));
378 }
379}