// file: crates/ksp-offchain-transport-lib/src/market_price_provider.rs // version: 6 /// Maximum UTF-8 byte length of one provider display name. pub const MARKET_PRICE_PROVIDER_DISPLAY_NAME_MAX_BYTES: usize = 96; /// Maximum byte length of one opaque provider identifier. pub const MARKET_PRICE_PROVIDER_ID_MAX_BYTES: usize = 64; /// Only price pair exposed by the `0.2.11` V1 public contract. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum MarketPricePair { /// Native SOL quoted directly in US dollars according to one provider's documented semantics. SolUsd, } impl MarketPricePair { /// Returns the stable human-readable pair code. #[must_use] pub const fn code(&self) -> &'static str { return match self { Self::SolUsd => "SOL/USD", }; } } /// Market-price semantics retained so normalized observations do not imply cross-provider equivalence. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum MarketPriceSemantics { /// Aggregated market price produced by a multi-market data provider. AggregatedMarket, /// USD price associated with one explicitly configured DEX pair. DexPairUsd, /// Last-trade price reported by one centralized exchange market. ExchangeLastTrade, /// Heuristic USD price derived from Solana swap/liquidity activity. SolanaHeuristic, /// Direct Solana-oriented spot price supplied by an on-chain market data provider. SolanaSpot, } /// Generic authentication capability exposed by one configured provider. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum MarketPriceProviderAuthMode { /// No credential is required for the configured access mode. None, /// Provider accepts an API key but also supports an unauthenticated mode selected by configuration. OptionalApiKey, /// An API key is required for the configured access mode. RequiredApiKey, } /// Scope to which a provider documents a request limit. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum MarketPriceProviderRateLimitScope { /// Limit is associated with the configured account or API key. Account, /// Limit is associated with the source IP address. Ip, /// Limit is associated with an organization or project wider than one key. Organization, /// Provider documentation does not expose a stronger stable scope. Unspecified, } /// Shape of one generic provider request-limit capability. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum MarketPriceProviderRateLimitKind { /// Dynamic or server-driven limit that cannot be represented as one safe fixed local cadence. Dynamic, /// Locally enforceable fixed request budget over a documented window. Fixed, } /// Generic provider request-limit capability with validated fixed-limit values. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Serialize)] pub struct MarketPriceProviderRateLimit { burst: std::option::Option, kind: crate::MarketPriceProviderRateLimitKind, requests: std::option::Option, scope: crate::MarketPriceProviderRateLimitScope, window_seconds: std::option::Option, } impl MarketPriceProviderRateLimit { /// Creates a validated fixed request limit. pub fn fixed( requests: u32, window_seconds: u32, burst: std::option::Option, scope: crate::MarketPriceProviderRateLimitScope, ) -> ksp_core_lib::Result { if requests == 0 || window_seconds == 0 || burst == std::option::Option::Some(0) { return std::result::Result::Err(provider_descriptor_error()); } return std::result::Result::Ok(Self { burst, kind: crate::MarketPriceProviderRateLimitKind::Fixed, requests: std::option::Option::Some(requests), scope, window_seconds: std::option::Option::Some(window_seconds), }); } /// Creates a dynamic/server-driven request-limit descriptor. #[must_use] pub const fn dynamic(scope: crate::MarketPriceProviderRateLimitScope) -> Self { return Self { burst: std::option::Option::None, kind: crate::MarketPriceProviderRateLimitKind::Dynamic, requests: std::option::Option::None, scope, window_seconds: std::option::Option::None, }; } /// Returns optional documented burst capacity. #[must_use] pub const fn burst(&self) -> std::option::Option { return self.burst; } /// Returns whether the limit is fixed or dynamic/server-driven. #[must_use] pub const fn kind(&self) -> crate::MarketPriceProviderRateLimitKind { return self.kind; } /// Returns the request budget for a fixed limit, or `None` for a dynamic limit. #[must_use] pub const fn requests(&self) -> std::option::Option { return self.requests; } /// Returns the documented limit scope. #[must_use] pub const fn scope(&self) -> crate::MarketPriceProviderRateLimitScope { return self.scope; } /// Returns the fixed window duration in seconds, or `None` for a dynamic limit. #[must_use] pub const fn window_seconds(&self) -> std::option::Option { return self.window_seconds; } } /// Period used by one documented long-term provider quota. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum MarketPriceProviderQuotaPeriod { /// Quota resets on a provider-defined daily period. Day, /// Quota resets on a provider-defined monthly period. Month, } /// Unit used by one documented long-term provider quota. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum MarketPriceProviderQuotaUnit { /// Provider-defined compute units consumed by API operations. ComputeUnits, /// Provider-specific credits, not assumed to equal HTTP requests. Credits, /// HTTP/API requests. Requests, } /// Long-term provider quota descriptor exposed as non-authoritative capability metadata. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Serialize)] pub struct MarketPriceProviderLongTermQuota { amount: u64, period: crate::MarketPriceProviderQuotaPeriod, unit: crate::MarketPriceProviderQuotaUnit, } impl MarketPriceProviderLongTermQuota { /// Creates a non-zero documented quota descriptor. pub fn new(amount: u64, period: crate::MarketPriceProviderQuotaPeriod, unit: crate::MarketPriceProviderQuotaUnit) -> ksp_core_lib::Result { if amount == 0 { return std::result::Result::Err(provider_descriptor_error()); } return std::result::Result::Ok(Self { amount, period, unit }); } /// Returns the documented amount without treating it as a local remaining counter. #[must_use] pub const fn amount(&self) -> u64 { return self.amount; } /// Returns the provider-defined quota period. #[must_use] pub const fn period(&self) -> crate::MarketPriceProviderQuotaPeriod { return self.period; } /// Returns the documented quota unit. #[must_use] pub const fn unit(&self) -> crate::MarketPriceProviderQuotaUnit { return self.unit; } } /// Informational cost of one normalized SOL/USD request in a provider-defined quota unit. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Serialize)] pub struct MarketPriceProviderRequestCost { amount: u64, unit: crate::MarketPriceProviderQuotaUnit, } impl crate::MarketPriceProviderRequestCost { /// Creates a non-zero informational request-cost descriptor. pub fn new(amount: u64, unit: crate::MarketPriceProviderQuotaUnit) -> ksp_core_lib::Result { if amount == 0 { return std::result::Result::Err(provider_descriptor_error()); } return std::result::Result::Ok(Self { amount, unit }); } /// Returns the documented amount consumed by one SOL/USD request. #[must_use] pub const fn amount(&self) -> u64 { return self.amount; } /// Returns the provider-defined quota unit used by this request cost. #[must_use] pub const fn unit(&self) -> crate::MarketPriceProviderQuotaUnit { return self.unit; } } /// Opaque validated provider identifier owned by Off-chain Transport. #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)] #[serde(try_from = "std::string::String", into = "std::string::String")] pub struct MarketPriceProviderId(std::string::String); impl MarketPriceProviderId { /// Creates one bounded stable provider identifier. pub fn new(value: impl std::convert::Into) -> ksp_core_lib::Result { ksp_logging_lib::trace!(target: crate::TRACING_TARGET, "validating market-price provider identifier"); let value = value.into(); if !valid_provider_id(value.as_str()) { ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = "provider_id", "rejected invalid market-price provider identifier"); return std::result::Result::Err(ksp_core_lib::Error::new( crate::ERROR_CODE_MARKET_PRICE_PROVIDER_ID_INVALID, "invalid off-chain market-price provider identifier", )); } return std::result::Result::Ok(Self(value)); } /// Returns the opaque identifier as a stable string. #[must_use] pub fn as_str(&self) -> &str { return self.0.as_str(); } } impl std::fmt::Display for MarketPriceProviderId { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter.write_str(self.0.as_str()); } } impl std::convert::TryFrom for MarketPriceProviderId { type Error = ksp_core_lib::Error; fn try_from(value: std::string::String) -> std::result::Result { return crate::MarketPriceProviderId::new(value); } } impl std::convert::From for std::string::String { fn from(value: MarketPriceProviderId) -> Self { return value.0; } } /// Provider capability and presentation descriptor consumed by provider-agnostic callers. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct MarketPriceProviderDescriptor { auth_mode: crate::MarketPriceProviderAuthMode, display_name: std::string::String, id: crate::MarketPriceProviderId, long_term_quota: std::option::Option, rate_limit: crate::MarketPriceProviderRateLimit, semantics: crate::MarketPriceSemantics, sol_usd_request_cost: std::option::Option, supports_sol_usd: bool, } impl MarketPriceProviderDescriptor { /// Creates a validated provider-neutral descriptor. pub fn new( id: crate::MarketPriceProviderId, display_name: impl std::convert::Into, semantics: crate::MarketPriceSemantics, auth_mode: crate::MarketPriceProviderAuthMode, rate_limit: crate::MarketPriceProviderRateLimit, long_term_quota: std::option::Option, supports_sol_usd: bool, ) -> ksp_core_lib::Result { ksp_logging_lib::trace!(target: crate::TRACING_TARGET, "validating market-price provider descriptor"); let display_name = display_name.into(); if !valid_display_name(display_name.as_str()) { ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = "display_name", "rejected invalid market-price provider descriptor"); return std::result::Result::Err(provider_descriptor_error()); } return std::result::Result::Ok(Self { auth_mode, display_name, id, long_term_quota, rate_limit, semantics, sol_usd_request_cost: std::option::Option::None, supports_sol_usd, }); } /// Attaches informational provider cost metadata for one normalized SOL/USD request. pub fn with_sol_usd_request_cost(mut self, request_cost: crate::MarketPriceProviderRequestCost) -> ksp_core_lib::Result { if let std::option::Option::Some(quota) = self.long_term_quota && quota.unit() != request_cost.unit() { return std::result::Result::Err(provider_descriptor_error()); } self.sol_usd_request_cost = std::option::Option::Some(request_cost); return std::result::Result::Ok(self); } /// Returns the configured authentication capability. #[must_use] pub const fn auth_mode(&self) -> crate::MarketPriceProviderAuthMode { return self.auth_mode; } /// Returns the safe display name. #[must_use] pub fn display_name(&self) -> &str { return self.display_name.as_str(); } /// Returns the opaque provider identifier. #[must_use] pub const fn id(&self) -> &crate::MarketPriceProviderId { return &self.id; } /// Returns optional long-term quota metadata without exposing a local remaining counter. #[must_use] pub const fn long_term_quota(&self) -> std::option::Option { return self.long_term_quota; } /// Returns the configured request-limit capability. #[must_use] pub const fn rate_limit(&self) -> crate::MarketPriceProviderRateLimit { return self.rate_limit; } /// Returns the documented price semantics. #[must_use] pub const fn semantics(&self) -> crate::MarketPriceSemantics { return self.semantics; } /// Returns optional informational provider cost for one normalized SOL/USD request. #[must_use] pub const fn sol_usd_request_cost(&self) -> std::option::Option { return self.sol_usd_request_cost; } /// Reports whether this descriptor can serve the V1 SOL/USD pair. #[must_use] pub const fn supports_sol_usd(&self) -> bool { return self.supports_sol_usd; } } /// Generic runtime availability state exposed without provider-specific error parsing. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(tag = "state", rename_all = "snake_case")] pub enum MarketPriceProviderAvailability { /// Required authentication material is unavailable or rejected. AuthenticationUnavailable, /// Provider is locally cooling down until the supplied timestamp. CoolingDown { /// Earliest known wall-clock timestamp at which a new attempt may be admitted. retry_at: crate::MarketPriceTimestamp, }, /// Provider is disabled by runtime configuration. Disabled, /// Runtime settings do not satisfy the provider adapter contract. Misconfigured, /// Provider-reported quota prevents current use. QuotaUnavailable, /// Provider is eligible for a new request. Ready, /// Transport/provider failure is transient; retry time is present only when actually known. TemporarilyUnavailable { /// Optional next retry timestamp derived from safe runtime/provider information. retry_at: std::option::Option, }, } impl crate::MarketPriceProviderAvailability { /// Reports whether a generic refresh may be attempted immediately. #[must_use] pub const fn is_refresh_eligible(&self) -> bool { return matches!(self, Self::Ready); } /// Reports whether a refresh may be attempted at the supplied wall-clock timestamp. #[must_use] pub fn is_refresh_eligible_at(&self, now: crate::MarketPriceTimestamp) -> bool { return match self { Self::Ready => true, Self::CoolingDown { retry_at } => *retry_at <= now, Self::TemporarilyUnavailable { retry_at: std::option::Option::Some(retry_at) } => *retry_at <= now, Self::TemporarilyUnavailable { retry_at: std::option::Option::None } => true, Self::AuthenticationUnavailable | Self::Disabled | Self::Misconfigured | Self::QuotaUnavailable => false, }; } /// Returns the known next retry timestamp for cooling-down or temporary states. #[must_use] pub const fn retry_at(&self) -> std::option::Option { return match self { Self::CoolingDown { retry_at } => std::option::Option::Some(*retry_at), Self::TemporarilyUnavailable { retry_at } => *retry_at, Self::AuthenticationUnavailable | Self::Disabled | Self::Misconfigured | Self::QuotaUnavailable | Self::Ready => std::option::Option::None, }; } } /// Current provider-neutral runtime state projection. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct MarketPriceProviderState { availability: crate::MarketPriceProviderAvailability, provider_id: crate::MarketPriceProviderId, } impl MarketPriceProviderState { /// Creates one generic state projection for a configured provider. #[must_use] pub fn new(provider_id: crate::MarketPriceProviderId, availability: crate::MarketPriceProviderAvailability) -> Self { return Self { availability, provider_id }; } /// Returns the generic availability classification. #[must_use] pub const fn availability(&self) -> crate::MarketPriceProviderAvailability { return self.availability; } /// Returns the opaque provider identifier. #[must_use] pub const fn provider_id(&self) -> &crate::MarketPriceProviderId { return &self.provider_id; } /// Replaces the internal availability projection while preserving provider identity. pub(crate) fn set_availability(&mut self, availability: crate::MarketPriceProviderAvailability) { self.availability = availability; } } fn provider_descriptor_error() -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_PROVIDER_DESCRIPTOR_INVALID, "invalid off-chain market-price provider descriptor"); } fn valid_display_name(value: &str) -> bool { if value.is_empty() || value.len() > crate::MARKET_PRICE_PROVIDER_DISPLAY_NAME_MAX_BYTES || value.trim() != value { return false; } for character in value.chars() { if character.is_control() { return false; } } return true; } fn valid_provider_id(value: &str) -> bool { if value.is_empty() || value.len() > crate::MARKET_PRICE_PROVIDER_ID_MAX_BYTES { return false; } for byte in value.bytes() { if !(byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-' || byte == b'_') { return false; } } return true; } #[cfg(test)] #[path = "../unit_tests/market_price_provider.rs"] mod tests;