aignt-jupiter-client 0.3.0

Jupiter API Client
Documentation
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,
        })
    }

    /// Fetch prices for any number of ids (array of strings). Will batch by 50 per request.
    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() {
            // Rate limiter
            self.rate_limiter.until_ready().await;

            // Build URL: https://api.jup.ag/price/v3?ids=<comma-separated>
            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)
    }
}