Files
khadhroony-solana-project/crates/ksp-offchain-transport-lib/src/market_price_coinpaprika.rs
2026-08-25 21:47:47 +02:00

188 lines
8.0 KiB
Rust

// 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<Self> {
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<Self> {
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<crate::MarketPriceObservation> {
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<crate::HttpGetRequest> {
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<crate::MarketPriceProviderDescriptor> {
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<crate::MarketPriceObservation> {
let wire = match serde_json::from_slice::<CoinPaprikaWireResponse>(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<serde_json::value::RawValue>,
}
#[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;