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