openzeppelin_relayer/domain/relayer/solana/dex/
jupiter_swap.rs

1//! JupiterSwapDex
2//!
3//! Implements the `DexStrategy` trait to perform Solana token swaps via the
4//! Jupiter Swap REST API. This module handles:
5//!  1. Fetching a swap quote from Jupiter.
6//!  2. Building the swap transaction.
7//!  3. Decoding and signing the transaction.
8//!  4. Sending the signed transaction on-chain.
9//!  5. Confirming transaction execution.
10use 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        // Wait for transaction confirmation
136        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, // 0.5%
196            route_plan: vec![RoutePlan {
197                swap_info: SwapInfo {
198                    amm_key: "63mqrcydH89L7RhuMC3jLBojrRc2u3QWmjP4UrXsnotS".to_string(), // noboost
199                    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"; // USDC
225        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
226        let amount = 1000000; // 1 USDC
227        let output_amount = 24860952; // ~0.025 SOL
228        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"; // USDC
307        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
308        let amount = 1000000; // 1 USDC
309        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"; // USDC
359        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
360        let amount = 1000000; // 1 USDC
361        let output_amount = 24860952; // ~0.025 SOL
362        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"; // USDC
423        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
424        let amount = 1000000; // 1 USDC
425        let output_amount = 24860952; // ~0.025 SOL
426        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"; // USDC
485        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
486        let amount = 1000000; // 1 USDC
487        let output_amount = 24860952; // ~0.025 SOL
488        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"; // USDC
560        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
561        let amount = 1000000; // 1 USDC
562        let output_amount = 24860952; // ~0.025 SOL
563        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"; // USDC
641        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
642        let amount = 1000000; // 1 USDC
643        let output_amount = 24860952; // ~0.025 SOL
644        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}