// file: crates/ksp-offchain-transport-lib/src/market_price_birdeye.rs // version: 1 //! Birdeye SOL/USD market-price adapter using the official Price Single REST endpoint. const BIRDEYE_API_KEY_HEADER: &str = "x-api-key"; const BIRDEYE_CHAIN_HEADER: &str = "x-chain"; const BIRDEYE_PRICE_URL: &str = "https://public-api.birdeye.so/defi/price"; const BIRDEYE_PROVIDER_ID: &str = "birdeye"; const BIRDEYE_SOL_MINT: &str = "So11111111111111111111111111111111111111112"; /// Runtime settings for the Birdeye Standard market-price adapter. pub struct MarketPriceBirdeyeSettings { api_key: std::option::Option, common: crate::MarketPriceProviderCommonSettings, } impl crate::MarketPriceBirdeyeSettings { /// Creates Birdeye settings. An API key is mandatory while the provider is enabled. pub fn new(enabled: bool, api_key: std::option::Option) -> ksp_core_lib::Result { let provider_id = match crate::MarketPriceProviderId::new(BIRDEYE_PROVIDER_ID) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let api_key = match api_key { std::option::Option::Some(value) => match crate::MarketPriceApiKey::new(BIRDEYE_PROVIDER_ID, value) { std::result::Result::Ok(value) => std::option::Option::Some(value), std::result::Result::Err(error) => return std::result::Result::Err(error), }, std::option::Option::None if enabled => return std::result::Result::Err(provider_settings_error("api_key")), std::option::Option::None => std::option::Option::None, }; let common = crate::MarketPriceProviderCommonSettings::new(provider_id, enabled); return std::result::Result::Ok(Self { api_key, common }); } /// Returns common provider identity and enablement settings. #[must_use] pub const fn common(&self) -> &crate::MarketPriceProviderCommonSettings { return &self.common; } fn api_key(&self) -> std::option::Option<&crate::MarketPriceApiKey> { return self.api_key.as_ref(); } } impl std::fmt::Debug for crate::MarketPriceBirdeyeSettings { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("MarketPriceBirdeyeSettings") .field("api_key_present", &self.api_key.is_some()) .field("common", &self.common) .finish(); } } /// Birdeye SOL/USD provider adapter backed by the Standard Price Single endpoint. pub struct MarketPriceBirdeyeProvider { admission: crate::HttpAdmissionController, descriptor: crate::MarketPriceProviderDescriptor, http: crate::HttpRestClient, settings: crate::MarketPriceBirdeyeSettings, } impl crate::MarketPriceBirdeyeProvider { /// Builds one Birdeye provider from validated runtime settings. pub fn new(settings: crate::MarketPriceBirdeyeSettings) -> 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 Birdeye capability descriptor. #[must_use] pub const fn descriptor(&self) -> &crate::MarketPriceProviderDescriptor { return &self.descriptor; } /// Returns the validated Birdeye settings without exposing credential material. #[must_use] pub const fn settings(&self) -> &crate::MarketPriceBirdeyeSettings { return &self.settings; } /// Fetches one normalized SOL/USD observation from Birdeye Price Single. 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(BIRDEYE_PROVIDER_ID)); } if let std::result::Result::Err(error) = crate::admit_request(BIRDEYE_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(&self.settings) { 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, BIRDEYE_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(settings: &crate::MarketPriceBirdeyeSettings) -> ksp_core_lib::Result { let mut request = match crate::HttpGetRequest::new_https(BIRDEYE_PRICE_URL) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; request.append_query_pair("address", BIRDEYE_SOL_MINT); if let std::result::Result::Err(error) = request.insert_sensitive_header(BIRDEYE_CHAIN_HEADER, "solana") { return std::result::Result::Err(error); } let api_key = match settings.api_key() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(provider_settings_error("api_key")), }; if let std::result::Result::Err(error) = request.insert_sensitive_header(BIRDEYE_API_KEY_HEADER, api_key.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(1, 1, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Account) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let quota = match crate::MarketPriceProviderLongTermQuota::new( 30_000, crate::MarketPriceProviderQuotaPeriod::Month, crate::MarketPriceProviderQuotaUnit::ComputeUnits, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let request_cost = match crate::MarketPriceProviderRequestCost::new(3, crate::MarketPriceProviderQuotaUnit::ComputeUnits) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let descriptor = match crate::MarketPriceProviderDescriptor::new( provider_id, "Birdeye", crate::MarketPriceSemantics::SolanaSpot, crate::MarketPriceProviderAuthMode::RequiredApiKey, rate_limit, std::option::Option::Some(quota), true, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return descriptor.with_sol_usd_request_cost(request_cost); } 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(BIRDEYE_PROVIDER_ID, "response", error)); }, }; if !wire.success { return std::result::Result::Err(crate::invalid_provider_response(BIRDEYE_PROVIDER_ID, "success")); } let data = match wire.data { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(BIRDEYE_PROVIDER_ID, "data")), }; let price_raw = match data.value.as_deref() { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(BIRDEYE_PROVIDER_ID, "data.value")), }; 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 provider_timestamp = match data.update_unix_time.and_then(crate::market_price_timestamp_from_unix_seconds) { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(BIRDEYE_PROVIDER_ID, "data.updateUnixTime")), }; let provenance = match crate::MarketPriceProvenance::new("birdeye:solana:wsol:value") { 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::SolanaSpot, request_started_at, received_at, std::option::Option::Some(provider_timestamp), 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, "Birdeye market-price settings are invalid") .with_context("provider", BIRDEYE_PROVIDER_ID) .with_context("field", field); } #[derive(serde::Deserialize)] struct BirdeyeWireData { #[serde(rename = "updateUnixTime")] update_unix_time: std::option::Option, value: std::option::Option>, } #[derive(serde::Deserialize)] struct BirdeyeWireResponse { data: std::option::Option, success: bool, } #[cfg(test)] #[path = "../unit_tests/market_price_birdeye.rs"] mod tests;