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: Some("1000".to_string()),
161                    fee_mint: Some(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: {error_message}"
304                );
305            }
306            Err(e) => panic!("Expected DexError but got different error: {e:?}"),
307            Ok(_) => panic!("Expected error but got Ok"),
308        }
309    }
310
311    #[tokio::test]
312    async fn test_execute_swap_missing_transaction() {
313        let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC
314        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
315        let amount = 1000000; // 1 USDC
316        let output_amount = 24860952; // ~0.025 SOL
317        let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
318
319        let mut mock_jupiter_service = create_mock_jupiter_service();
320        let mock_solana_signer = create_mock_solana_signer();
321
322        let mut order_response =
323            create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
324        order_response.transaction = None; // Missing transaction
325
326        mock_jupiter_service
327            .expect_get_ultra_order()
328            .times(1)
329            .returning(move |_| {
330                let order = order_response.clone();
331                Box::pin(async move { Ok(order) })
332            });
333
334        let dex =
335            JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
336
337        let result = dex
338            .execute_swap(SwapParams {
339                owner_address: owner_address.to_string(),
340                source_mint: source_mint.to_string(),
341                destination_mint: destination_mint.to_string(),
342                amount,
343                slippage_percent: 0.5,
344            })
345            .await;
346
347        match result {
348            Err(RelayerError::DexError(error_message)) => {
349                assert!(
350                    error_message.contains("Failed to get transaction from Jupiter order"),
351                    "Error message did not contain expected substrings: {error_message}"
352                );
353            }
354            Err(e) => panic!("Expected DexError but got different error: {e:?}"),
355            Ok(_) => panic!("Expected error but got Ok"),
356        }
357    }
358
359    #[tokio::test]
360    async fn test_execute_swap_invalid_transaction_format() {
361        let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC
362        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
363        let amount = 1000000; // 1 USDC
364        let output_amount = 24860952; // ~0.025 SOL
365        let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
366
367        let mut mock_jupiter_service = create_mock_jupiter_service();
368        let mock_solana_signer = create_mock_solana_signer();
369
370        let mut order_response =
371            create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
372        order_response.transaction = Some("invalid-transaction-format".to_string()); // Invalid format
373
374        mock_jupiter_service
375            .expect_get_ultra_order()
376            .times(1)
377            .returning(move |_| {
378                let order = order_response.clone();
379                Box::pin(async move { Ok(order) })
380            });
381
382        let dex =
383            JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
384
385        let result = dex
386            .execute_swap(SwapParams {
387                owner_address: owner_address.to_string(),
388                source_mint: source_mint.to_string(),
389                destination_mint: destination_mint.to_string(),
390                amount,
391                slippage_percent: 0.5,
392            })
393            .await;
394
395        match result {
396            Err(RelayerError::DexError(error_message)) => {
397                assert!(
398                    error_message.contains("Failed to decode swap transaction"),
399                    "Error message did not contain expected substrings: {error_message}"
400                );
401            }
402            Err(e) => panic!("Expected DexError but got different error: {e:?}"),
403            Ok(_) => panic!("Expected error but got Ok"),
404        }
405    }
406
407    #[tokio::test]
408    async fn test_execute_swap_signing_error() {
409        let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC
410        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
411        let amount = 1000000; // 1 USDC
412        let output_amount = 24860952; // ~0.025 SOL
413        let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
414
415        let mut mock_jupiter_service = create_mock_jupiter_service();
416        let mut mock_solana_signer = create_mock_solana_signer();
417
418        let expected_order =
419            create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
420
421        mock_jupiter_service
422            .expect_get_ultra_order()
423            .times(1)
424            .returning(move |_| {
425                let order = expected_order.clone();
426                Box::pin(async move { Ok(order) })
427            });
428
429        mock_solana_signer
430            .expect_sign()
431            .times(1)
432            .returning(move |_| {
433                Box::pin(async move {
434                    Err(SignerError::SigningError(
435                        "Failed to sign: invalid key".to_string(),
436                    ))
437                })
438            });
439
440        let dex =
441            JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
442
443        let result = dex
444            .execute_swap(SwapParams {
445                owner_address: owner_address.to_string(),
446                source_mint: source_mint.to_string(),
447                destination_mint: destination_mint.to_string(),
448                amount,
449                slippage_percent: 0.5,
450            })
451            .await;
452
453        match result {
454            Err(RelayerError::DexError(error_message)) => {
455                assert!(
456                    error_message.contains("Failed to sign Dex swap transaction")
457                        && error_message.contains("Failed to sign: invalid key"),
458                    "Error message did not contain expected substrings: {error_message}"
459                );
460            }
461            Err(e) => panic!("Expected DexError but got different error: {e:?}"),
462            Ok(_) => panic!("Expected error but got Ok"),
463        }
464    }
465
466    #[tokio::test]
467    async fn test_execute_swap_execution_error() {
468        let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC
469        let destination_mint = "So11111111111111111111111111111111111111112"; // SOL
470        let amount = 1000000; // 1 USDC
471        let output_amount = 24860952; // ~0.025 SOL
472        let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
473        let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
474
475        let mut mock_jupiter_service = create_mock_jupiter_service();
476        let mut mock_solana_signer = create_mock_solana_signer();
477
478        let expected_order =
479            create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
480
481        mock_jupiter_service
482            .expect_get_ultra_order()
483            .times(1)
484            .returning(move |_| {
485                let order = expected_order.clone();
486                Box::pin(async move { Ok(order) })
487            });
488
489        mock_solana_signer
490            .expect_sign()
491            .times(1)
492            .returning(move |_| Box::pin(async move { Ok(test_signature) }));
493
494        mock_jupiter_service
495            .expect_execute_ultra_order()
496            .times(1)
497            .returning(move |_| {
498                Box::pin(async move {
499                    Err(crate::services::JupiterServiceError::ApiError {
500                        message: "Execution failed: price slippage too high".to_string(),
501                    })
502                })
503            });
504
505        let dex =
506            JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
507
508        let result = dex
509            .execute_swap(SwapParams {
510                owner_address: owner_address.to_string(),
511                source_mint: source_mint.to_string(),
512                destination_mint: destination_mint.to_string(),
513                amount,
514                slippage_percent: 0.5,
515            })
516            .await;
517
518        match result {
519            Err(RelayerError::DexError(error_message)) => {
520                assert!(
521                    error_message.contains("Failed to execute order")
522                        && error_message.contains("price slippage too high"),
523                    "Error message did not contain expected substrings: {error_message}"
524                );
525            }
526            Err(e) => panic!("Expected DexError but got different error: {e:?}"),
527            Ok(_) => panic!("Expected error but got Ok"),
528        }
529    }
530}