1use std::sync::Arc;
11
12use super::{DexStrategy, SwapParams, SwapResult};
13use crate::domain::relayer::RelayerError;
14use crate::models::{EncodedSerializedTransaction, JupiterSwapOptions};
15use crate::services::{
16 provider::{SolanaProvider, SolanaProviderError, SolanaProviderTrait},
17 signer::{SolanaSignTrait, SolanaSigner},
18 JupiterService, JupiterServiceTrait, PrioritizationFeeLamports, PriorityLevelWitMaxLamports,
19 QuoteRequest, SwapRequest,
20};
21use async_trait::async_trait;
22use solana_sdk::transaction::VersionedTransaction;
23use tracing::debug;
24
25pub struct JupiterSwapDex<P, S, J>
26where
27 P: SolanaProviderTrait + 'static,
28 S: SolanaSignTrait + 'static,
29 J: JupiterServiceTrait + 'static,
30{
31 provider: Arc<P>,
32 signer: Arc<S>,
33 jupiter_service: Arc<J>,
34 jupiter_swap_options: Option<JupiterSwapOptions>,
35}
36
37pub type DefaultJupiterSwapDex = JupiterSwapDex<SolanaProvider, SolanaSigner, JupiterService>;
38
39impl<P, S, J> JupiterSwapDex<P, S, J>
40where
41 P: SolanaProviderTrait + 'static,
42 S: SolanaSignTrait + 'static,
43 J: JupiterServiceTrait + 'static,
44{
45 pub fn new(
46 provider: Arc<P>,
47 signer: Arc<S>,
48 jupiter_service: Arc<J>,
49 jupiter_swap_options: Option<JupiterSwapOptions>,
50 ) -> Self {
51 Self {
52 provider,
53 signer,
54 jupiter_service,
55 jupiter_swap_options,
56 }
57 }
58}
59
60#[async_trait]
61impl<P, S, J> DexStrategy for JupiterSwapDex<P, S, J>
62where
63 P: SolanaProviderTrait + Send + Sync + 'static,
64 S: SolanaSignTrait + Send + Sync + 'static,
65 J: JupiterServiceTrait + Send + Sync + 'static,
66{
67 async fn execute_swap(&self, params: SwapParams) -> Result<SwapResult, RelayerError> {
68 debug!(params = ?params, "executing Jupiter swap");
69
70 let quote = self
71 .jupiter_service
72 .get_quote(QuoteRequest {
73 input_mint: params.source_mint.clone(),
74 output_mint: params.destination_mint.clone(),
75 amount: params.amount,
76 slippage: params.slippage_percent as f32,
77 })
78 .await
79 .map_err(|e| RelayerError::DexError(format!("Failed to get Jupiter quote: {e}")))?;
80 debug!(quote = ?quote, "received quote");
81
82 let swap_tx = self
83 .jupiter_service
84 .get_swap_transaction(SwapRequest {
85 quote_response: quote.clone(),
86 user_public_key: params.owner_address,
87 wrap_and_unwrap_sol: Some(true),
88 fee_account: None,
89 compute_unit_price_micro_lamports: None,
90 prioritization_fee_lamports: Some(PrioritizationFeeLamports {
91 priority_level_with_max_lamports: PriorityLevelWitMaxLamports {
92 max_lamports: self
93 .jupiter_swap_options
94 .as_ref()
95 .and_then(|o| o.priority_fee_max_lamports),
96 priority_level: self
97 .jupiter_swap_options
98 .as_ref()
99 .and_then(|o| o.priority_level.clone()),
100 },
101 }),
102 dynamic_compute_unit_limit: self
103 .jupiter_swap_options
104 .as_ref()
105 .map(|o| o.dynamic_compute_unit_limit.unwrap_or_default()),
106 })
107 .await
108 .map_err(|e| RelayerError::DexError(format!("Failed to get swap transaction: {e}")))?;
109
110 debug!(swap_tx = ?swap_tx, "received swap transaction");
111
112 let mut swap_tx = VersionedTransaction::try_from(EncodedSerializedTransaction::new(
113 swap_tx.swap_transaction,
114 ))
115 .map_err(|e| RelayerError::DexError(format!("Failed to decode swap transaction: {e}")))?;
116 let signature = self
117 .signer
118 .sign(&swap_tx.message.serialize())
119 .await
120 .map_err(|e| RelayerError::DexError(format!("Failed to sign Dex transaction: {e}")))?;
121
122 swap_tx.signatures[0] = signature;
123
124 let signature = self
125 .provider
126 .send_versioned_transaction(&swap_tx)
127 .await
128 .map_err(|e| match e {
129 SolanaProviderError::RpcError(err) => {
130 RelayerError::ProviderError(format!("Failed to send transaction: {err}"))
131 }
132 _ => RelayerError::ProviderError(format!("Unexpected error: {e}")),
133 })?;
134
135 debug!(signature = %signature, "waiting for transaction confirmation");
137 self.provider
138 .confirm_transaction(&signature)
139 .await
140 .map_err(|e| {
141 RelayerError::ProviderError(format!("Transaction failed to confirm: {e}"))
142 })?;
143
144 debug!(signature = %signature, "transaction confirmed");
145
146 Ok(SwapResult {
147 mint: params.source_mint,
148 source_amount: params.amount,
149 destination_amount: quote.out_amount,
150 transaction_signature: signature.to_string(),
151 error: None,
152 })
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::{
160 models::SignerError,
161 services::{
162 provider::MockSolanaProviderTrait, signer::MockSolanaSignTrait, JupiterServiceError,
163 MockJupiterServiceTrait, QuoteResponse, RoutePlan, SwapInfo, SwapResponse,
164 },
165 };
166 use solana_sdk::signature::Signature;
167 use std::str::FromStr;
168
169 fn create_mock_jupiter_service() -> MockJupiterServiceTrait {
170 MockJupiterServiceTrait::new()
171 }
172
173 fn create_mock_solana_provider() -> MockSolanaProviderTrait {
174 MockSolanaProviderTrait::new()
175 }
176
177 fn create_mock_solana_signer() -> MockSolanaSignTrait {
178 MockSolanaSignTrait::new()
179 }
180
181 fn create_test_quote_response(
182 input_mint: &str,
183 output_mint: &str,
184 amount: u64,
185 out_amount: u64,
186 ) -> QuoteResponse {
187 QuoteResponse {
188 input_mint: input_mint.to_string(),
189 output_mint: output_mint.to_string(),
190 in_amount: amount,
191 out_amount,
192 other_amount_threshold: out_amount,
193 price_impact_pct: 0.1,
194 swap_mode: "ExactIn".to_string(),
195 slippage_bps: 50, route_plan: vec![RoutePlan {
197 swap_info: SwapInfo {
198 amm_key: "63mqrcydH89L7RhuMC3jLBojrRc2u3QWmjP4UrXsnotS".to_string(), label: "Stabble Stable Swap".to_string(),
200 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
201 output_mint: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB".to_string(),
202 in_amount: "1000000".to_string(),
203 out_amount: "999984".to_string(),
204 fee_amount: Some("10".to_string()),
205 fee_mint: Some("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB".to_string()),
206 },
207 percent: 1,
208 }],
209 }
210 }
211
212 fn create_test_swap_response(encoded_transaction: &str) -> SwapResponse {
213 SwapResponse {
214 swap_transaction: encoded_transaction.to_string(),
215 last_valid_block_height: 123456789,
216 prioritization_fee_lamports: Some(5000),
217 compute_unit_limit: Some(20000),
218 simulation_error: None,
219 }
220 }
221
222 #[tokio::test]
223 async fn test_execute_swap_success() {
224 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
229 let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
230
231 let mut mock_jupiter_service = create_mock_jupiter_service();
232 let mut mock_solana_provider = create_mock_solana_provider();
233 let mut mock_solana_signer = create_mock_solana_signer();
234
235 let quote_response =
236 create_test_quote_response(source_mint, destination_mint, amount, output_amount);
237
238 let encoded_tx = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAKEZhsMunBegjHhwObzSrJeKhnl3sehIwqA8OCTejBJ/Z+O7sAR2gDS0+R1HXkqqjr0Wo3+auYeJQtq0il4DAumgiiHZpJZ1Uy9xq1yiOta3BcBOI7Dv+jmETs0W7Leny+AsVIwZWPN51bjn3Xk4uSzTFeAEom3HHY/EcBBpOfm7HkzWyukBvmNY5l9pnNxB/lTC52M7jy0Pxg6NhYJ37e1WXRYOFdoHOThs0hoFy/UG3+mVBbkR4sB9ywdKopv6IHO9+wuF/sV/02h9w+AjIBszK2bmCBPIrCZH4mqBdRcBFVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPS2wOQQj9KmokeOrgrMWdshu07fURwWLPYC0eDAkB+1Jh0UqsxbwO7GNdqHBaH3CjnuNams8L+PIsxs5JAZ16jJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FmsH4P9uc5VDeldVYzceVRhzPQ3SsaI7BOphAAiCnjaBgMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAtD/6J/XX9kp0wJsfKVh53ksJqzbfyd1RSzIap7OM5ejnStls42Wf0xNRAChL93gEW4UQqPNOSYySLu5vwwX4aQR51VvyMcBu7nTFbs5oFQf9sbLeo/SOUQKxzaJWvBOPBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkGtJJ5s3DlXjsp517KoA8Lg71wC+tMHoDO9HDeQbotrwUMAAUCwFwVAAwACQOhzhsAAAAAAAoGAAQAIgcQAQEPOxAIAAUGAgQgIg8PDQ8hEg4JExEGARQUFAgQKAgmKgEDFhgXFSUnJCkQIywQIysIHSIqAh8DHhkbGhwLL8EgmzNB1pyBBwMAAAA6AWQAAU9kAQIvAABkAgNAQg8AAAAAAE3WYgAAAAAADwAAEAMEAAABCQMW8exZwhONJLLrrr9eKTOouI7XVrRLBjytPl3cL6rziwS+v7vCBB+8CQctooGHnRbQ3aoExfOLSH0uJhZijTPAKrJbYSJJ5hP1VwRmY2FlBkRkC2JtQsJRwDIR3Tbag/HLEdZxTPfqLWdCCyd0nco65bHdIoy/ByorMycoLzADMiYs";
239 let swap_response = create_test_swap_response(encoded_tx);
240
241 mock_jupiter_service
242 .expect_get_quote()
243 .times(1)
244 .returning(move |_| {
245 let response = quote_response.clone();
246 Box::pin(async move { Ok(response) })
247 });
248
249 mock_jupiter_service
250 .expect_get_swap_transaction()
251 .times(1)
252 .returning(move |_| {
253 let response = swap_response.clone();
254 Box::pin(async move { Ok(response) })
255 });
256
257 mock_solana_signer
258 .expect_sign()
259 .times(1)
260 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
261
262 mock_solana_provider
263 .expect_send_versioned_transaction()
264 .times(1)
265 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
266
267 mock_solana_provider
268 .expect_confirm_transaction()
269 .times(1)
270 .returning(move |_| Box::pin(async move { Ok(true) }));
271
272 let dex = JupiterSwapDex::new(
273 Arc::new(mock_solana_provider),
274 Arc::new(mock_solana_signer),
275 Arc::new(mock_jupiter_service),
276 None,
277 );
278
279 let result = dex
280 .execute_swap(SwapParams {
281 owner_address: owner_address.to_string(),
282 source_mint: source_mint.to_string(),
283 destination_mint: destination_mint.to_string(),
284 amount,
285 slippage_percent: 0.5,
286 })
287 .await;
288
289 assert!(
290 result.is_ok(),
291 "Swap should succeed, but got error: {:?}",
292 result.err()
293 );
294
295 let swap_result = result.unwrap();
296 assert_eq!(swap_result.source_amount, amount);
297 assert_eq!(swap_result.destination_amount, output_amount);
298 assert_eq!(
299 swap_result.transaction_signature,
300 test_signature.to_string()
301 );
302 }
303
304 #[tokio::test]
305 async fn test_execute_swap_get_quote_error() {
306 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
310
311 let mut mock_jupiter_service = create_mock_jupiter_service();
312 let mock_solana_provider = create_mock_solana_provider();
313 let mock_solana_signer = create_mock_solana_signer();
314
315 mock_jupiter_service
316 .expect_get_quote()
317 .times(1)
318 .returning(move |_| {
319 Box::pin(async move {
320 Err(crate::services::JupiterServiceError::ApiError {
321 message: "API error: insufficient liquidity".to_string(),
322 })
323 })
324 });
325
326 let dex = JupiterSwapDex::new(
327 Arc::new(mock_solana_provider),
328 Arc::new(mock_solana_signer),
329 Arc::new(mock_jupiter_service),
330 None,
331 );
332
333 let result = dex
334 .execute_swap(SwapParams {
335 owner_address: owner_address.to_string(),
336 source_mint: source_mint.to_string(),
337 destination_mint: destination_mint.to_string(),
338 amount,
339 slippage_percent: 0.5,
340 })
341 .await;
342
343 match result {
344 Err(RelayerError::DexError(error_message)) => {
345 assert!(
346 error_message.contains("Failed to get Jupiter quote")
347 && error_message.contains("insufficient liquidity"),
348 "Error message did not contain expected substrings: {error_message}"
349 );
350 }
351 Err(e) => panic!("Expected DexError but got different error: {e:?}"),
352 Ok(_) => panic!("Expected error but got Ok"),
353 }
354 }
355
356 #[tokio::test]
357 async fn test_execute_swap_get_transaction_error() {
358 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
363
364 let mut mock_jupiter_service = create_mock_jupiter_service();
365 let mock_solana_provider = create_mock_solana_provider();
366 let mock_solana_signer = create_mock_solana_signer();
367
368 let quote_response =
369 create_test_quote_response(source_mint, destination_mint, amount, output_amount);
370
371 mock_jupiter_service
372 .expect_get_quote()
373 .times(1)
374 .returning(move |_| {
375 let response = quote_response.clone();
376 Box::pin(async move { Ok(response) })
377 });
378
379 mock_jupiter_service
380 .expect_get_swap_transaction()
381 .times(1)
382 .returning(move |_| {
383 Box::pin(async move {
384 Err(JupiterServiceError::ApiError {
385 message: "Failed to prepare transaction: rate limit exceeded".to_string(),
386 })
387 })
388 });
389
390 let dex = JupiterSwapDex::new(
391 Arc::new(mock_solana_provider),
392 Arc::new(mock_solana_signer),
393 Arc::new(mock_jupiter_service),
394 None,
395 );
396
397 let result = dex
398 .execute_swap(SwapParams {
399 owner_address: owner_address.to_string(),
400 source_mint: source_mint.to_string(),
401 destination_mint: destination_mint.to_string(),
402 amount,
403 slippage_percent: 0.5,
404 })
405 .await;
406
407 match result {
408 Err(RelayerError::DexError(error_message)) => {
409 assert!(
410 error_message.contains("Failed to get swap transaction")
411 && error_message.contains("rate limit exceeded"),
412 "Error message did not contain expected substrings: {error_message}"
413 );
414 }
415 Err(e) => panic!("Expected DexError but got different error: {e:?}"),
416 Ok(_) => panic!("Expected error but got Ok"),
417 }
418 }
419
420 #[tokio::test]
421 async fn test_execute_swap_invalid_transaction_format() {
422 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
427
428 let mut mock_jupiter_service = create_mock_jupiter_service();
429 let mock_solana_provider = create_mock_solana_provider();
430 let mock_solana_signer = create_mock_solana_signer();
431
432 let quote_response =
433 create_test_quote_response(source_mint, destination_mint, amount, output_amount);
434
435 let swap_response = create_test_swap_response("invalid-transaction-format");
436
437 mock_jupiter_service
438 .expect_get_quote()
439 .times(1)
440 .returning(move |_| {
441 let response = quote_response.clone();
442 Box::pin(async move { Ok(response) })
443 });
444
445 mock_jupiter_service
446 .expect_get_swap_transaction()
447 .times(1)
448 .returning(move |_| {
449 let response = swap_response.clone();
450 Box::pin(async move { Ok(response) })
451 });
452
453 let dex = JupiterSwapDex::new(
454 Arc::new(mock_solana_provider),
455 Arc::new(mock_solana_signer),
456 Arc::new(mock_jupiter_service),
457 None,
458 );
459
460 let result = dex
461 .execute_swap(SwapParams {
462 owner_address: owner_address.to_string(),
463 source_mint: source_mint.to_string(),
464 destination_mint: destination_mint.to_string(),
465 amount,
466 slippage_percent: 0.5,
467 })
468 .await;
469
470 match result {
471 Err(RelayerError::DexError(error_message)) => {
472 assert!(
473 error_message.contains("Failed to decode swap transaction"),
474 "Error message did not contain expected substrings: {error_message}"
475 );
476 }
477 Err(e) => panic!("Expected DexError but got different error: {e:?}"),
478 Ok(_) => panic!("Expected error but got Ok"),
479 }
480 }
481
482 #[tokio::test]
483 async fn test_execute_swap_signing_error() {
484 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
489
490 let mut mock_jupiter_service = create_mock_jupiter_service();
491 let mock_solana_provider = create_mock_solana_provider();
492 let mut mock_solana_signer = create_mock_solana_signer();
493
494 let quote_response =
495 create_test_quote_response(source_mint, destination_mint, amount, output_amount);
496
497 let encoded_tx = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAKEZhsMunBegjHhwObzSrJeKhnl3sehIwqA8OCTejBJ/Z+O7sAR2gDS0+R1HXkqqjr0Wo3+auYeJQtq0il4DAumgiiHZpJZ1Uy9xq1yiOta3BcBOI7Dv+jmETs0W7Leny+AsVIwZWPN51bjn3Xk4uSzTFeAEom3HHY/EcBBpOfm7HkzWyukBvmNY5l9pnNxB/lTC52M7jy0Pxg6NhYJ37e1WXRYOFdoHOThs0hoFy/UG3+mVBbkR4sB9ywdKopv6IHO9+wuF/sV/02h9w+AjIBszK2bmCBPIrCZH4mqBdRcBFVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPS2wOQQj9KmokeOrgrMWdshu07fURwWLPYC0eDAkB+1Jh0UqsxbwO7GNdqHBaH3CjnuNams8L+PIsxs5JAZ16jJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FmsH4P9uc5VDeldVYzceVRhzPQ3SsaI7BOphAAiCnjaBgMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAtD/6J/XX9kp0wJsfKVh53ksJqzbfyd1RSzIap7OM5ejnStls42Wf0xNRAChL93gEW4UQqPNOSYySLu5vwwX4aQR51VvyMcBu7nTFbs5oFQf9sbLeo/SOUQKxzaJWvBOPBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkGtJJ5s3DlXjsp517KoA8Lg71wC+tMHoDO9HDeQbotrwUMAAUCwFwVAAwACQOhzhsAAAAAAAoGAAQAIgcQAQEPOxAIAAUGAgQgIg8PDQ8hEg4JExEGARQUFAgQKAgmKgEDFhgXFSUnJCkQIywQIysIHSIqAh8DHhkbGhwLL8EgmzNB1pyBBwMAAAA6AWQAAU9kAQIvAABkAgNAQg8AAAAAAE3WYgAAAAAADwAAEAMEAAABCQMW8exZwhONJLLrrr9eKTOouI7XVrRLBjytPl3cL6rziwS+v7vCBB+8CQctooGHnRbQ3aoExfOLSH0uJhZijTPAKrJbYSJJ5hP1VwRmY2FlBkRkC2JtQsJRwDIR3Tbag/HLEdZxTPfqLWdCCyd0nco65bHdIoy/ByorMycoLzADMiYs";
498 let swap_response = create_test_swap_response(encoded_tx);
499
500 mock_jupiter_service
501 .expect_get_quote()
502 .times(1)
503 .returning(move |_| {
504 let response = quote_response.clone();
505 Box::pin(async move { Ok(response) })
506 });
507
508 mock_jupiter_service
509 .expect_get_swap_transaction()
510 .times(1)
511 .returning(move |_| {
512 let response = swap_response.clone();
513 Box::pin(async move { Ok(response) })
514 });
515
516 mock_solana_signer
517 .expect_sign()
518 .times(1)
519 .returning(move |_| {
520 Box::pin(async move {
521 Err(SignerError::SigningError(
522 "Failed to sign: invalid key".to_string(),
523 ))
524 })
525 });
526
527 let dex = JupiterSwapDex::new(
528 Arc::new(mock_solana_provider),
529 Arc::new(mock_solana_signer),
530 Arc::new(mock_jupiter_service),
531 None,
532 );
533
534 let result = dex
535 .execute_swap(SwapParams {
536 owner_address: owner_address.to_string(),
537 source_mint: source_mint.to_string(),
538 destination_mint: destination_mint.to_string(),
539 amount,
540 slippage_percent: 0.5,
541 })
542 .await;
543
544 match result {
545 Err(RelayerError::DexError(error_message)) => {
546 assert!(
547 error_message.contains("Failed to sign Dex transaction")
548 && error_message.contains("Failed to sign: invalid key"),
549 "Error message did not contain expected substrings: {error_message}"
550 );
551 }
552 Err(e) => panic!("Expected DexError but got different error: {e:?}"),
553 Ok(_) => panic!("Expected error but got Ok"),
554 }
555 }
556
557 #[tokio::test]
558 async fn test_execute_swap_send_transaction_error() {
559 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
564 let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
565
566 let mut mock_jupiter_service = create_mock_jupiter_service();
567 let mut mock_solana_provider = create_mock_solana_provider();
568 let mut mock_solana_signer = create_mock_solana_signer();
569
570 let quote_response =
571 create_test_quote_response(source_mint, destination_mint, amount, output_amount);
572
573 let encoded_tx = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAKEZhsMunBegjHhwObzSrJeKhnl3sehIwqA8OCTejBJ/Z+O7sAR2gDS0+R1HXkqqjr0Wo3+auYeJQtq0il4DAumgiiHZpJZ1Uy9xq1yiOta3BcBOI7Dv+jmETs0W7Leny+AsVIwZWPN51bjn3Xk4uSzTFeAEom3HHY/EcBBpOfm7HkzWyukBvmNY5l9pnNxB/lTC52M7jy0Pxg6NhYJ37e1WXRYOFdoHOThs0hoFy/UG3+mVBbkR4sB9ywdKopv6IHO9+wuF/sV/02h9w+AjIBszK2bmCBPIrCZH4mqBdRcBFVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPS2wOQQj9KmokeOrgrMWdshu07fURwWLPYC0eDAkB+1Jh0UqsxbwO7GNdqHBaH3CjnuNams8L+PIsxs5JAZ16jJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FmsH4P9uc5VDeldVYzceVRhzPQ3SsaI7BOphAAiCnjaBgMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAtD/6J/XX9kp0wJsfKVh53ksJqzbfyd1RSzIap7OM5ejnStls42Wf0xNRAChL93gEW4UQqPNOSYySLu5vwwX4aQR51VvyMcBu7nTFbs5oFQf9sbLeo/SOUQKxzaJWvBOPBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkGtJJ5s3DlXjsp517KoA8Lg71wC+tMHoDO9HDeQbotrwUMAAUCwFwVAAwACQOhzhsAAAAAAAoGAAQAIgcQAQEPOxAIAAUGAgQgIg8PDQ8hEg4JExEGARQUFAgQKAgmKgEDFhgXFSUnJCkQIywQIysIHSIqAh8DHhkbGhwLL8EgmzNB1pyBBwMAAAA6AWQAAU9kAQIvAABkAgNAQg8AAAAAAE3WYgAAAAAADwAAEAMEAAABCQMW8exZwhONJLLrrr9eKTOouI7XVrRLBjytPl3cL6rziwS+v7vCBB+8CQctooGHnRbQ3aoExfOLSH0uJhZijTPAKrJbYSJJ5hP1VwRmY2FlBkRkC2JtQsJRwDIR3Tbag/HLEdZxTPfqLWdCCyd0nco65bHdIoy/ByorMycoLzADMiYs";
574 let swap_response = create_test_swap_response(encoded_tx);
575
576 mock_jupiter_service
577 .expect_get_quote()
578 .times(1)
579 .returning(move |_| {
580 let response = quote_response.clone();
581 Box::pin(async move { Ok(response) })
582 });
583
584 mock_jupiter_service
585 .expect_get_swap_transaction()
586 .times(1)
587 .returning(move |_| {
588 let response = swap_response.clone();
589 Box::pin(async move { Ok(response) })
590 });
591
592 mock_solana_signer
593 .expect_sign()
594 .times(1)
595 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
596
597 mock_solana_provider
598 .expect_send_versioned_transaction()
599 .times(1)
600 .returning(move |_| {
601 Box::pin(async move {
602 Err(SolanaProviderError::RpcError(
603 "Transaction simulation failed: Insufficient balance for spend".to_string(),
604 ))
605 })
606 });
607
608 let dex = JupiterSwapDex::new(
609 Arc::new(mock_solana_provider),
610 Arc::new(mock_solana_signer),
611 Arc::new(mock_jupiter_service),
612 None,
613 );
614
615 let result = dex
616 .execute_swap(SwapParams {
617 owner_address: owner_address.to_string(),
618 source_mint: source_mint.to_string(),
619 destination_mint: destination_mint.to_string(),
620 amount,
621 slippage_percent: 0.5,
622 })
623 .await;
624
625 match result {
626 Err(RelayerError::ProviderError(error_message)) => {
627 assert!(
628 error_message.contains("Failed to send transaction")
629 && error_message.contains("Insufficient balance"),
630 "Error message did not contain expected substrings: {error_message}"
631 );
632 }
633 Err(e) => panic!("Expected ProviderError but got different error: {e:?}"),
634 Ok(_) => panic!("Expected error but got Ok"),
635 }
636 }
637
638 #[tokio::test]
639 async fn test_execute_swap_confirm_transaction_error() {
640 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
645 let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
646
647 let mut mock_jupiter_service = create_mock_jupiter_service();
648 let mut mock_solana_provider = create_mock_solana_provider();
649 let mut mock_solana_signer = create_mock_solana_signer();
650
651 let quote_response =
652 create_test_quote_response(source_mint, destination_mint, amount, output_amount);
653
654 let encoded_tx = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAKEZhsMunBegjHhwObzSrJeKhnl3sehIwqA8OCTejBJ/Z+O7sAR2gDS0+R1HXkqqjr0Wo3+auYeJQtq0il4DAumgiiHZpJZ1Uy9xq1yiOta3BcBOI7Dv+jmETs0W7Leny+AsVIwZWPN51bjn3Xk4uSzTFeAEom3HHY/EcBBpOfm7HkzWyukBvmNY5l9pnNxB/lTC52M7jy0Pxg6NhYJ37e1WXRYOFdoHOThs0hoFy/UG3+mVBbkR4sB9ywdKopv6IHO9+wuF/sV/02h9w+AjIBszK2bmCBPIrCZH4mqBdRcBFVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPS2wOQQj9KmokeOrgrMWdshu07fURwWLPYC0eDAkB+1Jh0UqsxbwO7GNdqHBaH3CjnuNams8L+PIsxs5JAZ16jJclj04kifG7PRApFI4NgwtaE5na/xCEBI572Nvp+FmsH4P9uc5VDeldVYzceVRhzPQ3SsaI7BOphAAiCnjaBgMGRm/lIRcy/+ytunLDm+e8jOW7xfcSayxDmzpAAAAAtD/6J/XX9kp0wJsfKVh53ksJqzbfyd1RSzIap7OM5ejnStls42Wf0xNRAChL93gEW4UQqPNOSYySLu5vwwX4aQR51VvyMcBu7nTFbs5oFQf9sbLeo/SOUQKxzaJWvBOPBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkGtJJ5s3DlXjsp517KoA8Lg71wC+tMHoDO9HDeQbotrwUMAAUCwFwVAAwACQOhzhsAAAAAAAoGAAQAIgcQAQEPOxAIAAUGAgQgIg8PDQ8hEg4JExEGARQUFAgQKAgmKgEDFhgXFSUnJCkQIywQIysIHSIqAh8DHhkbGhwLL8EgmzNB1pyBBwMAAAA6AWQAAU9kAQIvAABkAgNAQg8AAAAAAE3WYgAAAAAADwAAEAMEAAABCQMW8exZwhONJLLrrr9eKTOouI7XVrRLBjytPl3cL6rziwS+v7vCBB+8CQctooGHnRbQ3aoExfOLSH0uJhZijTPAKrJbYSJJ5hP1VwRmY2FlBkRkC2JtQsJRwDIR3Tbag/HLEdZxTPfqLWdCCyd0nco65bHdIoy/ByorMycoLzADMiYs";
655 let swap_response = create_test_swap_response(encoded_tx);
656
657 mock_jupiter_service
658 .expect_get_quote()
659 .times(1)
660 .returning(move |_| {
661 let response = quote_response.clone();
662 Box::pin(async move { Ok(response) })
663 });
664
665 mock_jupiter_service
666 .expect_get_swap_transaction()
667 .times(1)
668 .returning(move |_| {
669 let response = swap_response.clone();
670 Box::pin(async move { Ok(response) })
671 });
672
673 mock_solana_signer
674 .expect_sign()
675 .times(1)
676 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
677
678 mock_solana_provider
679 .expect_send_versioned_transaction()
680 .times(1)
681 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
682
683 mock_solana_provider
684 .expect_confirm_transaction()
685 .times(1)
686 .returning(move |_| {
687 Box::pin(async move {
688 Err(SolanaProviderError::RpcError(
689 "Transaction timed out".to_string(),
690 ))
691 })
692 });
693
694 let dex = JupiterSwapDex::new(
695 Arc::new(mock_solana_provider),
696 Arc::new(mock_solana_signer),
697 Arc::new(mock_jupiter_service),
698 None,
699 );
700
701 let result = dex
702 .execute_swap(SwapParams {
703 owner_address: owner_address.to_string(),
704 source_mint: source_mint.to_string(),
705 destination_mint: destination_mint.to_string(),
706 amount,
707 slippage_percent: 0.5,
708 })
709 .await;
710
711 match result {
712 Err(RelayerError::ProviderError(error_message)) => {
713 assert!(
714 error_message.contains("Transaction failed to confirm")
715 && error_message.contains("Transaction timed out"),
716 "Error message did not contain expected substrings: {error_message}"
717 );
718 }
719 Err(e) => panic!("Expected ProviderError but got different error: {e:?}"),
720 Ok(_) => panic!("Expected error but got Ok"),
721 }
722 }
723}