use crate::constants::{DEFAULT_BASE_URL, DEFAULT_TIMEOUT_SECS, MAX_IDS_PER_REQUEST, MAX_REQUESTS_PER_SECOND};
use crate::types::PriceEntry;
use anyhow::{Context, Result};
use governor::clock::DefaultClock;
use governor::state::{InMemoryState, NotKeyed};
use governor::{Quota, RateLimiter};
use std::collections::HashMap;
use std::num::NonZeroU32;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct JupiterApiClient {
http: reqwest::Client,
pub(crate) base_url: String,
timeout: Duration,
rate_limiter: Arc<RateLimiter<NotKeyed, InMemoryState, DefaultClock>>,
api_key: String,
}
impl JupiterApiClient {
pub fn new(api_key: String) -> Result<Self> {
let quota = Quota::per_second(
NonZeroU32::new(MAX_REQUESTS_PER_SECOND).context("Invalid rate limit value")?,
);
let rate_limiter = Arc::new(RateLimiter::direct(quota));
Ok(Self {
http: reqwest::Client::builder()
.build()
.context("Failed to build reqwest client")?,
base_url: DEFAULT_BASE_URL.to_string(),
timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
rate_limiter,
api_key,
})
}
pub async fn get_prices(&self, ids: &[String]) -> Result<HashMap<String, PriceEntry>> {
if ids.is_empty() {
return Ok(HashMap::new());
}
let mut out = HashMap::with_capacity(ids.len());
for (i, chunk) in ids.chunks(MAX_IDS_PER_REQUEST).enumerate() {
self.rate_limiter.until_ready().await;
let url = format!("{}/price/v3", self.base_url);
let request = self.http
.get(&url)
.query(&[("ids", chunk.join(","))])
.timeout(self.timeout)
.header("x-api-key", &self.api_key);
let resp = request
.send()
.await
.with_context(|| format!("Failed to send request for batch {}", i + 1))?
.error_for_status()
.with_context(|| format!("Price API returned error for batch {}", i + 1))?;
let price_response: HashMap<String, PriceEntry> = resp
.json()
.await
.with_context(|| format!("Failed to parse response for batch {}", i + 1))?;
out.extend(price_response);
}
Ok(out)
}
}