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 wiremock::{
584 matchers::{method, path, query_param},
585 Mock, MockServer, ResponseTemplate,
586 };
587
588 #[tokio::test]
589 async fn test_get_quote() {
590 let service = MainnetJupiterService::new();
591
592 let request = QuoteRequest {
594 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), output_mint: "So11111111111111111111111111111111111111112".to_string(), amount: 1000000, slippage: 0.5, };
599
600 let result = service.get_quote(request).await;
601 assert!(result.is_ok());
602
603 let quote = result.unwrap();
604 assert_eq!(
605 quote.input_mint,
606 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
607 );
608 assert_eq!(
609 quote.output_mint,
610 "So11111111111111111111111111111111111111112"
611 );
612 assert!(quote.out_amount > 0);
613 }
614
615 #[tokio::test]
616 async fn test_get_sol_to_token_quote() {
617 let service = MainnetJupiterService::new();
618
619 let result = service
620 .get_sol_to_token_quote("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 1000000, 0.5)
621 .await;
622 assert!(result.is_ok());
623
624 let quote = result.unwrap();
625 assert_eq!(
626 quote.input_mint,
627 "So11111111111111111111111111111111111111112"
628 );
629 assert_eq!(
630 quote.output_mint,
631 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
632 );
633 assert!(quote.out_amount > 0);
634 }
635
636 #[tokio::test]
637 async fn test_mock_get_quote() {
638 let service = MainnetJupiterService::new();
639
640 let request = QuoteRequest {
642 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), output_mint: "So11111111111111111111111111111111111111112".to_string(), amount: 1000000, slippage: 0.5, };
647
648 let result = service.get_quote(request).await;
649 assert!(result.is_ok());
650
651 let quote = result.unwrap();
652 assert_eq!(
653 quote.input_mint,
654 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
655 );
656 assert_eq!(
657 quote.output_mint,
658 "So11111111111111111111111111111111111111112"
659 );
660 assert!(quote.out_amount > 0);
661 }
662
663 #[tokio::test]
664 async fn test_get_swap_transaction() {
665 let mock_server = MockServer::start().await;
666
667 let quote = QuoteResponse {
668 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
669 output_mint: "So11111111111111111111111111111111111111112".to_string(),
670 in_amount: 1000000,
671 out_amount: 24860952,
672 other_amount_threshold: 24362733,
673 price_impact_pct: 0.1,
674 swap_mode: "ExactIn".to_string(),
675 slippage_bps: 50,
676 route_plan: vec![RoutePlan {
677 percent: 100,
678 swap_info: SwapInfo {
679 amm_key: "test_amm_key".to_string(),
680 label: "test_label".to_string(),
681 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
682 output_mint: "So11111111111111111111111111111111111111112".to_string(),
683 in_amount: "1000000".to_string(),
684 out_amount: "24860952".to_string(),
685 fee_amount: "1000".to_string(),
686 fee_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
687 },
688 }],
689 };
690
691 let swap_response = SwapResponse {
692 swap_transaction:
693 "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
694 .to_string(),
695 last_valid_block_height: 12345678,
696 prioritization_fee_lamports: Some(5000),
697 compute_unit_limit: Some(200000),
698 simulation_error: None,
699 };
700
701 Mock::given(method("POST"))
702 .and(path("/swap/v1/swap"))
703 .respond_with(ResponseTemplate::new(200).set_body_json(&swap_response))
704 .expect(1)
705 .mount(&mock_server)
706 .await;
707
708 let service = MainnetJupiterService {
709 client: Client::new(),
710 base_url: mock_server.uri(),
711 };
712
713 let request = SwapRequest {
714 quote_response: quote,
715 user_public_key: "test_public_key".to_string(),
716 wrap_and_unwrap_sol: Some(true),
717 fee_account: None,
718 compute_unit_price_micro_lamports: None,
719 prioritization_fee_lamports: None,
720 dynamic_compute_unit_limit: Some(true),
721 };
722
723 let result = service.get_swap_transaction(request).await;
724
725 assert!(result.is_ok());
726 let response = result.unwrap();
727 assert_eq!(response.last_valid_block_height, 12345678);
728 assert_eq!(response.prioritization_fee_lamports, Some(5000));
729 assert_eq!(response.compute_unit_limit, Some(200000));
730 }
731
732 #[tokio::test]
733 async fn test_get_ultra_order() {
734 let mock_server = MockServer::start().await;
735
736 let ultra_response = UltraOrderResponse {
737 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
738 output_mint: "So11111111111111111111111111111111111111112".to_string(),
739 in_amount: 1000000,
740 out_amount: 24860952,
741 other_amount_threshold: 24362733,
742 price_impact_pct: 0.1,
743 swap_mode: "ExactIn".to_string(),
744 slippage_bps: 50,
745 route_plan: vec![RoutePlan {
746 percent: 100,
747 swap_info: SwapInfo {
748 amm_key: "test_amm_key".to_string(),
749 label: "test_label".to_string(),
750 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
751 output_mint: "So11111111111111111111111111111111111111112".to_string(),
752 in_amount: "1000000".to_string(),
753 out_amount: "24860952".to_string(),
754 fee_amount: "1000".to_string(),
755 fee_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
756 },
757 }],
758 prioritization_fee_lamports: 5000,
759 transaction: Some("test_transaction".to_string()),
760 request_id: "test_request_id".to_string(),
761 };
762
763 Mock::given(method("GET"))
764 .and(path("/ultra/v1/order"))
765 .and(query_param(
766 "inputMint",
767 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
768 ))
769 .and(query_param(
770 "outputMint",
771 "So11111111111111111111111111111111111111112",
772 ))
773 .and(query_param("amount", "1000000"))
774 .and(query_param("taker", "test_taker"))
775 .respond_with(ResponseTemplate::new(200).set_body_json(&ultra_response))
776 .expect(1)
777 .mount(&mock_server)
778 .await;
779 let service = MainnetJupiterService {
780 client: Client::new(),
781 base_url: mock_server.uri(),
782 };
783
784 let request = UltraOrderRequest {
785 input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
786 output_mint: "So11111111111111111111111111111111111111112".to_string(),
787 amount: 1000000,
788 taker: "test_taker".to_string(),
789 };
790
791 let result = service.get_ultra_order(request).await;
792
793 assert!(result.is_ok());
794 let response = result.unwrap();
795 assert_eq!(response.in_amount, 1000000);
796 assert_eq!(response.out_amount, 24860952);
797 assert_eq!(response.request_id, "test_request_id");
798 assert!(response.transaction.is_some());
799 }
800
801 #[tokio::test]
802 async fn test_execute_ultra_order() {
803 let mock_server = MockServer::start().await;
804
805 let execute_response = UltraExecuteResponse {
806 signature: Some("mock_signature".to_string()),
807 status: "success".to_string(),
808 slot: Some("123456789".to_string()),
809 error: None,
810 code: 0,
811 total_input_amount: Some("1000000".to_string()),
812 total_output_amount: Some("1000000".to_string()),
813 input_amount_result: Some("1000000".to_string()),
814 output_amount_result: Some("1000000".to_string()),
815 swap_events: Some(vec![SwapEvents {
816 input_mint: "mock_input_mint".to_string(),
817 output_mint: "mock_output_mint".to_string(),
818 input_amount: "1000000".to_string(),
819 output_amount: "1000000".to_string(),
820 }]),
821 };
822
823 Mock::given(method("POST"))
824 .and(path("/ultra/v1/execute"))
825 .respond_with(ResponseTemplate::new(200).set_body_json(&execute_response))
826 .expect(1)
827 .mount(&mock_server)
828 .await;
829
830 let service = MainnetJupiterService {
831 client: Client::new(),
832 base_url: mock_server.uri(),
833 };
834
835 let request = UltraExecuteRequest {
836 signed_transaction: "signed_transaction_data".to_string(),
837 request_id: "test_request_id".to_string(),
838 };
839
840 let result = service.execute_ultra_order(request).await;
841
842 assert!(result.is_ok());
843 let response = result.unwrap();
844 assert_eq!(response.signature, Some("mock_signature".to_string()));
845 }
846
847 #[tokio::test]
848 async fn test_error_handling_for_api_errors() {
849 let mock_server = MockServer::start().await;
850
851 Mock::given(method("GET"))
852 .and(path("/ultra/v1/order"))
853 .respond_with(ResponseTemplate::new(400).set_body_string("Invalid request"))
854 .expect(1)
855 .mount(&mock_server)
856 .await;
857
858 let service = MainnetJupiterService {
859 client: Client::new(),
860 base_url: mock_server.uri(),
861 };
862
863 let request = UltraOrderRequest {
864 input_mint: "invalid_mint".to_string(),
865 output_mint: "invalid_mint".to_string(),
866 amount: 1000000,
867 taker: "test_taker".to_string(),
868 };
869
870 let result = service.get_ultra_order(request).await;
871
872 assert!(result.is_err());
873 match result {
874 Err(JupiterServiceError::HttpRequestError(err)) => {
875 assert!(err
876 .to_string()
877 .contains("HTTP status client error (400 Bad Request)"));
878 }
879 _ => panic!("Expected ApiError but got different error type"),
880 }
881 }
882}