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(format!("Transaction simulation failed: {}", err));
260 }
261
262 let commitment_level = commitment.unwrap_or(CommitmentLevel::Confirmed);
263 let expiry_time = Instant::now() + Duration::from_millis(90_000);
264 let mut retries = 0;
265 let signature = transaction.signatures[0];
266
267 while Instant::now() < expiry_time {
268 let status = rpc_client
270 .get_signature_status_with_commitment(
271 &signature,
272 CommitmentConfig {
273 commitment: commitment_level,
274 },
275 )
276 .await
277 .map_err(|e| format!("Failed to get signature status: {}", e))?;
278
279 match status {
280 Some(Ok(())) => {
282 return Ok(signature);
283 }
284 Some(Err(err)) => {
286 return Err(format!("Transaction failed: {}", err));
287 }
288 None => {
290 println!("sending {}...", signature);
292 match rpc_client
293 .send_transaction_with_config(
294 &transaction,
295 RpcSendTransactionConfig {
296 skip_preflight: true,
297 preflight_commitment: Some(commitment_level),
298 max_retries: Some(0), ..RpcSendTransactionConfig::default()
300 },
301 )
302 .await
303 {
304 Ok(_) => {
305 retries += 1;
306 }
307 Err(err) => {
308 println!("Transaction send failed (attempt {}): {}", retries, err);
309 }
310 }
311 }
312 }
313 sleep(Duration::from_secs(1)).await;
315 }
316
317 println!("Transaction send timeout: {}", signature);
318 Ok(signature)
319}
320
321pub async fn send_transaction(
328 transaction: VersionedTransaction,
329 commitment: Option<CommitmentLevel>,
330) -> Result<Signature, String> {
331 let rpc_client = config::get_rpc_client()?;
332 send_transaction_with_config(transaction, commitment, &rpc_client).await
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use crate::compute_budget;
339 use solana_keypair::{Keypair, Signer};
340 use solana_system_interface::instruction::transfer;
341
342 #[test]
343 fn test_get_writable_accounts() {
344 let keypair = Keypair::new();
345 let recipient = Keypair::new().pubkey();
346
347 let instructions = vec![transfer(&keypair.pubkey(), &recipient, 1_000_000)];
348
349 let writable_accounts = compute_budget::get_writable_accounts(&instructions);
350 assert_eq!(writable_accounts.len(), 2);
351 assert!(writable_accounts.contains(&keypair.pubkey()));
352 assert!(writable_accounts.contains(&recipient));
353 }
354
355 #[test]
356 fn test_fee_config_default() {
357 let config = FeeConfig::default();
358 assert_eq!(config.compute_unit_margin_multiplier, 1.1);
359 assert_eq!(config.jito_block_engine_url, "https://bundles.jito.wtf");
360 }
361}