Skip to main content

jupiter_client/
client.rs

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    /// Fetch prices for any number of ids (array of strings). Will batch by 50 per request.
40    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            // Rate limiter
48            self.rate_limiter.until_ready().await;
49
50            // Build URL: https://api.jup.ag/price/v3?ids=<comma-separated>
51            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}