1use 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, 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 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
178 let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
179
180 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 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"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; 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"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; 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; 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"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; 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()); 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"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; 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"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; 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}