openzeppelin_relayer/services/jupiter/
mod.rs

1//! Jupiter API service module
2//! Jupiter API service is used to get quotes for token swaps
3//! Jupiter is not supported on devnet/testnet, so a mock service is used instead
4//! The mock service returns a quote with the same input and output amount
5use 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, // base64 encoded transaction
128    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    /// Get a quote for a given input and output mint
275    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    /// Get a quote for a SOL to a given token
297    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
379// Jupiter Dev Service
380// This service is used on testnet/devnets to mock the Jupiter API service
381// due to the lack of a testnet API service
382pub 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    /// Get a quote for a SOL to a given token
426    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        // Provide realistic-looking mock data
447        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        // USDC -> SOL quote request
592        let request = QuoteRequest {
593            input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), // noboost
594            output_mint: "So11111111111111111111111111111111111111112".to_string(), // SOL
595            amount: 1000000,                                                        // 1 USDC
596            slippage: 0.5,                                                          // 0.5%
597        };
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        // USDC -> SOL quote request
640        let request = QuoteRequest {
641            input_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(), // USDC
642            output_mint: "So11111111111111111111111111111111111111112".to_string(), // SOL
643            amount: 1000000,                                                        // 1 USDC
644            slippage: 0.5,                                                          // 0.5%
645        };
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}