1use std::sync::Arc;
11
12use super::{DexStrategy, SwapParams, SwapResult};
13use crate::domain::relayer::RelayerError;
14use crate::models::EncodedSerializedTransaction;
15use crate::services::{
16 JupiterService, JupiterServiceTrait, SolanaSignTrait, SolanaSigner, UltraExecuteRequest,
17 UltraOrderRequest,
18};
19use async_trait::async_trait;
20use log::info;
21use solana_sdk::transaction::VersionedTransaction;
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 info!("Executing Jupiter swap using ultra api: {:?}", params);
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 info!("Received order: {:?}", 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 =
93 EncodedSerializedTransaction::try_from(&swap_tx).map_err(|e| {
94 RelayerError::DexError(format!("Failed to serialize transaction: {}", e))
95 })?;
96 let response = self
97 .jupiter_service
98 .execute_ultra_order(UltraExecuteRequest {
99 signed_transaction: serialized_transaction.into_inner(),
100 request_id: order.request_id,
101 })
102 .await
103 .map_err(|e| RelayerError::DexError(format!("Failed to execute order: {}", e)))?;
104 info!("Order executed successfully, response: {:?}", response);
105
106 Ok(SwapResult {
107 mint: params.source_mint,
108 source_amount: params.amount,
109 destination_amount: order.out_amount,
110 transaction_signature: response.signature.unwrap_or_default(),
111 error: response.error,
112 })
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::{
120 models::SignerError,
121 services::{
122 MockJupiterServiceTrait, MockSolanaSignTrait, RoutePlan, SwapEvents, SwapInfo,
123 UltraExecuteResponse, UltraOrderResponse,
124 },
125 };
126 use mockall::predicate;
127 use solana_sdk::signature::Signature;
128 use std::str::FromStr;
129
130 fn create_mock_jupiter_service() -> MockJupiterServiceTrait {
131 MockJupiterServiceTrait::new()
132 }
133
134 fn create_mock_solana_signer() -> MockSolanaSignTrait {
135 MockSolanaSignTrait::new()
136 }
137
138 fn create_test_ultra_order_response(
139 input_mint: &str,
140 output_mint: &str,
141 amount: u64,
142 out_amount: u64,
143 ) -> UltraOrderResponse {
144 UltraOrderResponse {
145 input_mint: input_mint.to_string(),
146 output_mint: output_mint.to_string(),
147 in_amount: amount,
148 out_amount,
149 other_amount_threshold: out_amount,
150 price_impact_pct: 0.1,
151 swap_mode: "ExactIn".to_string(),
152 slippage_bps: 50, route_plan: vec![RoutePlan {
154 percent: 100,
155 swap_info: SwapInfo {
156 amm_key: "test_amm_key".to_string(),
157 label: "Test".to_string(),
158 input_mint: input_mint.to_string(),
159 output_mint: output_mint.to_string(),
160 in_amount: amount.to_string(),
161 out_amount: out_amount.to_string(),
162 fee_amount: "1000".to_string(),
163 fee_mint: input_mint.to_string(),
164 },
165 }],
166 prioritization_fee_lamports: 5000,
167 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()),
168 request_id: "test-request-id".to_string(),
169 }
170 }
171
172 #[tokio::test]
173 async fn test_execute_swap_success() {
174 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
180 let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
181
182 let mut mock_jupiter_service = create_mock_jupiter_service();
184 let mut mock_solana_signer = create_mock_solana_signer();
185
186 let expected_order =
187 create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
188
189 let expected_execute_response = UltraExecuteResponse {
191 signature: Some(test_signature.to_string()),
192 status: "success".to_string(),
193 slot: Some("123456789".to_string()),
194 error: None,
195 code: 0,
196 total_input_amount: Some("1000000".to_string()),
197 total_output_amount: Some("1000000".to_string()),
198 input_amount_result: Some("1000000".to_string()),
199 output_amount_result: Some("1000000".to_string()),
200 swap_events: Some(vec![SwapEvents {
201 input_mint: "mock_input_mint".to_string(),
202 output_mint: "mock_output_mint".to_string(),
203 input_amount: "1000000".to_string(),
204 output_amount: "1000000".to_string(),
205 }]),
206 };
207
208 mock_jupiter_service
209 .expect_get_ultra_order()
210 .with(predicate::function(move |req: &UltraOrderRequest| {
211 req.input_mint == source_mint
212 && req.output_mint == destination_mint
213 && req.amount == amount
214 && req.taker == owner_address
215 }))
216 .times(1)
217 .returning(move |_| {
218 let order = expected_order.clone();
219 Box::pin(async move { Ok(order) })
220 });
221
222 mock_solana_signer
223 .expect_sign()
224 .times(1)
225 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
226
227 mock_jupiter_service
228 .expect_execute_ultra_order()
229 .with(predicate::function(move |req: &UltraExecuteRequest| {
230 req.request_id == "test-request-id"
231 }))
232 .times(1)
233 .returning(move |_| {
234 let response = expected_execute_response.clone();
235 Box::pin(async move { Ok(response) })
236 });
237
238 let dex =
239 JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
240
241 let result = dex
242 .execute_swap(SwapParams {
243 owner_address: owner_address.to_string(),
244 source_mint: source_mint.to_string(),
245 destination_mint: destination_mint.to_string(),
246 amount,
247 slippage_percent: 0.5,
248 })
249 .await;
250
251 assert!(
252 result.is_ok(),
253 "Swap should succeed, but got error: {:?}",
254 result.err()
255 );
256
257 let swap_result = result.unwrap();
258 assert_eq!(swap_result.source_amount, amount);
259 assert_eq!(swap_result.destination_amount, output_amount);
260 assert_eq!(
261 swap_result.transaction_signature,
262 test_signature.to_string()
263 );
264 }
265
266 #[tokio::test]
267 async fn test_execute_swap_get_order_error() {
268 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
272
273 let mut mock_jupiter_service = create_mock_jupiter_service();
274 let mock_solana_signer = create_mock_solana_signer();
275
276 mock_jupiter_service
277 .expect_get_ultra_order()
278 .times(1)
279 .returning(move |_| {
280 Box::pin(async move {
281 Err(crate::services::JupiterServiceError::ApiError {
282 message: "API error: insufficient liquidity".to_string(),
283 })
284 })
285 });
286
287 let dex =
288 JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
289
290 let result = dex
291 .execute_swap(SwapParams {
292 owner_address: owner_address.to_string(),
293 source_mint: source_mint.to_string(),
294 destination_mint: destination_mint.to_string(),
295 amount,
296 slippage_percent: 0.5,
297 })
298 .await;
299
300 match result {
301 Err(RelayerError::DexError(error_message)) => {
302 assert!(
303 error_message.contains("Failed to get Jupiter Ultra order")
304 && error_message.contains("insufficient liquidity"),
305 "Error message did not contain expected substrings: {}",
306 error_message
307 );
308 }
309 Err(e) => panic!("Expected DexError but got different error: {:?}", e),
310 Ok(_) => panic!("Expected error but got Ok"),
311 }
312 }
313
314 #[tokio::test]
315 async fn test_execute_swap_missing_transaction() {
316 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
321
322 let mut mock_jupiter_service = create_mock_jupiter_service();
323 let mock_solana_signer = create_mock_solana_signer();
324
325 let mut order_response =
326 create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
327 order_response.transaction = None; mock_jupiter_service
330 .expect_get_ultra_order()
331 .times(1)
332 .returning(move |_| {
333 let order = order_response.clone();
334 Box::pin(async move { Ok(order) })
335 });
336
337 let dex =
338 JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
339
340 let result = dex
341 .execute_swap(SwapParams {
342 owner_address: owner_address.to_string(),
343 source_mint: source_mint.to_string(),
344 destination_mint: destination_mint.to_string(),
345 amount,
346 slippage_percent: 0.5,
347 })
348 .await;
349
350 match result {
351 Err(RelayerError::DexError(error_message)) => {
352 assert!(
353 error_message.contains("Failed to get transaction from Jupiter order"),
354 "Error message did not contain expected substrings: {}",
355 error_message
356 );
357 }
358 Err(e) => panic!("Expected DexError but got different error: {:?}", e),
359 Ok(_) => panic!("Expected error but got Ok"),
360 }
361 }
362
363 #[tokio::test]
364 async fn test_execute_swap_invalid_transaction_format() {
365 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
370
371 let mut mock_jupiter_service = create_mock_jupiter_service();
372 let mock_solana_signer = create_mock_solana_signer();
373
374 let mut order_response =
375 create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
376 order_response.transaction = Some("invalid-transaction-format".to_string()); mock_jupiter_service
379 .expect_get_ultra_order()
380 .times(1)
381 .returning(move |_| {
382 let order = order_response.clone();
383 Box::pin(async move { Ok(order) })
384 });
385
386 let dex =
387 JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
388
389 let result = dex
390 .execute_swap(SwapParams {
391 owner_address: owner_address.to_string(),
392 source_mint: source_mint.to_string(),
393 destination_mint: destination_mint.to_string(),
394 amount,
395 slippage_percent: 0.5,
396 })
397 .await;
398
399 match result {
400 Err(RelayerError::DexError(error_message)) => {
401 assert!(
402 error_message.contains("Failed to decode swap transaction"),
403 "Error message did not contain expected substrings: {}",
404 error_message
405 );
406 }
407 Err(e) => panic!("Expected DexError but got different error: {:?}", e),
408 Ok(_) => panic!("Expected error but got Ok"),
409 }
410 }
411
412 #[tokio::test]
413 async fn test_execute_swap_signing_error() {
414 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
419
420 let mut mock_jupiter_service = create_mock_jupiter_service();
421 let mut mock_solana_signer = create_mock_solana_signer();
422
423 let expected_order =
424 create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
425
426 mock_jupiter_service
427 .expect_get_ultra_order()
428 .times(1)
429 .returning(move |_| {
430 let order = expected_order.clone();
431 Box::pin(async move { Ok(order) })
432 });
433
434 mock_solana_signer
435 .expect_sign()
436 .times(1)
437 .returning(move |_| {
438 Box::pin(async move {
439 Err(SignerError::SigningError(
440 "Failed to sign: invalid key".to_string(),
441 ))
442 })
443 });
444
445 let dex =
446 JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
447
448 let result = dex
449 .execute_swap(SwapParams {
450 owner_address: owner_address.to_string(),
451 source_mint: source_mint.to_string(),
452 destination_mint: destination_mint.to_string(),
453 amount,
454 slippage_percent: 0.5,
455 })
456 .await;
457
458 match result {
459 Err(RelayerError::DexError(error_message)) => {
460 assert!(
461 error_message.contains("Failed to sign Dex swap transaction")
462 && error_message.contains("Failed to sign: invalid key"),
463 "Error message did not contain expected substrings: {}",
464 error_message
465 );
466 }
467 Err(e) => panic!("Expected DexError but got different error: {:?}", e),
468 Ok(_) => panic!("Expected error but got Ok"),
469 }
470 }
471
472 #[tokio::test]
473 async fn test_execute_swap_execution_error() {
474 let source_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; let owner_address = "BFzfNx3UdatqpBX4zzJH9Cp7GQZpwc3Fg1aPgYbSgZyf";
479 let test_signature = Signature::from_str("2jg9xbGLtZRsiJBrDWQnz33JuLjDkiKSZuxZPdjJ3qrJbMeTEerXFAKynkPW63J88nq63cvosDNRsg9VqHtGixvP").unwrap();
480
481 let mut mock_jupiter_service = create_mock_jupiter_service();
482 let mut mock_solana_signer = create_mock_solana_signer();
483
484 let expected_order =
485 create_test_ultra_order_response(source_mint, destination_mint, amount, output_amount);
486
487 mock_jupiter_service
488 .expect_get_ultra_order()
489 .times(1)
490 .returning(move |_| {
491 let order = expected_order.clone();
492 Box::pin(async move { Ok(order) })
493 });
494
495 mock_solana_signer
496 .expect_sign()
497 .times(1)
498 .returning(move |_| Box::pin(async move { Ok(test_signature) }));
499
500 mock_jupiter_service
501 .expect_execute_ultra_order()
502 .times(1)
503 .returning(move |_| {
504 Box::pin(async move {
505 Err(crate::services::JupiterServiceError::ApiError {
506 message: "Execution failed: price slippage too high".to_string(),
507 })
508 })
509 });
510
511 let dex =
512 JupiterUltraDex::new(Arc::new(mock_solana_signer), Arc::new(mock_jupiter_service));
513
514 let result = dex
515 .execute_swap(SwapParams {
516 owner_address: owner_address.to_string(),
517 source_mint: source_mint.to_string(),
518 destination_mint: destination_mint.to_string(),
519 amount,
520 slippage_percent: 0.5,
521 })
522 .await;
523
524 match result {
525 Err(RelayerError::DexError(error_message)) => {
526 assert!(
527 error_message.contains("Failed to execute order")
528 && error_message.contains("price slippage too high"),
529 "Error message did not contain expected substrings: {}",
530 error_message
531 );
532 }
533 Err(e) => panic!("Expected DexError but got different error: {:?}", e),
534 Ok(_) => panic!("Expected error but got Ok"),
535 }
536 }
537}