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