515 lines
19 KiB
Rust
515 lines
19 KiB
Rust
// 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<u32>,
|
|
kind: crate::MarketPriceProviderRateLimitKind,
|
|
requests: std::option::Option<u32>,
|
|
scope: crate::MarketPriceProviderRateLimitScope,
|
|
window_seconds: std::option::Option<u32>,
|
|
}
|
|
|
|
impl MarketPriceProviderRateLimit {
|
|
/// Creates a validated fixed request limit.
|
|
pub fn fixed(
|
|
requests: u32,
|
|
window_seconds: u32,
|
|
burst: std::option::Option<u32>,
|
|
scope: crate::MarketPriceProviderRateLimitScope,
|
|
) -> ksp_core_lib::Result<Self> {
|
|
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<u32> {
|
|
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<u32> {
|
|
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<u32> {
|
|
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<Self> {
|
|
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<Self> {
|
|
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<std::string::String>) -> ksp_core_lib::Result<Self> {
|
|
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<std::string::String> for MarketPriceProviderId {
|
|
type Error = ksp_core_lib::Error;
|
|
|
|
fn try_from(value: std::string::String) -> std::result::Result<Self, Self::Error> {
|
|
return crate::MarketPriceProviderId::new(value);
|
|
}
|
|
}
|
|
|
|
impl std::convert::From<MarketPriceProviderId> 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<crate::MarketPriceProviderLongTermQuota>,
|
|
rate_limit: crate::MarketPriceProviderRateLimit,
|
|
semantics: crate::MarketPriceSemantics,
|
|
sol_usd_request_cost: std::option::Option<crate::MarketPriceProviderRequestCost>,
|
|
supports_sol_usd: bool,
|
|
}
|
|
|
|
impl MarketPriceProviderDescriptor {
|
|
/// Creates a validated provider-neutral descriptor.
|
|
pub fn new(
|
|
id: crate::MarketPriceProviderId,
|
|
display_name: impl std::convert::Into<std::string::String>,
|
|
semantics: crate::MarketPriceSemantics,
|
|
auth_mode: crate::MarketPriceProviderAuthMode,
|
|
rate_limit: crate::MarketPriceProviderRateLimit,
|
|
long_term_quota: std::option::Option<crate::MarketPriceProviderLongTermQuota>,
|
|
supports_sol_usd: bool,
|
|
) -> ksp_core_lib::Result<Self> {
|
|
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<Self> {
|
|
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<crate::MarketPriceProviderLongTermQuota> {
|
|
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<crate::MarketPriceProviderRequestCost> {
|
|
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<crate::MarketPriceTimestamp>,
|
|
},
|
|
}
|
|
|
|
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<crate::MarketPriceTimestamp> {
|
|
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;
|