jup_sdk/
lib.rs

1use reqwest::Client;
2use solana_network_sdk::Solana;
3use std::{collections::HashMap, time::Duration};
4use tokio::time;
5
6use crate::{
7    global::{DEFAULT_SLIPPAGE_BPS, JUPITER_BASE_URL},
8    monitor::{Monitor, TransactionMonitorConfig, TransactionMonitorResult},
9    retry::RetryConfig,
10    router::RouteAnalysis,
11    tool::{is_valid_mint_address, validate_pubkey, validate_slippage_bps},
12    types::{
13        JupiterError, PriceResponse, QuoteRequest, QuoteResponse, SwapRequest, SwapResponse,
14        TokenInfo,
15    },
16};
17
18pub mod global;
19pub mod monitor;
20pub mod retry;
21pub mod router;
22pub mod tool;
23pub mod types;
24
25/// Configuration for Jupiter API client
26#[derive(Debug, Clone)]
27pub struct ClientConfig {
28    pub base_url: String,
29    pub timeout: Duration,
30    pub connect_timeout: Duration,
31    pub pool_idle_timeout: Duration,
32    pub pool_max_idle_per_host: usize,
33    pub user_agent: String,
34    pub max_retries: u32,
35    pub retry_delay: Duration,
36    pub rate_limit_requests_per_second: Option<u32>,
37}
38
39impl Default for ClientConfig {
40    fn default() -> Self {
41        Self {
42            base_url: crate::global::JUPITER_BASE_URL.to_string(),
43            timeout: Duration::from_secs(30),
44            connect_timeout: Duration::from_secs(10),
45            pool_idle_timeout: Duration::from_secs(90),
46            pool_max_idle_per_host: 10,
47            user_agent: format!("jup-sdk/{}", env!("CARGO_PKG_VERSION")),
48            max_retries: 3,
49            retry_delay: Duration::from_millis(500),
50            rate_limit_requests_per_second: Some(10), // Jupiter API 限制
51        }
52    }
53}
54
55/// Main client for interacting with Jupiter API
56pub struct JupiterClient {
57    client: Client,
58    base_url: String,
59    config: ClientConfig,
60    solana: Solana,
61}
62
63impl JupiterClient {
64    /// create a client
65    /// Creates a new Jupiter client with default configuration
66    ///
67    /// # Example
68    /// ```rust
69    /// use jupiter_sdk::JupiterClient;
70    /// let client = JupiterClient::new().unwrap();
71    /// ```
72    pub fn new() -> Result<Self, JupiterError> {
73        Ok(Self {
74            client: Client::new(),
75            base_url: JUPITER_BASE_URL.to_string(),
76            config: ClientConfig::default(),
77            solana: Solana::new(solana_network_sdk::types::Mode::MAIN)
78                .map_err(|e| JupiterError::Error(format!("create solana client error: {:?}", e)))?,
79        })
80    }
81
82    /// create a client based on the URL, using the default configuration.
83    /// Creates a client with custom base URL
84    ///
85    /// # Example
86    /// ```rust
87    /// use jupiter_sdk::JupiterClient;
88    /// let client = JupiterClient::from_base_url("https://quote-api.jup.ag".to_string()).unwrap();
89    /// ```
90    pub fn from_base_url(base_url: String) -> Result<Self, JupiterError> {
91        Ok(Self {
92            client: Client::new(),
93            base_url,
94            config: ClientConfig::default(),
95            solana: Solana::new(solana_network_sdk::types::Mode::MAIN)
96                .map_err(|e| JupiterError::Error(format!("create solana client error: {:?}", e)))?,
97        })
98    }
99
100    /// reate a client based on an existing client, using the default configuration.
101    pub fn from_client(client: Client) -> Result<Self, JupiterError> {
102        Ok(Self {
103            client,
104            base_url: JUPITER_BASE_URL.to_string(),
105            config: ClientConfig::default(),
106            solana: Solana::new(solana_network_sdk::types::Mode::MAIN)
107                .map_err(|e| JupiterError::Error(format!("create solana client error: {:?}", e)))?,
108        })
109    }
110
111    /// create a client using configuration
112    pub fn from_config(config: ClientConfig) -> Result<Self, crate::types::JupiterError> {
113        let client = reqwest::Client::builder()
114            .timeout(config.timeout)
115            .connect_timeout(config.connect_timeout)
116            .pool_idle_timeout(config.pool_idle_timeout)
117            .pool_max_idle_per_host(config.pool_max_idle_per_host)
118            .user_agent(&config.user_agent)
119            .build()
120            .map_err(|e| crate::types::JupiterError::NetworkError(e.to_string()))?;
121        Ok(Self {
122            client,
123            base_url: config.base_url.clone(),
124            config: config,
125            solana: Solana::new(solana_network_sdk::types::Mode::MAIN)
126                .map_err(|e| JupiterError::Error(format!("create solana client error: {:?}", e)))?,
127        })
128    }
129
130    /// create a client with rate limiting
131    pub fn with_rate_limit(requests_per_second: u32) -> Result<Self, crate::types::JupiterError> {
132        let mut config = ClientConfig::default();
133        config.rate_limit_requests_per_second = Some(requests_per_second);
134        Self::from_config(config)
135    }
136
137    /// Monitors transaction status
138    ///
139    /// # Example
140    /// ```rust
141    /// use jupiter_sdk::{JupiterClient, Solana};
142    ///
143    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
144    /// let client = JupiterClient::new()?;
145    /// let solana = Solana::new(solana_network_sdk::types::Mode::MAIN)?;
146    /// let signature = "5verv...";
147    /// let result = client.monitor_transaction(signature, &solana, None).await?;
148    /// # Ok(())
149    /// # }
150    /// ```
151    pub async fn monitor_transaction(
152        &self,
153        signature: &str,
154        solana: &Solana,
155        config: Option<TransactionMonitorConfig>,
156    ) -> Result<TransactionMonitorResult, JupiterError> {
157        let monitor = Monitor;
158        monitor
159            .monitor_transaction_status(signature, solana, config)
160            .await
161    }
162
163    /// Monitors multiple transactions in batch
164    pub async fn monitor_transactions_batch(
165        &self,
166        signatures: &[String],
167        solana: &Solana,
168        config: Option<TransactionMonitorConfig>,
169    ) -> Result<Vec<TransactionMonitorResult>, JupiterError> {
170        let monitor = Monitor;
171        monitor
172            .monitor_transactions_batch(signatures, solana, config)
173            .await
174    }
175
176    /// Gets a quote for token swap
177    ///
178    /// # Example
179    /// ```rust
180    /// use jupiter_sdk::{JupiterClient, QuoteRequest};
181    ///
182    /// async fn example() -> Result<(), Box<dyn std::error::Error>> {
183    /// let client = JupiterClient::new()?;
184    /// let request = QuoteRequest {
185    ///     input_mint: "So11111111111111111111111111111111111111112".to_string(),
186    ///     output_mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
187    ///     amount: 1000000,
188    ///     slippage_bps: 50,
189    ///     fee_bps: None,
190    ///     only_direct_routes: None,
191    ///     as_legacy_transaction: None,
192    ///     restrict_middle_tokens: None,
193    /// };
194    /// let quote = client.get_quote(&request).await?;
195    /// Ok(())
196    /// }
197    /// ```
198    pub async fn get_quote(&self, request: &QuoteRequest) -> Result<QuoteResponse, JupiterError> {
199        self.validate_quote_request(request)?;
200        let url = format!("{}/quote", self.base_url);
201        let response = self
202            .client
203            .get(&url)
204            .query(&request)
205            .send()
206            .await
207            .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
208        let status = response.status();
209        if !status.is_success() {
210            let error_text = response
211                .text()
212                .await
213                .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
214            return Err(JupiterError::RequestFailed(format!(
215                "HTTP {}: {}",
216                status, error_text
217            )));
218        }
219        let quote: QuoteResponse = response
220            .json()
221            .await
222            .map_err(|e| JupiterError::ParseError(e.to_string()))?;
223        Ok(quote)
224    }
225
226    /// Gets swap transaction data
227    ///
228    /// # Example
229    /// ```rust
230    /// use jupiter_sdk::{JupiterClient, SwapRequest, QuoteResponse};
231    ///
232    /// async fn example() -> Result<(), Box<dyn std::error::Error>> {
233    /// let client = JupiterClient::new()?;
234    /// let quote = QuoteResponse { /* ... */ };
235    /// let request = SwapRequest {
236    ///     quote_response: quote,
237    ///     user_public_key: "YourPublicKeyHere".to_string(),
238    ///     wrap_and_unwrap_sol: Some(true),
239    ///     compute_unit_price: None,
240    ///     prioritization_fee_lamports: None,
241    /// };
242    /// let swap_response = client.get_swap_transaction(&request).await?;
243    /// Ok(())
244    /// }
245    /// ```
246    pub async fn get_swap_transaction_data(
247        &self,
248        request: &SwapRequest,
249    ) -> Result<SwapResponse, JupiterError> {
250        self.validate_swap_request(request)?;
251        let url = format!("{}/swap", self.base_url);
252        let response = self
253            .client
254            .post(&url)
255            .json(&request)
256            .send()
257            .await
258            .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
259        let status = response.status();
260        if !status.is_success() {
261            let error_text = response
262                .text()
263                .await
264                .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
265            return Err(JupiterError::RequestFailed(format!(
266                "HTTP {}: {}",
267                status, error_text
268            )));
269        }
270        let swap_response: SwapResponse = response
271            .json()
272            .await
273            .map_err(|e| JupiterError::ParseError(e.to_string()))?;
274        Ok(swap_response)
275    }
276
277    /// Gets list of all supported tokens
278    pub async fn get_tokens(&self) -> Result<Vec<TokenInfo>, JupiterError> {
279        let url = format!("{}/tokens", self.base_url);
280        let response = self
281            .client
282            .get(&url)
283            .send()
284            .await
285            .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
286        let status = response.status();
287        if !status.is_success() {
288            let error_text = response
289                .text()
290                .await
291                .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
292            return Err(JupiterError::RequestFailed(format!(
293                "HTTP {}: {}",
294                status, error_text
295            )));
296        }
297        let tokens: Vec<TokenInfo> = response
298            .json()
299            .await
300            .map_err(|e| JupiterError::ParseError(e.to_string()))?;
301        Ok(tokens)
302    }
303
304    /// Gets prices for multiple tokens
305    pub async fn get_price(
306        &self,
307        ids: &[String],
308    ) -> Result<HashMap<String, PriceResponse>, JupiterError> {
309        if ids.is_empty() {
310            return Err(JupiterError::InvalidInput(
311                "No token IDs provided".to_string(),
312            ));
313        }
314        let url = format!("{}/price", self.base_url);
315        let mut params = HashMap::new();
316        params.insert("ids", ids.join(","));
317        let response = self
318            .client
319            .get(&url)
320            .query(&params)
321            .send()
322            .await
323            .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
324        let status = response.status();
325        if !status.is_success() {
326            let error_text = response
327                .text()
328                .await
329                .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
330            return Err(JupiterError::RequestFailed(format!(
331                "HTTP {}: {}",
332                status, error_text
333            )));
334        }
335        let prices: HashMap<String, PriceResponse> = response
336            .json()
337            .await
338            .map_err(|e| JupiterError::ParseError(e.to_string()))?;
339        Ok(prices)
340    }
341
342    /// Gets multiple routes for token swap
343    pub async fn get_routes(
344        &self,
345        input_mint: &str,
346        output_mint: &str,
347        amount: u64,
348        slippage_bps: u16,
349    ) -> Result<Vec<QuoteResponse>, JupiterError> {
350        self.validate_mint_address(input_mint)?;
351        self.validate_mint_address(output_mint)?;
352        validate_slippage_bps(slippage_bps).map_err(|e| JupiterError::Error(format!("{:?}", e)))?;
353        let url = format!("{}/quote", self.base_url);
354        let params = [
355            ("inputMint", input_mint),
356            ("outputMint", output_mint),
357            ("amount", &amount.to_string()),
358            ("slippageBps", &slippage_bps.to_string()),
359        ];
360        let response = self
361            .client
362            .get(&url)
363            .query(&params)
364            .send()
365            .await
366            .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
367        let status = response.status();
368        if !status.is_success() {
369            let error_text = response
370                .text()
371                .await
372                .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
373            return Err(JupiterError::RequestFailed(format!(
374                "HTTP {}: {}",
375                status, error_text
376            )));
377        }
378        let routes: Vec<QuoteResponse> = response
379            .json()
380            .await
381            .map_err(|e| JupiterError::ParseError(e.to_string()))?;
382        Ok(routes)
383    }
384
385    /// Simple method to get swap quote
386    ///
387    /// # Example
388    /// ```rust
389    /// use jupiter_sdk::JupiterClient;
390    ///
391    /// async fn example() -> Result<(), Box<dyn std::error::Error>> {
392    /// let client = JupiterClient::new()?;
393    /// let input_mint = "So11111111111111111111111111111111111111112";
394    /// let output_mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
395    /// let amount = 1000000;
396    /// let quote = client.simple_swap_quote(input_mint, output_mint, amount, Some(50)).await?;
397    /// Ok(())
398    /// }
399    /// ```
400    pub async fn simple_swap_quote(
401        &self,
402        input_mint: &str,
403        output_mint: &str,
404        amount: u64,
405        slippage_bps: Option<u16>,
406    ) -> Result<QuoteResponse, JupiterError> {
407        let slippage = slippage_bps.unwrap_or(DEFAULT_SLIPPAGE_BPS);
408        let request = QuoteRequest {
409            input_mint: input_mint.to_string(),
410            output_mint: output_mint.to_string(),
411            amount,
412            slippage_bps: slippage,
413            fee_bps: None,
414            only_direct_routes: None,
415            as_legacy_transaction: None,
416            restrict_middle_tokens: None,
417        };
418        self.get_quote(&request).await
419    }
420
421    /// Finds token by symbol
422    pub async fn get_token_by_symbol(
423        &self,
424        symbol: &str,
425    ) -> Result<Option<TokenInfo>, JupiterError> {
426        let tokens = self.get_tokens().await?;
427        let token = tokens
428            .into_iter()
429            .find(|token| token.symbol.to_lowercase() == symbol.to_lowercase());
430        Ok(token)
431    }
432
433    /// Finds token by address
434    pub async fn get_token_by_address(
435        &self,
436        address: &str,
437    ) -> Result<Option<TokenInfo>, JupiterError> {
438        self.validate_mint_address(address)?;
439        let tokens = self.get_tokens().await?;
440        let token = tokens.into_iter().find(|token| token.address == address);
441        Ok(token)
442    }
443
444    /// Gets price for a single token
445    pub async fn get_token_price(&self, mint_address: &str) -> Result<Option<f64>, JupiterError> {
446        self.validate_mint_address(mint_address)?;
447        let prices = self.get_price(&[mint_address.to_string()]).await?;
448        Ok(prices.get(mint_address).map(|price| price.price))
449    }
450
451    /// Creates swap transaction from quote
452    pub async fn create_swap_transaction(
453        &self,
454        quote: QuoteResponse,
455        user_public_key: &str,
456        wrap_and_unwrap_sol: Option<bool>,
457    ) -> Result<SwapResponse, JupiterError> {
458        self.validate_pubkey(user_public_key)?;
459        let request = SwapRequest {
460            quote_response: quote,
461            user_public_key: user_public_key.to_string(),
462            wrap_and_unwrap_sol,
463            compute_unit_price: None,
464            prioritization_fee_lamports: None,
465        };
466        self.get_swap_transaction_data(&request).await
467    }
468
469    pub async fn get_quotes_batch(
470        &self,
471        requests: &[QuoteRequest],
472    ) -> Result<Vec<Result<QuoteResponse, JupiterError>>, JupiterError> {
473        let mut results = Vec::new();
474        for request in requests {
475            let result = self.get_quote(request).await;
476            results.push(result);
477        }
478        Ok(results)
479    }
480
481    pub async fn get_quote_with_retry(
482        &self,
483        request: &QuoteRequest,
484        max_retries: u32,
485    ) -> Result<QuoteResponse, JupiterError> {
486        for attempt in 0..=max_retries {
487            match self.get_quote(request).await {
488                Ok(quote) => return Ok(quote),
489                Err(e) if attempt == max_retries => return Err(e),
490                Err(e) if e.is_retriable() => {
491                    let delay_ms = 200 * (attempt + 1) as u64;
492                    tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
493                    continue;
494                }
495                Err(e) => return Err(e),
496            }
497        }
498        unreachable!()
499    }
500
501    /// Get Route Map - Used to understand all available transaction paths
502    /// Gets all token pairs and routing information supported by Jupiter
503    pub async fn get_indexed_route_map(
504        &self,
505    ) -> Result<crate::types::IndexedRouteMapResponse, JupiterError> {
506        let url = format!("{}/indexed-route-map", self.base_url);
507        let response = self
508            .client
509            .get(&url)
510            .send()
511            .await
512            .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
513        let status = response.status();
514        if !status.is_success() {
515            let error_text = response
516                .text()
517                .await
518                .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
519            return Err(JupiterError::RequestFailed(format!(
520                "HTTP {}: {}",
521                status, error_text
522            )));
523        }
524        let route_map: crate::types::IndexedRouteMapResponse = response
525            .json()
526            .await
527            .map_err(|e| JupiterError::ParseError(e.to_string()))?;
528        Ok(route_map)
529    }
530
531    /// Get a list of program IDs - used to verify the programs involved in a transaction
532    /// Get all Solana program IDs involved in a Jupiter exchange
533    pub async fn get_program_ids(&self) -> Result<Vec<String>, JupiterError> {
534        let url = format!("{}/program-ids", self.base_url);
535        let response = self
536            .client
537            .get(&url)
538            .send()
539            .await
540            .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
541        let status = response.status();
542        if !status.is_success() {
543            let error_text = response
544                .text()
545                .await
546                .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
547            return Err(JupiterError::RequestFailed(format!(
548                "HTTP {}: {}",
549                status, error_text
550            )));
551        }
552        let program_ids: Vec<String> = response
553            .json()
554            .await
555            .map_err(|e| JupiterError::ParseError(e.to_string()))?;
556        Ok(program_ids)
557    }
558
559    pub async fn health(&self) -> Result<bool, JupiterError> {
560        let url = format!("{}/health", self.base_url);
561        let response = self
562            .client
563            .get(&url)
564            .send()
565            .await
566            .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
567        Ok(response.status().is_success())
568    }
569
570    /// Batch Price Retrieval - Retrieve prices of multiple tokens at once
571    /// Efficiently retrieve price information for multiple tokens, reducing the number of API calls.
572    pub async fn get_prices_batch(
573        &self,
574        token_pairs: &[(&str, &str)], // (mint_address, vs_token)
575    ) -> Result<HashMap<String, f64>, JupiterError> {
576        if token_pairs.is_empty() {
577            return Ok(HashMap::new());
578        }
579        let ids: Vec<String> = token_pairs
580            .iter()
581            .map(|(mint, vs)| format!("{}:{}", mint, vs))
582            .collect();
583        let mut params = HashMap::new();
584        params.insert("ids", ids.join(","));
585        let url = format!("{}/price", self.base_url);
586        let response = self
587            .client
588            .get(&url)
589            .query(&params)
590            .send()
591            .await
592            .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
593        let status = response.status();
594        if !status.is_success() {
595            let error_text = response
596                .text()
597                .await
598                .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
599            return Err(JupiterError::RequestFailed(format!(
600                "HTTP {}: {}",
601                status, error_text
602            )));
603        }
604        let prices: HashMap<String, crate::types::PriceResponse> = response
605            .json()
606            .await
607            .map_err(|e| JupiterError::ParseError(e.to_string()))?;
608        let result = prices
609            .into_iter()
610            .map(|(id, price)| (id, price.price))
611            .collect();
612        Ok(result)
613    }
614
615    /// Advanced Route Analysis - Compare multiple routes and select the optimal one
616    //  Analyze metrics such as price impact, slippage, and execution time of different routes.
617    pub async fn analyze_routes(
618        &self,
619        input_mint: &str,
620        output_mint: &str,
621        amount: u64,
622        max_routes: Option<usize>,
623    ) -> Result<RouteAnalysis, JupiterError> {
624        let routes = self.get_routes(input_mint, output_mint, amount, 50).await?;
625        if routes.is_empty() {
626            return Err(JupiterError::RequestFailed("No routes found".to_string()));
627        }
628        let best_route = routes.first().unwrap().clone();
629        let mut analysis = RouteAnalysis::new(best_route);
630        if routes.len() > 1 {
631            let max_alt = max_routes.unwrap_or(3).min(routes.len() - 1);
632            analysis.alternative_routes = routes[1..=max_alt].to_vec();
633        }
634        if let Ok(price_impact) = analysis.best_route.price_impact_pct.parse::<f64>() {
635            analysis.confidence_score = (100.0 - price_impact.max(0.0)) / 100.0;
636            analysis.confidence_score = analysis.confidence_score.max(0.1).min(1.0);
637        }
638        Ok(analysis)
639    }
640
641    /// Paginated token list - Use pagination when retrieving a large number of tokens
642    /// Supports paginated retrieval of token lists to avoid loading too much data at once.
643    pub async fn get_tokens_paginated(
644        &self,
645        page: Option<u32>,
646        page_size: Option<u32>,
647    ) -> Result<Vec<TokenInfo>, JupiterError> {
648        let url = format!("{}/tokens", self.base_url);
649        let mut request_builder = self.client.get(&url);
650        if let Some(page) = page {
651            request_builder = request_builder.query(&[("page", page)]);
652        }
653        if let Some(page_size) = page_size {
654            request_builder = request_builder.query(&[("pageSize", page_size)]);
655        }
656        let response = request_builder
657            .send()
658            .await
659            .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
660        let status = response.status();
661        if !status.is_success() {
662            let error_text = response
663                .text()
664                .await
665                .map_err(|e| JupiterError::NetworkError(e.to_string()))?;
666            return Err(JupiterError::RequestFailed(format!(
667                "HTTP {}: {}",
668                status, error_text
669            )));
670        }
671        let tokens: Vec<TokenInfo> = response
672            .json()
673            .await
674            .map_err(|e| JupiterError::ParseError(e.to_string()))?;
675        Ok(tokens)
676    }
677
678    /// Filter tokens by tag - Get tokens categorized by purpose
679    /// Filter tokens by tag (e.g., stablecoin, defi, etc.)
680    pub async fn get_tokens_by_tag(&self, tag: &str) -> Result<Vec<TokenInfo>, JupiterError> {
681        let all_tokens = self.get_tokens().await?;
682        let filtered: Vec<TokenInfo> = all_tokens
683            .into_iter()
684            .filter(|token| token.tags.iter().any(|t| t == tag))
685            .collect();
686        Ok(filtered)
687    }
688
689    /// Calculate transaction fees - Estimate transaction execution costs
690    /// Estimate transaction fees based on transaction complexity and current network status
691    pub async fn estimate_transaction_fee(
692        &self,
693        quote: &QuoteResponse,
694        priority_fee: Option<u64>,
695    ) -> Result<u64, JupiterError> {
696        // Base compute unit fee in micro-lamports per CU
697        let base_fee = 5000; // micro-lamports per CU
698        // Estimate compute units based on route complexity
699        let compute_units = match quote.route_plan.len() {
700            1 => 100_000, // Simple swap
701            2 => 150_000, // Medium complexity
702            _ => 200_000, // Complex route
703        };
704        let total_fee = base_fee * compute_units / 1_000_000; // 转换为 lamports
705        let priority_fee = priority_fee.unwrap_or(0);
706        Ok(total_fee + priority_fee)
707    }
708
709    /// Exchange transaction creation with retries
710    pub async fn get_swap_transaction_with_retry(
711        &self,
712        request: &crate::types::SwapRequest,
713        config: &RetryConfig,
714    ) -> Result<crate::types::SwapResponse, JupiterError> {
715        self.execute_with_retry(|| self.get_swap_transaction_data(request), config)
716            .await
717    }
718
719    async fn execute_with_retry<F, T, Fut>(
720        &self,
721        operation: F,
722        config: &RetryConfig,
723    ) -> Result<T, JupiterError>
724    where
725        F: Fn() -> Fut,
726        Fut: std::future::Future<Output = Result<T, JupiterError>>,
727    {
728        let mut last_error = None;
729
730        for attempt in 0..=config.max_retries {
731            match operation().await {
732                Ok(result) => return Ok(result),
733                Err(e) => {
734                    last_error = Some(e.clone());
735                    if attempt < config.max_retries && e.is_retriable() {
736                        let delay = Self::cal_delay(attempt, config);
737                        time::sleep(delay).await;
738                        continue;
739                    } else {
740                        break;
741                    }
742                }
743            }
744        }
745        Err(last_error
746            .unwrap_or_else(|| JupiterError::Error("Unknown error after retries".to_string())))
747    }
748
749    /// Calculate backoff delay
750    fn cal_delay(attempt: u32, config: &RetryConfig) -> Duration {
751        let delay = config.initial_delay.as_millis() as f64
752            * config.backoff_multiplier.powi(attempt as i32);
753        let delay = delay.min(config.max_delay.as_millis() as f64);
754        Duration::from_millis(delay as u64)
755    }
756
757    fn validate_quote_request(&self, request: &QuoteRequest) -> Result<(), JupiterError> {
758        self.validate_mint_address(&request.input_mint)
759            .map_err(|e| JupiterError::Error(format!("{:?}", e)))?;
760        self.validate_mint_address(&request.output_mint)
761            .map_err(|e| JupiterError::Error(format!("{:?}", e)))?;
762        validate_slippage_bps(request.slippage_bps)
763            .map_err(|e| JupiterError::Error(format!("{:?}", e)))?;
764        if request.amount == 0 {
765            return Err(JupiterError::InvalidInput(
766                "Amount must be greater than 0".to_string(),
767            ));
768        }
769        Ok(())
770    }
771
772    fn validate_swap_request(&self, request: &SwapRequest) -> Result<(), JupiterError> {
773        self.validate_pubkey(&request.user_public_key)?;
774        Ok(())
775    }
776
777    fn validate_mint_address(&self, address: &str) -> Result<(), JupiterError> {
778        if !is_valid_mint_address(address) {
779            return Err(JupiterError::InvalidInput(format!(
780                "Invalid mint address: {}",
781                address
782            )));
783        }
784        Ok(())
785    }
786
787    fn validate_pubkey(&self, pubkey: &str) -> Result<(), JupiterError> {
788        validate_pubkey(pubkey)
789            .map_err(|e| JupiterError::InvalidInput(format!("Invalid public key: {}", e)))?;
790        Ok(())
791    }
792}