1use crate::{
6 constants::{JUPITER_BASE_API_URL, WRAPPED_SOL_MINT},
7 utils::field_as_string,
8};
9use async_trait::async_trait;
10#[cfg(test)]
11use mockall::automock;
12use reqwest::Client;
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16#[derive(Error, Debug)]
17pub enum JupiterServiceError {
18 #[error("HTTP request failed: {0}")]
19 HttpRequestError(#[from] reqwest::Error),
20 #[error("API returned an error: {message}")]
21 ApiError { message: String },
22 #[error("Failed to deserialize response: {0}")]
23 DeserializationError(#[from] serde_json::Error),
24 #[error("An unknown error occurred")]
25 UnknownError,
26}
27
28#[derive(Debug, Serialize)]
29pub struct QuoteRequest {
30 #[serde(rename = "inputMint")]
31 pub input_mint: String,
32 #[serde(rename = "outputMint")]
33 pub output_mint: String,
34 pub amount: u64,
35 #[serde(rename = "slippage")]
36 pub slippage: f32,
37}
38
39#[derive(Debug, Deserialize, Serialize, Clone)]
40#[allow(dead_code)]
41pub struct SwapInfo {
42 #[serde(rename = "ammKey")]
43 pub amm_key: String,
44 pub label: String,
45 #[serde(rename = "inputMint")]
46 pub input_mint: String,
47 #[serde(rename = "outputMint")]
48 pub output_mint: String,
49 #[serde(rename = "inAmount")]
50 pub in_amount: String,
51 #[serde(rename = "outAmount")]
52 pub out_amount: String,
53 #[serde(rename = "feeAmount")]
54 pub fee_amount: String,
55 #[serde(rename = "feeMint")]
56 pub fee_mint: String,
57}
58
59#[derive(Debug, Deserialize, Serialize, Clone)]
60#[allow(dead_code)]
61pub struct RoutePlan {
62 pub percent: u32,
63 #[serde(rename = "swapInfo")]
64 pub swap_info: SwapInfo,
65}
66
67#[derive(Debug, Deserialize, Serialize, Clone)]
68#[allow(dead_code)]
69pub struct QuoteResponse {
70 #[serde(rename = "inputMint")]
71 pub input_mint: String,
72 #[serde(rename = "outputMint")]
73 pub output_mint: String,
74 #[serde(rename = "inAmount")]
75 #[serde(with = "field_as_string")]
76 pub in_amount: u64,
77 #[serde(rename = "outAmount")]
78 #[serde(with = "field_as_string")]
79 pub out_amount: u64,
80 #[serde(rename = "otherAmountThreshold")]
81 #[serde(with = "field_as_string")]
82 pub other_amount_threshold: u64,
83 #[serde(rename = "priceImpactPct")]
84 #[serde(with = "field_as_string")]
85 pub price_impact_pct: f64,
86 #[serde(rename = "swapMode")]
87 pub swap_mode: String,
88 #[serde(rename = "slippageBps")]
89 pub slippage_bps: u32,
90 #[serde(rename = "routePlan")]
91 pub route_plan: Vec<RoutePlan>,
92}
93
94#[derive(Debug, Serialize)]
95#[serde(rename_all = "camelCase")]
96pub struct PrioritizationFeeLamports {
97 pub priority_level_with_max_lamports: PriorityLevelWitMaxLamports,
98}
99
100#[derive(Debug, Serialize)]
101#[serde(rename_all = "camelCase")]
102pub struct PriorityLevelWitMaxLamports {
103 pub priority_level: Option<String>,
104 pub max_lamports: Option<u64>,
105}
106
107#[derive(Debug, Serialize)]
108#[serde(rename_all = "camelCase")]
109pub struct SwapRequest {
110 pub quote_response: QuoteResponse,
111 pub user_public_key: String,
112 pub wrap_and_unwrap_sol: Option<bool>,
113 pub fee_account: Option<String>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub compute_unit_price_micro_lamports: Option<u64>,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub prioritization_fee_lamports: Option<PrioritizationFeeLamports>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub dynamic_compute_unit_limit: Option<bool>,
120}
121
122#[derive(Debug, Deserialize, Serialize, Clone)]
123#[serde(rename_all = "camelCase")]
124pub struct SwapResponse {
125 pub swap_transaction: String, pub last_valid_block_height: u64,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub prioritization_fee_lamports: Option<u64>,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub compute_unit_limit: Option<u64>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub simulation_error: Option<String>,
133}
134
135#[derive(Debug, Deserialize, Serialize, Clone)]
136#[serde(rename_all = "camelCase")]
137pub struct UltraOrderRequest {
138 #[serde(rename = "inputMint")]
139 pub input_mint: String,
140 #[serde(rename = "outputMint")]
141 pub output_mint: String,
142 #[serde(with = "field_as_string")]
143 pub amount: u64,
144 pub taker: String,
145}
146
147#[derive(Debug, Deserialize, Serialize, Clone)]
148#[serde(rename_all = "camelCase")]
149pub struct UltraOrderResponse {
150 #[serde(rename = "inputMint")]
151 pub input_mint: String,
152 #[serde(rename = "outputMint")]
153 pub output_mint: String,
154 #[serde(rename = "inAmount")]
155 #[serde(with = "field_as_string")]
156 pub in_amount: u64,
157 #[serde(rename = "outAmount")]
158 #[serde(with = "field_as_string")]
159 pub out_amount: u64,
160 #[serde(rename = "otherAmountThreshold")]
161 #[serde(with = "field_as_string")]
162 pub other_amount_threshold: u64,
163 #[serde(rename = "priceImpactPct")]
164 #[serde(with = "field_as_string")]
165 pub price_impact_pct: f64,
166 #[serde(rename = "swapMode")]
167 pub swap_mode: String,
168 #[serde(rename = "slippageBps")]
169 pub slippage_bps: u32,
170 #[serde(rename = "routePlan")]
171 pub route_plan: Vec<RoutePlan>,
172 #[serde(rename = "prioritizationFeeLamports")]
173 pub prioritization_fee_lamports: u32,
174 pub transaction: Option<String>,
175 #[serde(rename = "requestId")]
176 pub request_id: String,
177}
178
179#[derive(Debug, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub struct UltraExecuteRequest {
182 #[serde(rename = "signedTransaction")]
183 pub signed_transaction: String,
184 #[serde(rename = "requestId")]
185 pub request_id: String,
186}
187
188#[derive(Debug, Deserialize, Serialize, Clone)]
189#[allow(dead_code)]
190pub struct SwapEvents {
191 #[serde(rename = "inputMint")]
192 pub input_mint: String,
193 #[serde(rename = "outputMint")]
194 pub output_mint: String,
195 #[serde(rename = "inputAmount")]
196 pub input_amount: String,
197 #[serde(rename = "outputAmount")]
198 pub output_amount: String,
199}
200
201#[derive(Debug, Deserialize, Serialize, Clone)]
202#[serde(rename_all = "camelCase")]
203pub struct UltraExecuteResponse {
204 pub signature: Option<String>,
205 pub status: String,
206 pub slot: Option<String>,
207 pub error: Option<String>,
208 pub code: u32,
209 #[serde(rename = "totalInputAmount")]
210 pub total_input_amount: Option<String>,
211 #[serde(rename = "totalOutputAmount")]
212 pub total_output_amount: Option<String>,
213 #[serde(rename = "inputAmountResult")]
214 pub input_amount_result: Option<String>,
215 #[serde(rename = "outputAmountResult")]
216 pub output_amount_result: Option<String>,
217 #[serde(rename = "swapEvents")]
218 pub swap_events: Option<Vec<SwapEvents>>,
219}
220
221#[async_trait]
222#[cfg_attr(test, automock)]
223pub trait JupiterServiceTrait: Send + Sync {
224 async fn get_quote(&self, request: QuoteRequest) -> Result<QuoteResponse, JupiterServiceError>;
225 async fn get_sol_to_token_quote(
226 &self,
227 input_mint: &str,
228 amount: u64,
229 slippage: f32,
230 ) -> Result<QuoteResponse, JupiterServiceError>;
231 async fn get_swap_transaction(
232 &self,
233 request: SwapRequest,
234 ) -> Result<SwapResponse, JupiterServiceError>;
235 async fn get_ultra_order(
236 &self,
237 request: UltraOrderRequest,
238 ) -> Result<UltraOrderResponse, JupiterServiceError>;
239 async fn execute_ultra_order(
240 &self,
241 request: UltraExecuteRequest,
242 ) -> Result<UltraExecuteResponse, JupiterServiceError>;
243}
244
245pub enum JupiterService {
246 Mainnet(MainnetJupiterService),
247 Mock(MockJupiterService),
248}
249
250pub struct MainnetJupiterService {
251 client: Client,
252 base_url: String,
253}
254
255impl MainnetJupiterService {
256 pub fn new() -> Self {
257 Self {
258 client: Client::new(),
259 base_url: JUPITER_BASE_API_URL.to_string(),
260 }
261 }
262}
263
264impl Default for MainnetJupiterService {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270#[async_trait]
271impl JupiterServiceTrait for MainnetJupiterService {
272 async fn get_quote(&self, request: QuoteRequest) -> Result<QuoteResponse, JupiterServiceError> {
274 let slippage_bps: u32 = request.slippage as u32 * 100;
275 let url = format!("{}/swap/v1/quote", self.base_url);
276
277 let response = self
278 .client
279 .get(&url)
280 .query(&[
281 ("inputMint", request.input_mint),
282 ("outputMint", request.output_mint),
283 ("amount", request.amount.to_string()),
284 ("slippageBps", slippage_bps.to_string()),
285 ])
286 .send()
287 .await?
288 .error_for_status()?;
289
290 let quote: QuoteResponse = response.json().await?;
291 Ok(quote)
292 }
293
294 async fn get_sol_to_token_quote(
296 &self,
297 output_mint: &str,
298 amount: u64,
299 slippage: f32,
300 ) -> Result<QuoteResponse, JupiterServiceError> {
301 let request = QuoteRequest {
302 input_mint: WRAPPED_SOL_MINT.to_string(),
303 output_mint: output_mint.to_string(),
304 amount,
305 slippage,
306 };
307
308 self.get_quote(request).await
309 }
310
311 async fn get_swap_transaction(
312 &self,
313 request: SwapRequest,
314 ) -> Result<SwapResponse, JupiterServiceError> {
315 let url = format!("{}/swap/v1/swap", self.base_url);
316 let response = self.client.post(&url).json(&request).send().await?;
317
318 if response.status().is_success() {
319 response
320 .json::<SwapResponse>()
321 .await
322 .map_err(JupiterServiceError::from)
323 } else {
324 let error_text = response
325 .text()
326 .await
327 .unwrap_or_else(|_| "Unknown error".to_string());
328 Err(JupiterServiceError::ApiError {
329 message: error_text,
330 })
331 }
332 }
333
334 async fn get_ultra_order(
335 &self,
336 request: UltraOrderRequest,
337 ) -> Result<UltraOrderResponse, JupiterServiceError> {
338 let url = format!("{}/ultra/v1/order", self.base_url);
339
340 let response = self
341 .client
342 .get(&url)
343 .query(&[
344 ("inputMint", request.input_mint),
345 ("outputMint", request.output_mint),
346 ("amount", request.amount.to_string()),
347 ("taker", request.taker),
348 ])
349 .send()
350 .await?
351 .error_for_status()?;
352
353 response.json().await.map_err(JupiterServiceError::from)
354 }
355
356 async fn execute_ultra_order(
357 &self,
358 request: UltraExecuteRequest,
359 ) -> Result<UltraExecuteResponse, JupiterServiceError> {
360 let url = format!("{}/ultra/v1/execute", self.base_url);
361 let response = self.client.post(&url).json(&request).send().await?;
362
363 if response.status().is_success() {
364 response.json().await.map_err(JupiterServiceError::from)
365 } else {
366 let error_text = response
367 .text()
368 .await
369 .unwrap_or_else(|_| "Unknown error".to_string());
370 Err(JupiterServiceError::ApiError {
371 message: error_text,
372 })
373 }
374 }
375}
376
377pub struct MockJupiterService {}
381
382impl MockJupiterService {
383 pub fn new() -> Self {
384 Self {}
385 }
386}
387
388impl Default for MockJupiterService {
389 fn default() -> Self {
390 Self::new()
391 }
392}
393
394#[async_trait]
395impl JupiterServiceTrait for MockJupiterService {
396 async fn get_quote(&self, request: QuoteRequest) -> Result<QuoteResponse, JupiterServiceError> {
397 let quote = QuoteResponse {
398 input_mint: request.input_mint.clone(),
399 output_mint: request.output_mint.clone(),
400 in_amount: request.amount,
401 out_amount: request.amount,
402 other_amount_threshold: 0,
403 price_impact_pct: 0.0,
404 swap_mode: "ExactIn".to_string(),
405 slippage_bps: 0,
406 route_plan: vec![RoutePlan {
407 percent: 100,
408 swap_info: SwapInfo {
409 amm_key: "mock_amm_key".to_string(),
410 label: "mock_label".to_string(),
411 input_mint: request.input_mint.clone(),
412 output_mint: request.output_mint.to_string(),
413 in_amount: request.amount.to_string(),
414 out_amount: request.amount.to_string(),
415 fee_amount: "0".to_string(),
416 fee_mint: "mock_fee_mint".to_string(),
417 },
418 }],
419 };
420 Ok(quote)
421 }
422
423 async fn get_sol_to_token_quote(
425 &self,
426 output_mint: &str,
427 amount: u64,
428 slippage: f32,
429 ) -> Result<QuoteResponse, JupiterServiceError> {
430 let request = QuoteRequest {
431 input_mint: WRAPPED_SOL_MINT.to_string(),
432 output_mint: output_mint.to_string(),
433 amount,
434 slippage,
435 };
436
437 self.get_quote(request).await
438 }
439
440 async fn get_swap_transaction(
441 &self,
442 _request: SwapRequest,
443 ) -> Result<SwapResponse, JupiterServiceError> {
444 Ok(SwapResponse {
446 swap_transaction: "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...".to_string(),
447 last_valid_block_height: 279632475,
448 prioritization_fee_lamports: Some(9999),
449 compute_unit_limit: Some(388876),
450 simulation_error: None,
451 })
452 }
453
454 async fn get_ultra_order(
455 &self,
456 request: UltraOrderRequest,
457 ) -> Result<UltraOrderResponse, JupiterServiceError> {
458 Ok(UltraOrderResponse {
459 input_mint: request.input_mint.clone(),
460 output_mint: request.output_mint.clone(),
461 in_amount: 10,
462 out_amount: 10,
463 other_amount_threshold: 1,
464 swap_mode: "ExactIn".to_string(),
465 price_impact_pct: 0.0,
466 route_plan: vec![RoutePlan {
467 percent: 100,
468 swap_info: SwapInfo {
469 amm_key: "mock_amm_key".to_string(),
470 label: "mock_label".to_string(),
471 input_mint: request.input_mint,
472 output_mint: request.output_mint.to_string(),
473 in_amount: request.amount.to_string(),
474 out_amount: request.amount.to_string(),
475 fee_amount: "0".to_string(),
476 fee_mint: "mock_fee_mint".to_string(),
477 },
478 }],
479 prioritization_fee_lamports: 0,
480 transaction: Some("test_transaction".to_string()),
481 request_id: "mock_request_id".to_string(),
482 slippage_bps: 0,
483 })
484 }
485
486 async fn execute_ultra_order(
487 &self,
488 _request: UltraExecuteRequest,
489 ) -> Result<UltraExecuteResponse, JupiterServiceError> {
490 Ok(UltraExecuteResponse {
491 signature: Some("mock_signature".to_string()),
492 status: "success".to_string(),
493 slot: Some("123456789".to_string()),
494 error: None,
495 code: 0,
496 total_input_amount: Some("1000000".to_string()),
497 total_output_amount: Some("1000000".to_string()),
498 input_amount_result: Some("1000000".to_string()),
499 output_amount_result: Some("1000000".to_string()),
500 swap_events: Some(vec![SwapEvents {
501 input_mint: "mock_input_mint".to_string(),
502 output_mint: "mock_output_mint".to_string(),
503 input_amount: "1000000".to_string(),
504 output_amount: "1000000".to_string(),
505 }]),
506 })
507 }
508}
509
510#[async_trait]
511impl JupiterServiceTrait for JupiterService {
512 async fn get_sol_to_token_quote(
513 &self,
514 output_mint: &str,
515 amount: u64,
516 slippage: f32,
517 ) -> Result<QuoteResponse, JupiterServiceError> {
518 match self {
519 JupiterService::Mock(service) => {
520 service
521 .get_sol_to_token_quote(output_mint, amount, slippage)
522 .await
523 }
524 JupiterService::Mainnet(service) => {
525 service
526 .get_sol_to_token_quote(output_mint, amount, slippage)
527 .await
528 }
529 }
530 }
531
532 async fn get_quote(&self, request: QuoteRequest) -> Result<QuoteResponse, JupiterServiceError> {
533 match self {
534 JupiterService::Mock(service) => service.get_quote(request).await,
535 JupiterService::Mainnet(service) => service.get_quote(request).await,
536 }
537 }
538
539 async fn get_swap_transaction(
540 &self,
541 request: SwapRequest,
542 ) -> Result<SwapResponse, JupiterServiceError> {
543 match self {
544 JupiterService::Mock(service) => service.get_swap_transaction(request).await,
545 JupiterService::Mainnet(service) => service.get_swap_transaction(request).await,
546 }
547 }
548
549 async fn get_ultra_order(
550 &self,
551 request: UltraOrderRequest,
552 ) -> Result<UltraOrderResponse, JupiterServiceError> {
553 match self {
554 JupiterService::Mock(service) => service.get_ultra_order(request).await,
555 JupiterService::Mainnet(service) => service.get_ultra_order(request).await,
556 }
557 }
558
559 async fn execute_ultra_order(
560 &self,
561 request: UltraExecuteRequest,
562 ) -> Result<UltraExecuteResponse, JupiterServiceError> {
563 match self {
564 JupiterService::Mock(service) => service.execute_ultra_order(request).await,
565 JupiterService::Mainnet(service) => service.execute_ultra_order(request).await,
566 }
567 }
568}
569
570impl JupiterService {
571 pub fn new_from_network(network: &str) -> Self {
572 match network {
573 "devnet" | "testnet" => JupiterService::Mock(MockJupiterService::new()),
574 "mainnet" => JupiterService::Mainnet(MainnetJupiterService::new()),
575 _ => JupiterService::Mainnet(MainnetJupiterService::new()),
576 }
577 }
578}
579
580#[cfg(test)]
581mod tests {
582 use super::*;
583 use mockito;
584
585 #[tokio::test]
586 async fn test_get_quote() {
587 let service = MainnetJupiterService::new();
588
589 let request = QuoteRequest {
591 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), output_mint: "So11111111111111111111111111111111111111112".to_string(), amount: 1000000, slippage: 0.5, };
596
597 let result = service.get_quote(request).await;
598 assert!(result.is_ok());
599
600 let quote = result.unwrap();
601 assert_eq!(
602 quote.input_mint,
603 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
604 );
605 assert_eq!(
606 quote.output_mint,
607 "So11111111111111111111111111111111111111112"
608 );
609 assert!(quote.out_amount > 0);
610 }
611
612 #[tokio::test]
613 async fn test_get_sol_to_token_quote() {
614 let service = MainnetJupiterService::new();
615
616 let result = service
617 .get_sol_to_token_quote("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 1000000, 0.5)
618 .await;
619 assert!(result.is_ok());
620
621 let quote = result.unwrap();
622 assert_eq!(
623 quote.input_mint,
624 "So11111111111111111111111111111111111111112"
625 );
626 assert_eq!(
627 quote.output_mint,
628 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
629 );
630 assert!(quote.out_amount > 0);
631 }
632
633 #[tokio::test]
634 async fn test_mock_get_quote() {
635 let service = MainnetJupiterService::new();
636
637 let request = QuoteRequest {
639 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), output_mint: "So11111111111111111111111111111111111111112".to_string(), amount: 1000000, slippage: 0.5, };
644
645 let result = service.get_quote(request).await;
646 assert!(result.is_ok());
647
648 let quote = result.unwrap();
649 assert_eq!(
650 quote.input_mint,
651 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
652 );
653 assert_eq!(
654 quote.output_mint,
655 "So11111111111111111111111111111111111111112"
656 );
657 assert!(quote.out_amount > 0);
658 }
659
660 #[tokio::test]
661 async fn test_get_swap_transaction() {
662 let mut mock_server = mockito::Server::new_async().await;
663
664 let quote = QuoteResponse {
665 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
666 output_mint: "So11111111111111111111111111111111111111112".to_string(),
667 in_amount: 1000000,
668 out_amount: 24860952,
669 other_amount_threshold: 24362733,
670 price_impact_pct: 0.1,
671 swap_mode: "ExactIn".to_string(),
672 slippage_bps: 50,
673 route_plan: vec![RoutePlan {
674 percent: 100,
675 swap_info: SwapInfo {
676 amm_key: "test_amm_key".to_string(),
677 label: "test_label".to_string(),
678 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
679 output_mint: "So11111111111111111111111111111111111111112".to_string(),
680 in_amount: "1000000".to_string(),
681 out_amount: "24860952".to_string(),
682 fee_amount: "1000".to_string(),
683 fee_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
684 },
685 }],
686 };
687
688 let swap_response = SwapResponse {
689 swap_transaction:
690 "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
691 .to_string(),
692 last_valid_block_height: 12345678,
693 prioritization_fee_lamports: Some(5000),
694 compute_unit_limit: Some(200000),
695 simulation_error: None,
696 };
697
698 let _mock = mock_server
699 .mock("POST", "/swap/v1/swap")
700 .with_status(200)
701 .with_header("content-type", "application/json")
702 .with_body(serde_json::to_string(&swap_response).unwrap())
703 .expect(1)
704 .create_async()
705 .await;
706
707 let service = MainnetJupiterService {
708 client: Client::new(),
709 base_url: mock_server.url(),
710 };
711
712 let request = SwapRequest {
713 quote_response: quote,
714 user_public_key: "test_public_key".to_string(),
715 wrap_and_unwrap_sol: Some(true),
716 fee_account: None,
717 compute_unit_price_micro_lamports: None,
718 prioritization_fee_lamports: None,
719 dynamic_compute_unit_limit: Some(true),
720 };
721
722 let result = service.get_swap_transaction(request).await;
723
724 assert!(result.is_ok());
725 let response = result.unwrap();
726 assert_eq!(response.last_valid_block_height, 12345678);
727 assert_eq!(response.prioritization_fee_lamports, Some(5000));
728 assert_eq!(response.compute_unit_limit, Some(200000));
729 }
730
731 #[tokio::test]
732 async fn test_get_ultra_order() {
733 let mut mock_server = mockito::Server::new_async().await;
734
735 let ultra_response = UltraOrderResponse {
736 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
737 output_mint: "So11111111111111111111111111111111111111112".to_string(),
738 in_amount: 1000000,
739 out_amount: 24860952,
740 other_amount_threshold: 24362733,
741 price_impact_pct: 0.1,
742 swap_mode: "ExactIn".to_string(),
743 slippage_bps: 50,
744 route_plan: vec![RoutePlan {
745 percent: 100,
746 swap_info: SwapInfo {
747 amm_key: "test_amm_key".to_string(),
748 label: "test_label".to_string(),
749 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
750 output_mint: "So11111111111111111111111111111111111111112".to_string(),
751 in_amount: "1000000".to_string(),
752 out_amount: "24860952".to_string(),
753 fee_amount: "1000".to_string(),
754 fee_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
755 },
756 }],
757 prioritization_fee_lamports: 5000,
758 transaction: Some("test_transaction".to_string()),
759 request_id: "test_request_id".to_string(),
760 };
761
762 let _mock = mock_server
763 .mock("GET", "/ultra/v1/order")
764 .match_query(mockito::Matcher::AllOf(vec![
765 mockito::Matcher::UrlEncoded(
766 "inputMint".into(),
767 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".into(),
768 ),
769 mockito::Matcher::UrlEncoded(
770 "outputMint".into(),
771 "So11111111111111111111111111111111111111112".into(),
772 ),
773 mockito::Matcher::UrlEncoded("amount".into(), "1000000".into()),
774 mockito::Matcher::UrlEncoded("taker".into(), "test_taker".into()),
775 ]))
776 .with_status(200)
777 .with_header("content-type", "application/json")
778 .with_body(serde_json::to_string(&ultra_response).unwrap())
779 .expect(1)
780 .create_async()
781 .await;
782 let service = MainnetJupiterService {
783 client: Client::new(),
784 base_url: mock_server.url(),
785 };
786
787 let request = UltraOrderRequest {
788 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
789 output_mint: "So11111111111111111111111111111111111111112".to_string(),
790 amount: 1000000,
791 taker: "test_taker".to_string(),
792 };
793
794 let result = service.get_ultra_order(request).await;
795
796 assert!(result.is_ok());
797 let response = result.unwrap();
798 assert_eq!(response.in_amount, 1000000);
799 assert_eq!(response.out_amount, 24860952);
800 assert_eq!(response.request_id, "test_request_id");
801 assert!(response.transaction.is_some());
802 }
803
804 #[tokio::test]
805 async fn test_execute_ultra_order() {
806 let mut mock_server = mockito::Server::new_async().await;
807
808 let execute_response = UltraExecuteResponse {
809 signature: Some("mock_signature".to_string()),
810 status: "success".to_string(),
811 slot: Some("123456789".to_string()),
812 error: None,
813 code: 0,
814 total_input_amount: Some("1000000".to_string()),
815 total_output_amount: Some("1000000".to_string()),
816 input_amount_result: Some("1000000".to_string()),
817 output_amount_result: Some("1000000".to_string()),
818 swap_events: Some(vec![SwapEvents {
819 input_mint: "mock_input_mint".to_string(),
820 output_mint: "mock_output_mint".to_string(),
821 input_amount: "1000000".to_string(),
822 output_amount: "1000000".to_string(),
823 }]),
824 };
825
826 let _mock = mock_server
827 .mock("POST", "/ultra/v1/execute")
828 .with_status(200)
829 .with_header("content-type", "application/json")
830 .with_body(serde_json::to_string(&execute_response).unwrap())
831 .expect(1)
832 .create_async()
833 .await;
834
835 let service = MainnetJupiterService {
836 client: Client::new(),
837 base_url: mock_server.url(),
838 };
839
840 let request = UltraExecuteRequest {
841 signed_transaction: "signed_transaction_data".to_string(),
842 request_id: "test_request_id".to_string(),
843 };
844
845 let result = service.execute_ultra_order(request).await;
846
847 assert!(result.is_ok());
848 let response = result.unwrap();
849 assert_eq!(response.signature, Some("mock_signature".to_string()));
850 }
851
852 #[tokio::test]
853 async fn test_error_handling_for_api_errors() {
854 let mut mock_server = mockito::Server::new_async().await;
855
856 let _mock = mock_server
857 .mock(
858 "GET",
859 mockito::Matcher::Regex(r"/ultra/v1/order\?.*".to_string()),
860 )
861 .with_status(400)
862 .with_body("Invalid request")
863 .create_async()
864 .await;
865
866 let service = MainnetJupiterService {
867 client: Client::new(),
868 base_url: mock_server.url(),
869 };
870
871 let request = UltraOrderRequest {
872 input_mint: "invalid_mint".to_string(),
873 output_mint: "invalid_mint".to_string(),
874 amount: 1000000,
875 taker: "test_taker".to_string(),
876 };
877
878 let result = service.get_ultra_order(request).await;
879
880 assert!(result.is_err());
881 match result {
882 Err(JupiterServiceError::HttpRequestError(err)) => {
883 assert!(err
884 .to_string()
885 .contains("HTTP status client error (400 Bad Request)"));
886 }
887 _ => panic!("Expected HttpRequestError but got different error type"),
888 }
889 }
890}