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