1use crate::constants::{DEFAULT_BASE_URL, DEFAULT_TIMEOUT_SECS, MAX_IDS_PER_REQUEST, MAX_REQUESTS_PER_SECOND};
2use crate::types::PriceEntry;
3use anyhow::{Context, Result};
4use governor::clock::DefaultClock;
5use governor::state::{InMemoryState, NotKeyed};
6use governor::{Quota, RateLimiter};
7use std::collections::HashMap;
8use std::num::NonZeroU32;
9use std::sync::Arc;
10use std::time::Duration;
11
12#[derive(Debug, Clone)]
13pub struct JupiterApiClient {
14 http: reqwest::Client,
15 pub(crate) base_url: String,
16 timeout: Duration,
17 rate_limiter: Arc<RateLimiter<NotKeyed, InMemoryState, DefaultClock>>,
18 api_key: String,
19}
20
21impl JupiterApiClient {
22 pub fn new(api_key: String) -> Result<Self> {
23 let quota = Quota::per_second(
24 NonZeroU32::new(MAX_REQUESTS_PER_SECOND).context("Invalid rate limit value")?,
25 );
26 let rate_limiter = Arc::new(RateLimiter::direct(quota));
27
28 Ok(Self {
29 http: reqwest::Client::builder()
30 .build()
31 .context("Failed to build reqwest client")?,
32 base_url: DEFAULT_BASE_URL.to_string(),
33 timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
34 rate_limiter,
35 api_key,
36 })
37 }
38
39 pub async fn get_prices(&self, ids: &[String]) -> Result<HashMap<String, PriceEntry>> {
41 if ids.is_empty() {
42 return Ok(HashMap::new());
43 }
44
45 let mut out = HashMap::with_capacity(ids.len());
46 for (i, chunk) in ids.chunks(MAX_IDS_PER_REQUEST).enumerate() {
47 self.rate_limiter.until_ready().await;
49
50 let url = format!("{}/price/v3", self.base_url);
52
53 let request = self.http
54 .get(&url)
55 .query(&[("ids", chunk.join(","))])
56 .timeout(self.timeout)
57 .header("x-api-key", &self.api_key);
58
59 let resp = request
60 .send()
61 .await
62 .with_context(|| format!("Failed to send request for batch {}", i + 1))?
63 .error_for_status()
64 .with_context(|| format!("Price API returned error for batch {}", i + 1))?;
65
66 let price_response: HashMap<String, PriceEntry> = resp
67 .json()
68 .await
69 .with_context(|| format!("Failed to parse response for batch {}", i + 1))?;
70
71 out.extend(price_response);
72 }
73
74 Ok(out)
75 }
76}