// file: crates/ksp-offchain-transport-lib/src/market_price_dexscreener.rs // version: 2 //! DexScreener SOL/USD adapter bound to one explicitly configured Solana pair address. const DEXSCREENER_PAIR_BASE_URL: &str = "https://api.dexscreener.com/latest/dex/pairs/solana"; const DEXSCREENER_PROVIDER_ID: &str = "dexscreener"; const DEXSCREENER_SOL_MINT: &str = "So11111111111111111111111111111111111111112"; /// Runtime settings for the keyless DexScreener market-price adapter. #[derive(Clone, Debug, Eq, PartialEq)] pub struct MarketPriceDexScreenerSettings { common: crate::MarketPriceProviderCommonSettings, pair_address: std::option::Option, } impl crate::MarketPriceDexScreenerSettings { /// Creates DexScreener settings for one optional explicit Solana pair address. /// /// An enabled provider requires a valid pair address. A disabled provider may omit it so Config does not need to fabricate provider data merely to keep the /// provider visible in the generic registry. pub fn new(enabled: bool, pair_address: std::option::Option) -> ksp_core_lib::Result { let provider_id = match crate::MarketPriceProviderId::new(DEXSCREENER_PROVIDER_ID) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let pair_address = match pair_address { std::option::Option::Some(value) => match value.parse::() { std::result::Result::Ok(value) => std::option::Option::Some(value), std::result::Result::Err(_) => return std::result::Result::Err(provider_settings_error("pair_address")), }, std::option::Option::None if enabled => return std::result::Result::Err(provider_settings_error("pair_address")), std::option::Option::None => std::option::Option::None, }; let common = crate::MarketPriceProviderCommonSettings::new(provider_id, enabled); return std::result::Result::Ok(Self { common, pair_address }); } /// Returns common provider identity and enablement settings. #[must_use] pub const fn common(&self) -> &crate::MarketPriceProviderCommonSettings { return &self.common; } /// Returns the explicit Solana pair address when one is configured. #[must_use] pub const fn pair_address(&self) -> std::option::Option<&ksp_core_lib::Pubkey> { return self.pair_address.as_ref(); } } /// DexScreener SOL/USD provider adapter for one configured pair. pub struct MarketPriceDexScreenerProvider { admission: crate::HttpAdmissionController, descriptor: crate::MarketPriceProviderDescriptor, http: crate::HttpRestClient, settings: crate::MarketPriceDexScreenerSettings, } impl crate::MarketPriceDexScreenerProvider { /// Builds one DexScreener provider from validated runtime settings. pub fn new(settings: crate::MarketPriceDexScreenerSettings) -> 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 DexScreener capability descriptor. #[must_use] pub const fn descriptor(&self) -> &crate::MarketPriceProviderDescriptor { return &self.descriptor; } /// Returns the validated DexScreener settings including the safe configured pair address. #[must_use] pub const fn settings(&self) -> &crate::MarketPriceDexScreenerSettings { return &self.settings; } /// Fetches one normalized SOL/USD observation from the configured DexScreener pair. 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(DEXSCREENER_PROVIDER_ID)); } if let std::result::Result::Err(error) = crate::admit_request(DEXSCREENER_PROVIDER_ID, &self.admission) { return std::result::Result::Err(error); } let pair_address = match self.settings.pair_address() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(provider_settings_error("pair_address")), }; 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(pair_address) { 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, DEXSCREENER_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(), pair_address, request_started_at, received_at); } } fn build_request(pair_address: &ksp_core_lib::Pubkey) -> ksp_core_lib::Result { let mut request = match crate::HttpGetRequest::new_https(DEXSCREENER_PAIR_BASE_URL) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let pair_address = pair_address.to_string(); if let std::result::Result::Err(error) = request.append_path_segment(pair_address.as_str()) { return std::result::Result::Err(error); } return std::result::Result::Ok(request); } fn descriptor_for(provider_id: crate::MarketPriceProviderId) -> ksp_core_lib::Result { let rate_limit = match crate::MarketPriceProviderRateLimit::fixed(300, 60, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Unspecified) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return crate::MarketPriceProviderDescriptor::new( provider_id, "DexScreener", crate::MarketPriceSemantics::DexPairUsd, crate::MarketPriceProviderAuthMode::None, rate_limit, std::option::Option::None, true, ); } fn parse_response( bytes: &[u8], provider_id: crate::MarketPriceProviderId, configured_pair_address: &ksp_core_lib::Pubkey, 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(DEXSCREENER_PROVIDER_ID, "response", error)); }, }; let pairs = match wire.pairs { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs")), }; if pairs.len() != 1 { return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs")); } let pair = &pairs[0]; if pair.chain_id != "solana" { return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs.chainId")); } let response_pair_address = match pair.pair_address.parse::() { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs.pairAddress")), }; if response_pair_address != *configured_pair_address { return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs.pairAddress")); } let base_token = match &pair.base_token { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs.baseToken")), }; if base_token.address != DEXSCREENER_SOL_MINT { return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs.baseToken.address")); } let price_raw = match pair.price_usd.as_deref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(DEXSCREENER_PROVIDER_ID, "pairs.priceUsd")), }; let price = match crate::MarketPriceDecimal::parse_json_raw(price_raw) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let provenance = match crate::MarketPriceProvenance::new(std::format!("dexscreener:solana:{configured_pair_address}:priceUsd")) { 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::DexPairUsd, request_started_at, received_at, std::option::Option::None, provenance, ); } fn provider_settings_error(field: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_PROVIDER_SETTINGS_INVALID, "DexScreener market-price settings are invalid") .with_context("provider", DEXSCREENER_PROVIDER_ID) .with_context("field", field); } #[derive(serde::Deserialize)] struct DexScreenerWirePair { #[serde(rename = "baseToken")] base_token: std::option::Option, #[serde(rename = "chainId")] chain_id: std::string::String, #[serde(rename = "pairAddress")] pair_address: std::string::String, #[serde(rename = "priceUsd")] price_usd: std::option::Option>, } #[derive(serde::Deserialize)] struct DexScreenerWireResponse { pairs: std::option::Option>, } #[derive(serde::Deserialize)] struct DexScreenerWireToken { address: std::string::String, } #[cfg(test)] #[path = "../unit_tests/market_price_dexscreener.rs"] mod tests;