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: "10".to_string(),
205 fee_mint: "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: {}",
349 error_message
350 );
351 }
352 Err(e) => panic!("Expected DexError but got different error: {:?}", e),
353 Ok(_) => panic!("Expected error but got Ok"),
354 }
355 }
356
357 #[tokio::test]
358 async fn test_execute_swap_get_transaction_error() {
359 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
364
365 let mut mock_jupiter_service = create_mock_jupiter_service();
366 let mock_solana_provider = create_mock_solana_provider();
367 let mock_solana_signer = create_mock_solana_signer();
368
369 let quote_response =
370 create_test_quote_response(source_mint, destination_mint, amount, output_amount);
371
372 mock_jupiter_service
373 .expect_get_quote()
374 .times(1)
375 .returning(move |_| {
376 let response = quote_response.clone();
377 Box::pin(async move { Ok(response) })
378 });
379
380 mock_jupiter_service
381 .expect_get_swap_transaction()
382 .times(1)
383 .returning(move |_| {
384 Box::pin(async move {
385 Err(JupiterServiceError::ApiError {
386 message: "Failed to prepare transaction: rate limit exceeded".to_string(),
387 })
388 })
389 });
390
391 let dex = JupiterSwapDex::new(
392 Arc::new(mock_solana_provider),
393 Arc::new(mock_solana_signer),
394 Arc::new(mock_jupiter_service),
395 None,
396 );
397
398 let result = dex
399 .execute_swap(SwapParams {
400 owner_address: owner_address.to_string(),
401 source_mint: source_mint.to_string(),
402 destination_mint: destination_mint.to_string(),
403 amount,
404 slippage_percent: 0.5,
405 })
406 .await;
407
408 match result {
409 Err(RelayerError::DexError(error_message)) => {
410 assert!(
411 error_message.contains("Failed to get swap transaction")
412 && error_message.contains("rate limit exceeded"),
413 "Error message did not contain expected substrings: {}",
414 error_message
415 );
416 }
417 Err(e) => panic!("Expected DexError but got different error: {:?}", e),
418 Ok(_) => panic!("Expected error but got Ok"),
419 }
420 }
421
422 #[tokio::test]
423 async fn test_execute_swap_invalid_transaction_format() {
424 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
429
430 let mut mock_jupiter_service = create_mock_jupiter_service();
431 let mock_solana_provider = create_mock_solana_provider();
432 let mock_solana_signer = create_mock_solana_signer();
433
434 let quote_response =
435 create_test_quote_response(source_mint, destination_mint, amount, output_amount);
436
437 let swap_response = create_test_swap_response("invalid-transaction-format");
438
439 mock_jupiter_service
440 .expect_get_quote()
441 .times(1)
442 .returning(move |_| {
443 let response = quote_response.clone();
444 Box::pin(async move { Ok(response) })
445 });
446
447 mock_jupiter_service
448 .expect_get_swap_transaction()
449 .times(1)
450 .returning(move |_| {
451 let response = swap_response.clone();
452 Box::pin(async move { Ok(response) })
453 });
454
455 let dex = JupiterSwapDex::new(
456 Arc::new(mock_solana_provider),
457 Arc::new(mock_solana_signer),
458 Arc::new(mock_jupiter_service),
459 None,
460 );
461
462 let result = dex
463 .execute_swap(SwapParams {
464 owner_address: owner_address.to_string(),
465 source_mint: source_mint.to_string(),
466 destination_mint: destination_mint.to_string(),
467 amount,
468 slippage_percent: 0.5,
469 })
470 .await;
471
472 match result {
473 Err(RelayerError::DexError(error_message)) => {
474 assert!(
475 error_message.contains("Failed to decode swap transaction"),
476 "Error message did not contain expected substrings: {}",
477 error_message
478 );
479 }
480 Err(e) => panic!("Expected DexError but got different error: {:?}", e),
481 Ok(_) => panic!("Expected error but got Ok"),
482 }
483 }
484
485 #[tokio::test]
486 async fn test_execute_swap_signing_error() {
487 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
492
493 let mut mock_jupiter_service = create_mock_jupiter_service();
494 let mock_solana_provider = create_mock_solana_provider();
495 let mut mock_solana_signer = create_mock_solana_signer();
496
497 let quote_response =
498 create_test_quote_response(source_mint, destination_mint, amount, output_amount);
499
500 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";
501 let swap_response = create_test_swap_response(encoded_tx);
502
503 mock_jupiter_service
504 .expect_get_quote()
505 .times(1)
506 .returning(move |_| {
507 let response = quote_response.clone();
508 Box::pin(async move { Ok(response) })
509 });
510
511 mock_jupiter_service
512 .expect_get_swap_transaction()
513 .times(1)
514 .returning(move |_| {
515 let response = swap_response.clone();
516 Box::pin(async move { Ok(response) })
517 });
518
519 mock_solana_signer
520 .expect_sign()
521 .times(1)
522 .returning(move |_| {
523 Box::pin(async move {
524 Err(SignerError::SigningError(
525 "Failed to sign: invalid key".to_string(),
526 ))
527 })
528 });
529
530 let dex = JupiterSwapDex::new(
531 Arc::new(mock_solana_provider),
532 Arc::new(mock_solana_signer),
533 Arc::new(mock_jupiter_service),
534 None,
535 );
536
537 let result = dex
538 .execute_swap(SwapParams {
539 owner_address: owner_address.to_string(),
540 source_mint: source_mint.to_string(),
541 destination_mint: destination_mint.to_string(),
542 amount,
543 slippage_percent: 0.5,
544 })
545 .await;
546
547 match result {
548 Err(RelayerError::DexError(error_message)) => {
549 assert!(
550 error_message.contains("Failed to sign Dex transaction")
551 && error_message.contains("Failed to sign: invalid key"),
552 "Error message did not contain expected substrings: {}",
553 error_message
554 );
555 }
556 Err(e) => panic!("Expected DexError but got different error: {:?}", e),
557 Ok(_) => panic!("Expected error but got Ok"),
558 }
559 }
560
561 #[tokio::test]
562 async fn test_execute_swap_send_transaction_error() {
563 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
568 let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
569
570 let mut mock_jupiter_service = create_mock_jupiter_service();
571 let mut mock_solana_provider = create_mock_solana_provider();
572 let mut mock_solana_signer = create_mock_solana_signer();
573
574 let quote_response =
575 create_test_quote_response(source_mint, destination_mint, amount, output_amount);
576
577 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";
578 let swap_response = create_test_swap_response(encoded_tx);
579
580 mock_jupiter_service
581 .expect_get_quote()
582 .times(1)
583 .returning(move |_| {
584 let response = quote_response.clone();
585 Box::pin(async move { Ok(response) })
586 });
587
588 mock_jupiter_service
589 .expect_get_swap_transaction()
590 .times(1)
591 .returning(move |_| {
592 let response = swap_response.clone();
593 Box::pin(async move { Ok(response) })
594 });
595
596 mock_solana_signer
597 .expect_sign()
598 .times(1)
599 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
600
601 mock_solana_provider
602 .expect_send_versioned_transaction()
603 .times(1)
604 .returning(move |_| {
605 Box::pin(async move {
606 Err(SolanaProviderError::RpcError(
607 "Transaction simulation failed: Insufficient balance for spend".to_string(),
608 ))
609 })
610 });
611
612 let dex = JupiterSwapDex::new(
613 Arc::new(mock_solana_provider),
614 Arc::new(mock_solana_signer),
615 Arc::new(mock_jupiter_service),
616 None,
617 );
618
619 let result = dex
620 .execute_swap(SwapParams {
621 owner_address: owner_address.to_string(),
622 source_mint: source_mint.to_string(),
623 destination_mint: destination_mint.to_string(),
624 amount,
625 slippage_percent: 0.5,
626 })
627 .await;
628
629 match result {
630 Err(RelayerError::ProviderError(error_message)) => {
631 assert!(
632 error_message.contains("Failed to send transaction")
633 && error_message.contains("Insufficient balance"),
634 "Error message did not contain expected substrings: {}",
635 error_message
636 );
637 }
638 Err(e) => panic!("Expected ProviderError but got different error: {:?}", e),
639 Ok(_) => panic!("Expected error but got Ok"),
640 }
641 }
642
643 #[tokio::test]
644 async fn test_execute_swap_confirm_transaction_error() {
645 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
650 let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
651
652 let mut mock_jupiter_service = create_mock_jupiter_service();
653 let mut mock_solana_provider = create_mock_solana_provider();
654 let mut mock_solana_signer = create_mock_solana_signer();
655
656 let quote_response =
657 create_test_quote_response(source_mint, destination_mint, amount, output_amount);
658
659 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";
660 let swap_response = create_test_swap_response(encoded_tx);
661
662 mock_jupiter_service
663 .expect_get_quote()
664 .times(1)
665 .returning(move |_| {
666 let response = quote_response.clone();
667 Box::pin(async move { Ok(response) })
668 });
669
670 mock_jupiter_service
671 .expect_get_swap_transaction()
672 .times(1)
673 .returning(move |_| {
674 let response = swap_response.clone();
675 Box::pin(async move { Ok(response) })
676 });
677
678 mock_solana_signer
679 .expect_sign()
680 .times(1)
681 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
682
683 mock_solana_provider
684 .expect_send_versioned_transaction()
685 .times(1)
686 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
687
688 mock_solana_provider
689 .expect_confirm_transaction()
690 .times(1)
691 .returning(move |_| {
692 Box::pin(async move {
693 Err(SolanaProviderError::RpcError(
694 "Transaction timed out".to_string(),
695 ))
696 })
697 });
698
699 let dex = JupiterSwapDex::new(
700 Arc::new(mock_solana_provider),
701 Arc::new(mock_solana_signer),
702 Arc::new(mock_jupiter_service),
703 None,
704 );
705
706 let result = dex
707 .execute_swap(SwapParams {
708 owner_address: owner_address.to_string(),
709 source_mint: source_mint.to_string(),
710 destination_mint: destination_mint.to_string(),
711 amount,
712 slippage_percent: 0.5,
713 })
714 .await;
715
716 match result {
717 Err(RelayerError::ProviderError(error_message)) => {
718 assert!(
719 error_message.contains("Transaction failed to confirm")
720 && error_message.contains("Transaction timed out"),
721 "Error message did not contain expected substrings: {}",
722 error_message
723 );
724 }
725 Err(e) => panic!("Expected ProviderError but got different error: {:?}", e),
726 Ok(_) => panic!("Expected error but got Ok"),
727 }
728 }
729}