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

1//! JupiterUltraDex
2//!
3//! Implements the `DexStrategy` trait to perform Solana token swaps via the
4//! Jupiter Ultra REST API. This module handles:
5//!  1. Fetching an Ultra order from Jupiter.
6//!  2. Decoding and signing the transaction.
7//!  3. Serializing and executing the signed order via Jupiter Ultra.
8//!  4. Returning the swap result as `SwapResult`.
9
10use std::sync::Arc;
11
12use super::{DexStrategy, SwapParams, SwapResult};
13use crate::domain::relayer::RelayerError;
14use crate::models::EncodedSerializedTransaction;
15use crate::services::{
16    signer::{SolanaSignTrait, SolanaSigner},
17    JupiterService, JupiterServiceTrait, UltraExecuteRequest, UltraOrderRequest,
18};
19use async_trait::async_trait;
20use solana_sdk::transaction::VersionedTransaction;
21use tracing::{debug, info};
22
23pub struct JupiterUltraDex<S, J>
24where
25    S: SolanaSignTrait + 'static,
26    J: JupiterServiceTrait + 'static,
27{
28    signer: Arc<S>,
29    jupiter_service: Arc<J>,
30}
31
32pub type DefaultJupiterUltraDex = JupiterUltraDex<SolanaSigner, JupiterService>;
33
34impl<S, J> JupiterUltraDex<S, J>
35where
36    S: SolanaSignTrait + 'static,
37    J: JupiterServiceTrait + 'static,
38{
39    pub fn new(signer: Arc<S>, jupiter_service: Arc<J>) -> Self {
40        Self {
41            signer,
42            jupiter_service,
43        }
44    }
45}
46
47#[async_trait]
48impl<S, J> DexStrategy for JupiterUltraDex<S, J>
49where
50    S: SolanaSignTrait + Send + Sync + 'static,
51    J: JupiterServiceTrait + Send + Sync + 'static,
52{
53    async fn execute_swap(&self, params: SwapParams) -> Result<SwapResult, RelayerError> {
54        debug!(params = ?params, "executing Jupiter swap using ultra api");
55
56        let order = self
57            .jupiter_service
58            .get_ultra_order(UltraOrderRequest {
59                input_mint: params.source_mint.clone(),
60                output_mint: params.destination_mint,
61                amount: params.amount,
62                taker: params.owner_address,
63            })
64            .await
65            .map_err(|e| {
66                RelayerError::DexError(format!("Failed to get Jupiter Ultra order: {e}"))
67            })?;
68
69        debug!(order = ?order, "received order");
70
71        let encoded_transaction = order.transaction.ok_or_else(|| {
72            RelayerError::DexError("Failed to get transaction from Jupiter order".to_string())
73        })?;
74
75        let mut swap_tx =
76            VersionedTransaction::try_from(EncodedSerializedTransaction::new(encoded_transaction))
77                .map_err(|e| {
78                    RelayerError::DexError(format!("Failed to decode swap transaction: {e}"))
79                })?;
80
81        let signature = self
82            .signer
83            .sign(&swap_tx.message.serialize())
84            .await
85            .map_err(|e| {
86                RelayerError::DexError(format!("Failed to sign Dex swap transaction: {e}"))
87            })?;
88
89        swap_tx.signatures[0] = signature;
90
91        info!("Execute order transaction");
92        let serialized_transaction = EncodedSerializedTransaction::try_from(&swap_tx)
93            .map_err(|e| RelayerError::DexError(format!("Failed to serialize transaction: {e}")))?;
94        let response = self
95            .jupiter_service
96            .execute_ultra_order(UltraExecuteRequest {
97                signed_transaction: serialized_transaction.into_inner(),
98                request_id: order.request_id,
99            })
100            .await
101            .map_err(|e| RelayerError::DexError(format!("Failed to execute order: {e}")))?;
102        debug!(response = ?response, "order executed successfully");
103
104        Ok(SwapResult {
105            mint: params.source_mint,
106            source_amount: params.amount,
107            destination_amount: order.out_amount,
108            transaction_signature: response.signature.unwrap_or_default(),
109            error: response.error,
110        })
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::{
118        models::SignerError,
119        services::{
120            signer::MockSolanaSignTrait, MockJupiterServiceTrait, RoutePlan, SwapEvents, SwapInfo,
121            UltraExecuteResponse, UltraOrderResponse,
122        },
123    };
124    use mockall::predicate;
125    use solana_sdk::signature::Signature;
126    use std::str::FromStr;
127
128    fn create_mock_jupiter_service() -> MockJupiterServiceTrait {
129        MockJupiterServiceTrait::new()
130    }
131
132    fn create_mock_solana_signer() -> MockSolanaSignTrait {
133        MockSolanaSignTrait::new()
134    }
135
136    fn create_test_ultra_order_response(
137        input_mint: &str,
138        output_mint: &str,
139        amount: u64,
140        out_amount: u64,
141    ) -> UltraOrderResponse {
142        UltraOrderResponse {
143            input_mint: input_mint.to_string(),
144            output_mint: output_mint.to_string(),
145            in_amount: amount,
146            out_amount,
147            other_amount_threshold: out_amount,
148            price_impact_pct: 0.1,
149            swap_mode: "ExactIn".to_string(),
150            slippage_bps: 50, // 0.5%
151            route_plan: vec![RoutePlan {
152                percent: 100,
153                swap_info: SwapInfo {
154                    amm_key: "test_amm_key".to_string(),
155                    label: "Test".to_string(),
156                    input_mint: input_mint.to_string(),
157                    output_mint: output_mint.to_string(),
158                    in_amount: amount.to_string(),
159                    out_amount: out_amount.to_string(),
160                    fee_amount: "1000".to_string(),
161                    fee_mint: input_mint.to_string(),
162                },
163            }],
164            prioritization_fee_lamports: 5000,
165            transaction: Some("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".to_string()),
166            request_id: "test-request-id".to_string(),
167        }
168    }
169
170    #[tokio::test]
171    async fn test_execute_swap_success() {
172        // Arrange
173        let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC
174        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
175        let amount = 1000000; // 1 USDC
176        let output_amount = 24860952; // ~0.025 SOL
177        let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
178        let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
179
180        // Create mocks
181        let mut mock_jupiter_service = create_mock_jupiter_service();
182        let mut mock_solana_signer = create_mock_solana_signer();
183
184        let expected_order =
185            create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
186
187        // Expected execute response
188        let expected_execute_response = UltraExecuteResponse {
189            signature: Some(test_signature.to_string()),
190            status: "success".to_string(),
191            slot: Some("123456789".to_string()),
192            error: None,
193            code: 0,
194            total_input_amount: Some("1000000".to_string()),
195            total_output_amount: Some("1000000".to_string()),
196            input_amount_result: Some("1000000".to_string()),
197            output_amount_result: Some("1000000".to_string()),
198            swap_events: Some(vec![SwapEvents {
199                input_mint: "mock_input_mint".to_string(),
200                output_mint: "mock_output_mint".to_string(),
201                input_amount: "1000000".to_string(),
202                output_amount: "1000000".to_string(),
203            }]),
204        };
205
206        mock_jupiter_service
207            .expect_get_ultra_order()
208            .with(predicate::function(move |req: &UltraOrderRequest| {
209                req.input_mint == source_mint
210                    && req.output_mint == destination_mint
211                    && req.amount == amount
212                    && req.taker == owner_address
213            }))
214            .times(1)
215            .returning(move |_| {
216                let order = expected_order.clone();
217                Box::pin(async move { Ok(order) })
218            });
219
220        mock_solana_signer
221            .expect_sign()
222            .times(1)
223            .returning(move |_| Box::pin(async move { Ok(test_signature) }));
224
225        mock_jupiter_service
226            .expect_execute_ultra_order()
227            .with(predicate::function(move |req: &UltraExecuteRequest| {
228                req.request_id == "test-request-id"
229            }))
230            .times(1)
231            .returning(move |_| {
232                let response = expected_execute_response.clone();
233                Box::pin(async move { Ok(response) })
234            });
235
236        let dex =
237            JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
238
239        let result = dex
240            .execute_swap(SwapParams {
241                owner_address: owner_address.to_string(),
242                source_mint: source_mint.to_string(),
243                destination_mint: destination_mint.to_string(),
244                amount,
245                slippage_percent: 0.5,
246            })
247            .await;
248
249        assert!(
250            result.is_ok(),
251            "Swap should succeed, but got error: {:?}",
252            result.err()
253        );
254
255        let swap_result = result.unwrap();
256        assert_eq!(swap_result.source_amount, amount);
257        assert_eq!(swap_result.destination_amount, output_amount);
258        assert_eq!(
259            swap_result.transaction_signature,
260            test_signature.to_string()
261        );
262    }
263
264    #[tokio::test]
265    async fn test_execute_swap_get_order_error() {
266        let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC
267        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
268        let amount = 1000000; // 1 USDC
269        let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
270
271        let mut mock_jupiter_service = create_mock_jupiter_service();
272        let mock_solana_signer = create_mock_solana_signer();
273
274        mock_jupiter_service
275            .expect_get_ultra_order()
276            .times(1)
277            .returning(move |_| {
278                Box::pin(async move {
279                    Err(crate::services::JupiterServiceError::ApiError {
280                        message: "API error: insufficient liquidity".to_string(),
281                    })
282                })
283            });
284
285        let dex =
286            JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
287
288        let result = dex
289            .execute_swap(SwapParams {
290                owner_address: owner_address.to_string(),
291                source_mint: source_mint.to_string(),
292                destination_mint: destination_mint.to_string(),
293                amount,
294                slippage_percent: 0.5,
295            })
296            .await;
297
298        match result {
299            Err(RelayerError::DexError(error_message)) => {
300                assert!(
301                    error_message.contains("Failed to get Jupiter Ultra order")
302                        && error_message.contains("insufficient liquidity"),
303                    "Error message did not contain expected substrings: {}",
304                    error_message
305                );
306            }
307            Err(e) => panic!("Expected DexError but got different error: {:?}", e),
308            Ok(_) => panic!("Expected error but got Ok"),
309        }
310    }
311
312    #[tokio::test]
313    async fn test_execute_swap_missing_transaction() {
314        let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC
315        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
316        let amount = 1000000; // 1 USDC
317        let output_amount = 24860952; // ~0.025 SOL
318        let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
319
320        let mut mock_jupiter_service = create_mock_jupiter_service();
321        let mock_solana_signer = create_mock_solana_signer();
322
323        let mut order_response =
324            create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
325        order_response.transaction = None; // Missing transaction
326
327        mock_jupiter_service
328            .expect_get_ultra_order()
329            .times(1)
330            .returning(move |_| {
331                let order = order_response.clone();
332                Box::pin(async move { Ok(order) })
333            });
334
335        let dex =
336            JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
337
338        let result = dex
339            .execute_swap(SwapParams {
340                owner_address: owner_address.to_string(),
341                source_mint: source_mint.to_string(),
342                destination_mint: destination_mint.to_string(),
343                amount,
344                slippage_percent: 0.5,
345            })
346            .await;
347
348        match result {
349            Err(RelayerError::DexError(error_message)) => {
350                assert!(
351                    error_message.contains("Failed to get transaction from Jupiter order"),
352                    "Error message did not contain expected substrings: {}",
353                    error_message
354                );
355            }
356            Err(e) => panic!("Expected DexError but got different error: {:?}", e),
357            Ok(_) => panic!("Expected error but got Ok"),
358        }
359    }
360
361    #[tokio::test]
362    async fn test_execute_swap_invalid_transaction_format() {
363        let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC
364        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
365        let amount = 1000000; // 1 USDC
366        let output_amount = 24860952; // ~0.025 SOL
367        let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
368
369        let mut mock_jupiter_service = create_mock_jupiter_service();
370        let mock_solana_signer = create_mock_solana_signer();
371
372        let mut order_response =
373            create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
374        order_response.transaction = Some("invalid-transaction-format".to_string()); // Invalid format
375
376        mock_jupiter_service
377            .expect_get_ultra_order()
378            .times(1)
379            .returning(move |_| {
380                let order = order_response.clone();
381                Box::pin(async move { Ok(order) })
382            });
383
384        let dex =
385            JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
386
387        let result = dex
388            .execute_swap(SwapParams {
389                owner_address: owner_address.to_string(),
390                source_mint: source_mint.to_string(),
391                destination_mint: destination_mint.to_string(),
392                amount,
393                slippage_percent: 0.5,
394            })
395            .await;
396
397        match result {
398            Err(RelayerError::DexError(error_message)) => {
399                assert!(
400                    error_message.contains("Failed to decode swap transaction"),
401                    "Error message did not contain expected substrings: {}",
402                    error_message
403                );
404            }
405            Err(e) => panic!("Expected DexError but got different error: {:?}", e),
406            Ok(_) => panic!("Expected error but got Ok"),
407        }
408    }
409
410    #[tokio::test]
411    async fn test_execute_swap_signing_error() {
412        let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC
413        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
414        let amount = 1000000; // 1 USDC
415        let output_amount = 24860952; // ~0.025 SOL
416        let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
417
418        let mut mock_jupiter_service = create_mock_jupiter_service();
419        let mut mock_solana_signer = create_mock_solana_signer();
420
421        let expected_order =
422            create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
423
424        mock_jupiter_service
425            .expect_get_ultra_order()
426            .times(1)
427            .returning(move |_| {
428                let order = expected_order.clone();
429                Box::pin(async move { Ok(order) })
430            });
431
432        mock_solana_signer
433            .expect_sign()
434            .times(1)
435            .returning(move |_| {
436                Box::pin(async move {
437                    Err(SignerError::SigningError(
438                        "Failed to sign: invalid key".to_string(),
439                    ))
440                })
441            });
442
443        let dex =
444            JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
445
446        let result = dex
447            .execute_swap(SwapParams {
448                owner_address: owner_address.to_string(),
449                source_mint: source_mint.to_string(),
450                destination_mint: destination_mint.to_string(),
451                amount,
452                slippage_percent: 0.5,
453            })
454            .await;
455
456        match result {
457            Err(RelayerError::DexError(error_message)) => {
458                assert!(
459                    error_message.contains("Failed to sign Dex swap transaction")
460                        && error_message.contains("Failed to sign: invalid key"),
461                    "Error message did not contain expected substrings: {}",
462                    error_message
463                );
464            }
465            Err(e) => panic!("Expected DexError but got different error: {:?}", e),
466            Ok(_) => panic!("Expected error but got Ok"),
467        }
468    }
469
470    #[tokio::test]
471    async fn test_execute_swap_execution_error() {
472        let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC
473        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
474        let amount = 1000000; // 1 USDC
475        let output_amount = 24860952; // ~0.025 SOL
476        let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
477        let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
478
479        let mut mock_jupiter_service = create_mock_jupiter_service();
480        let mut mock_solana_signer = create_mock_solana_signer();
481
482        let expected_order =
483            create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
484
485        mock_jupiter_service
486            .expect_get_ultra_order()
487            .times(1)
488            .returning(move |_| {
489                let order = expected_order.clone();
490                Box::pin(async move { Ok(order) })
491            });
492
493        mock_solana_signer
494            .expect_sign()
495            .times(1)
496            .returning(move |_| Box::pin(async move { Ok(test_signature) }));
497
498        mock_jupiter_service
499            .expect_execute_ultra_order()
500            .times(1)
501            .returning(move |_| {
502                Box::pin(async move {
503                    Err(crate::services::JupiterServiceError::ApiError {
504                        message: "Execution failed: price slippage too high".to_string(),
505                    })
506                })
507            });
508
509        let dex =
510            JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
511
512        let result = dex
513            .execute_swap(SwapParams {
514                owner_address: owner_address.to_string(),
515                source_mint: source_mint.to_string(),
516                destination_mint: destination_mint.to_string(),
517                amount,
518                slippage_percent: 0.5,
519            })
520            .await;
521
522        match result {
523            Err(RelayerError::DexError(error_message)) => {
524                assert!(
525                    error_message.contains("Failed to execute order")
526                        && error_message.contains("price slippage too high"),
527                    "Error message did not contain expected substrings: {}",
528                    error_message
529                );
530            }
531            Err(e) => panic!("Expected DexError but got different error: {:?}", e),
532            Ok(_) => panic!("Expected error but got Ok"),
533        }
534    }
535}