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: "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"; // 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: {}",
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"; // USDC
360        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
361        let amount = 1000000; // 1 USDC
362        let output_amount = 24860952; // ~0.025 SOL
363        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"; // USDC
425        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
426        let amount = 1000000; // 1 USDC
427        let output_amount = 24860952; // ~0.025 SOL
428        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"; // USDC
488        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
489        let amount = 1000000; // 1 USDC
490        let output_amount = 24860952; // ~0.025 SOL
491        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"; // USDC
564        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
565        let amount = 1000000; // 1 USDC
566        let output_amount = 24860952; // ~0.025 SOL
567        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"; // USDC
646        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
647        let amount = 1000000; // 1 USDC
648        let output_amount = 24860952; // ~0.025 SOL
649        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}