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: 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 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: {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"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; 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; 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"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; 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()); 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"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; 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"; let destination_mint = "So11111111111111111111111111111111111111112"; let amount = 1000000; let output_amount = 24860952; 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}