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#[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), }
52 }
53}
54
55pub struct JupiterClient {
57 client: Client,
58 base_url: String,
59 config: ClientConfig,
60 solana: Solana,
61}
62
63impl JupiterClient {
64 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 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 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 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 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 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 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 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 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 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 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(¶ms)
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 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(¶ms)
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 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 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 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 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 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 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 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 pub async fn get_prices_batch(
573 &self,
574 token_pairs: &[(&str, &str)], ) -> 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(¶ms)
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 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 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 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 pub async fn estimate_transaction_fee(
692 &self,
693 quote: &QuoteResponse,
694 priority_fee: Option<u64>,
695 ) -> Result<u64, JupiterError> {
696 let base_fee = 5000; let compute_units = match quote.route_plan.len() {
700 1 => 100_000, 2 => 150_000, _ => 200_000, };
704 let total_fee = base_fee * compute_units / 1_000_000; let priority_fee = priority_fee.unwrap_or(0);
706 Ok(total_fee + priority_fee)
707 }
708
709 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 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}