// file: crates/ksp-offchain-transport-lib/src/market_price_coinpaprika.rs // version: 1 //! CoinPaprika SOL/USD market-price adapter using the free official REST API directly through `reqwest`. const COINPAPRIKA_PROVIDER_ID: &str = "coinpaprika"; const COINPAPRIKA_SOL_ID: &str = "sol-solana"; const COINPAPRIKA_SOL_TICKER_URL: &str = "https://api.coinpaprika.com/v1/tickers/sol-solana"; /// Runtime settings for the keyless CoinPaprika market-price adapter. #[derive(Clone, Debug, Eq, PartialEq)] pub struct MarketPriceCoinPaprikaSettings { common: crate::MarketPriceProviderCommonSettings, } impl crate::MarketPriceCoinPaprikaSettings { /// Creates CoinPaprika settings for the free keyless REST surface. pub fn new(enabled: bool) -> ksp_core_lib::Result { let provider_id = match crate::MarketPriceProviderId::new(COINPAPRIKA_PROVIDER_ID) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(Self { common: crate::MarketPriceProviderCommonSettings::new(provider_id, enabled) }); } /// Returns common provider identity and enablement settings. #[must_use] pub const fn common(&self) -> &crate::MarketPriceProviderCommonSettings { return &self.common; } } /// CoinPaprika SOL/USD provider adapter. pub struct MarketPriceCoinPaprikaProvider { admission: crate::HttpAdmissionController, descriptor: crate::MarketPriceProviderDescriptor, http: crate::HttpRestClient, settings: crate::MarketPriceCoinPaprikaSettings, } impl crate::MarketPriceCoinPaprikaProvider { /// Builds one CoinPaprika provider from validated runtime settings. pub fn new(settings: crate::MarketPriceCoinPaprikaSettings) -> ksp_core_lib::Result { let descriptor = match descriptor_for(settings.common().provider_id().clone()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let runtime = match crate::provider_http_runtime(descriptor.rate_limit()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(Self { admission: runtime.1, descriptor, http: runtime.0, settings }); } /// Returns the provider-neutral CoinPaprika capability descriptor. #[must_use] pub const fn descriptor(&self) -> &crate::MarketPriceProviderDescriptor { return &self.descriptor; } /// Returns the validated CoinPaprika runtime settings. #[must_use] pub const fn settings(&self) -> &crate::MarketPriceCoinPaprikaSettings { return &self.settings; } /// Fetches one normalized SOL/USD observation from CoinPaprika. pub async fn fetch_sol_usd(&self) -> ksp_core_lib::Result { if !self.settings.common().enabled() { return std::result::Result::Err(crate::provider_disabled_error(COINPAPRIKA_PROVIDER_ID)); } if let std::result::Result::Err(error) = crate::admit_request(COINPAPRIKA_PROVIDER_ID, &self.admission) { return std::result::Result::Err(error); } let request_started_at = match crate::current_timestamp() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let request = match build_request() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let document = match crate::get_json(&self.http, &self.admission, COINPAPRIKA_PROVIDER_ID, request).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let received_at = match crate::current_timestamp() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return parse_response(document.as_bytes(), self.settings.common().provider_id().clone(), request_started_at, received_at); } } fn build_request() -> ksp_core_lib::Result { let mut request = match crate::HttpGetRequest::new_https(COINPAPRIKA_SOL_TICKER_URL) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; request.append_query_pair("quotes", "USD"); return std::result::Result::Ok(request); } fn descriptor_for(provider_id: crate::MarketPriceProviderId) -> ksp_core_lib::Result { let rate_limit = match crate::MarketPriceProviderRateLimit::fixed(10, 1, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Ip) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let quota = match crate::MarketPriceProviderLongTermQuota::new(20_000, crate::MarketPriceProviderQuotaPeriod::Month, crate::MarketPriceProviderQuotaUnit::Requests) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return crate::MarketPriceProviderDescriptor::new( provider_id, "CoinPaprika", crate::MarketPriceSemantics::AggregatedMarket, crate::MarketPriceProviderAuthMode::None, rate_limit, std::option::Option::Some(quota), true, ); } fn parse_response( bytes: &[u8], provider_id: crate::MarketPriceProviderId, request_started_at: crate::MarketPriceTimestamp, received_at: crate::MarketPriceTimestamp, ) -> ksp_core_lib::Result { let wire = match serde_json::from_slice::(bytes) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(crate::invalid_provider_response_with_source(COINPAPRIKA_PROVIDER_ID, "response", error)); }, }; if wire.id != COINPAPRIKA_SOL_ID || wire.symbol != "SOL" { return std::result::Result::Err(crate::invalid_provider_response(COINPAPRIKA_PROVIDER_ID, "identity")); } let price = match crate::MarketPriceDecimal::parse_json_raw(wire.quotes.usd.price.as_ref()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let provider_timestamp = match crate::market_price_timestamp_from_rfc3339(wire.last_updated.as_str()) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(crate::invalid_provider_response(COINPAPRIKA_PROVIDER_ID, "last_updated")); }, }; let provenance = match crate::MarketPriceProvenance::new("coinpaprika:sol-solana:usd") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return crate::MarketPriceObservation::new( provider_id, price, crate::MarketPriceSemantics::AggregatedMarket, request_started_at, received_at, std::option::Option::Some(provider_timestamp), provenance, ); } #[derive(serde::Deserialize)] struct CoinPaprikaWireQuote { price: std::boxed::Box, } #[derive(serde::Deserialize)] struct CoinPaprikaWireQuotes { #[serde(rename = "USD")] usd: CoinPaprikaWireQuote, } #[derive(serde::Deserialize)] struct CoinPaprikaWireResponse { id: std::string::String, last_updated: std::string::String, quotes: CoinPaprikaWireQuotes, symbol: std::string::String, } #[cfg(test)] #[path = "../unit_tests/market_price_coinpaprika.rs"] mod tests;