v0.2.11-pre.008
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/src/error.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
/// Stable off-chain transport error for HTTP 401/403 access denial.
|
||||
pub const ERROR_CODE_HTTP_ACCESS_DENIED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "http_access_denied");
|
||||
@@ -41,12 +41,17 @@ pub const ERROR_CODE_MARKET_PRICE_PROVIDER_DISABLED: ksp_core_lib::ErrorCode =
|
||||
/// Stable off-chain transport error for an invalid market-price provider identifier.
|
||||
pub const ERROR_CODE_MARKET_PRICE_PROVIDER_ID_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("offchain_transport", "market_price_provider_id_invalid");
|
||||
/// Stable off-chain transport error when a requested provider is absent from the configured service.
|
||||
pub const ERROR_CODE_MARKET_PRICE_PROVIDER_NOT_FOUND: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("offchain_transport", "market_price_provider_not_found");
|
||||
/// Stable off-chain transport error when a provider response violates its adapter contract.
|
||||
pub const ERROR_CODE_MARKET_PRICE_PROVIDER_RESPONSE_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("offchain_transport", "market_price_provider_response_invalid");
|
||||
/// Stable off-chain transport error for invalid common market-price provider settings.
|
||||
pub const ERROR_CODE_MARKET_PRICE_PROVIDER_SETTINGS_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("offchain_transport", "market_price_provider_settings_invalid");
|
||||
/// Stable off-chain transport error for an invalid generic market-price refresh request.
|
||||
pub const ERROR_CODE_MARKET_PRICE_REFRESH_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "market_price_refresh_invalid");
|
||||
/// Stable off-chain transport error for an invalid market-price provider registry.
|
||||
pub const ERROR_CODE_MARKET_PRICE_REGISTRY_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("offchain_transport", "market_price_registry_invalid");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/src/lib.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -7,10 +7,10 @@
|
||||
|
||||
//! KSP-owned off-chain transport foundation.
|
||||
//!
|
||||
//! `0.2.11-pre.007` completes the eight-provider SOL/USD adapter inventory with Birdeye and materializes the provider-neutral runtime registry.
|
||||
//! Birdeye uses its authenticated Solana Price Single endpoint, retains provider update time, and models both the Standard account cadence and informational
|
||||
//! compute-unit quota/cost metadata. Registry projections expose descriptors and generic availability without provider-specific branching. Refresh dispatch and
|
||||
//! Config integration remain outside this tranche.
|
||||
//! `0.2.11-pre.008` adds the generic SOL/USD refresh service over the complete eight-provider adapter inventory. The service owns provider dispatch,
|
||||
//! non-blocking rate-limit/cooldown handling, provider-neutral availability transitions and individual/multiple/all refresh operations. Provider-specific setup
|
||||
//! remains confined to composition while runtime consumers operate only on opaque provider identifiers, registry projections and normalized outcomes. Config
|
||||
//! integration remains reserved for the next tranche.
|
||||
|
||||
mod constants;
|
||||
mod error;
|
||||
@@ -31,6 +31,7 @@ mod market_price_kraken;
|
||||
mod market_price_observation;
|
||||
mod market_price_provider;
|
||||
mod market_price_registry;
|
||||
mod market_price_service;
|
||||
mod market_price_settings;
|
||||
|
||||
/// Stable error code for HTTP access denial.
|
||||
@@ -69,10 +70,14 @@ pub use self::error::ERROR_CODE_MARKET_PRICE_PROVIDER_DESCRIPTOR_INVALID;
|
||||
pub use self::error::ERROR_CODE_MARKET_PRICE_PROVIDER_DISABLED;
|
||||
/// Stable error code for an invalid provider identifier.
|
||||
pub use self::error::ERROR_CODE_MARKET_PRICE_PROVIDER_ID_INVALID;
|
||||
/// Stable error code when a generic refresh targets an unconfigured provider.
|
||||
pub use self::error::ERROR_CODE_MARKET_PRICE_PROVIDER_NOT_FOUND;
|
||||
/// Stable error code for a provider response that violates its adapter contract.
|
||||
pub use self::error::ERROR_CODE_MARKET_PRICE_PROVIDER_RESPONSE_INVALID;
|
||||
/// Stable error code for invalid common provider settings.
|
||||
pub use self::error::ERROR_CODE_MARKET_PRICE_PROVIDER_SETTINGS_INVALID;
|
||||
/// Stable error code for an invalid generic refresh request.
|
||||
pub use self::error::ERROR_CODE_MARKET_PRICE_REFRESH_INVALID;
|
||||
/// Stable error code for an invalid provider registry.
|
||||
pub use self::error::ERROR_CODE_MARKET_PRICE_REGISTRY_INVALID;
|
||||
/// Birdeye Standard SOL/USD provider adapter.
|
||||
@@ -163,6 +168,12 @@ pub use self::market_price_provider::MarketPriceSemantics;
|
||||
pub use self::market_price_registry::MarketPriceProviderRegistry;
|
||||
/// Immutable descriptor-plus-state entry exposed by the configured provider registry.
|
||||
pub use self::market_price_registry::MarketPriceProviderRegistryEntry;
|
||||
/// Provider-specific setup accepted once by the generic market-price service.
|
||||
pub use self::market_price_service::MarketPriceProviderSetup;
|
||||
/// Generic result of one market-price refresh.
|
||||
pub use self::market_price_service::MarketPriceRefreshOutcome;
|
||||
/// Provider-agnostic market-price refresh service.
|
||||
pub use self::market_price_service::MarketPriceService;
|
||||
/// Common provider settings shared by provider-specific runtime settings.
|
||||
pub use self::market_price_settings::MarketPriceProviderCommonSettings;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/src/market_price_provider.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
/// Maximum UTF-8 byte length of one provider display name.
|
||||
pub const MARKET_PRICE_PROVIDER_DISPLAY_NAME_MAX_BYTES: usize = 96;
|
||||
@@ -418,6 +418,18 @@ impl crate::MarketPriceProviderAvailability {
|
||||
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> {
|
||||
@@ -454,6 +466,11 @@ impl MarketPriceProviderState {
|
||||
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 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/src/market_price_registry.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Provider-neutral registry projection for configured market-price providers.
|
||||
|
||||
@@ -25,6 +25,11 @@ impl crate::MarketPriceProviderRegistryEntry {
|
||||
return &self.state;
|
||||
}
|
||||
|
||||
/// Replaces this entry's generic availability projection for the owning runtime service.
|
||||
pub(crate) fn set_availability(&mut self, availability: crate::MarketPriceProviderAvailability) {
|
||||
self.state.set_availability(availability);
|
||||
}
|
||||
|
||||
/// Creates one generic registry entry while keeping descriptor and state identity synchronized.
|
||||
#[must_use]
|
||||
pub fn new(descriptor: crate::MarketPriceProviderDescriptor, availability: crate::MarketPriceProviderAvailability) -> Self {
|
||||
@@ -80,6 +85,20 @@ impl crate::MarketPriceProviderRegistry {
|
||||
return self.entry(provider_id).map(crate::MarketPriceProviderRegistryEntry::state);
|
||||
}
|
||||
|
||||
/// Updates one existing provider availability and reports whether the provider was present.
|
||||
pub(crate) fn set_availability(&mut self, provider_id: &crate::MarketPriceProviderId, availability: crate::MarketPriceProviderAvailability) -> bool {
|
||||
let index = self.entries.binary_search_by(|entry| return entry.descriptor().id().cmp(provider_id));
|
||||
let entry = match index {
|
||||
std::result::Result::Ok(value) => self.entries.get_mut(value),
|
||||
std::result::Result::Err(_) => std::option::Option::None,
|
||||
};
|
||||
if let std::option::Option::Some(entry) = entry {
|
||||
entry.set_availability(availability);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Builds one bounded deterministic registry and rejects duplicate provider identifiers.
|
||||
pub fn new(mut entries: std::vec::Vec<crate::MarketPriceProviderRegistryEntry>) -> ksp_core_lib::Result<Self> {
|
||||
if entries.len() > MARKET_PRICE_PROVIDER_REGISTRY_MAX_ENTRIES {
|
||||
|
||||
386
crates/ksp-offchain-transport-lib/src/market_price_service.rs
Normal file
386
crates/ksp-offchain-transport-lib/src/market_price_service.rs
Normal file
@@ -0,0 +1,386 @@
|
||||
// file: crates/ksp-offchain-transport-lib/src/market_price_service.rs
|
||||
// version: 1
|
||||
|
||||
//! Generic market-price refresh service owning provider dispatch and availability transitions.
|
||||
|
||||
const MARKET_PRICE_RATE_LIMIT_FALLBACK_MILLIS: u64 = 1_000;
|
||||
const MARKET_PRICE_REFRESH_MAX_PROVIDERS: usize = 64;
|
||||
|
||||
/// Provider-specific runtime setup consumed once by [`crate::MarketPriceService`].
|
||||
///
|
||||
/// This enum is intended for composition layers such as Config. Runtime consumers use the generic service methods and never need to branch on provider kinds.
|
||||
pub enum MarketPriceProviderSetup {
|
||||
/// Birdeye Standard setup.
|
||||
Birdeye(crate::MarketPriceBirdeyeSettings),
|
||||
/// Coinbase Exchange setup.
|
||||
CoinbaseExchange(crate::MarketPriceCoinbaseExchangeSettings),
|
||||
/// CoinGecko setup.
|
||||
CoinGecko(crate::MarketPriceCoinGeckoSettings),
|
||||
/// CoinMarketCap setup.
|
||||
CoinMarketCap(crate::MarketPriceCoinMarketCapSettings),
|
||||
/// CoinPaprika setup.
|
||||
CoinPaprika(crate::MarketPriceCoinPaprikaSettings),
|
||||
/// DexScreener setup bound to one explicit Solana pair.
|
||||
DexScreener(crate::MarketPriceDexScreenerSettings),
|
||||
/// Jupiter Price V3 setup.
|
||||
Jupiter(crate::MarketPriceJupiterSettings),
|
||||
/// Kraken Spot setup.
|
||||
Kraken(crate::MarketPriceKrakenSettings),
|
||||
}
|
||||
|
||||
/// Generic result of one explicit market-price refresh attempt or eligibility projection.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct MarketPriceRefreshOutcome {
|
||||
observation: std::option::Option<crate::MarketPriceObservation>,
|
||||
state: crate::MarketPriceProviderState,
|
||||
}
|
||||
|
||||
impl crate::MarketPriceRefreshOutcome {
|
||||
/// Returns the successful normalized observation when this refresh produced one.
|
||||
#[must_use]
|
||||
pub fn observation(&self) -> std::option::Option<&crate::MarketPriceObservation> {
|
||||
return self.observation.as_ref();
|
||||
}
|
||||
|
||||
/// Returns the provider identifier without exposing provider-specific runtime types.
|
||||
#[must_use]
|
||||
pub const fn provider_id(&self) -> &crate::MarketPriceProviderId {
|
||||
return self.state.provider_id();
|
||||
}
|
||||
|
||||
/// Reports whether this refresh produced a new observation.
|
||||
#[must_use]
|
||||
pub fn refreshed(&self) -> bool {
|
||||
return self.observation.is_some();
|
||||
}
|
||||
|
||||
/// Returns the resulting provider-neutral runtime state.
|
||||
#[must_use]
|
||||
pub const fn state(&self) -> &crate::MarketPriceProviderState {
|
||||
return &self.state;
|
||||
}
|
||||
|
||||
fn from_state(state: crate::MarketPriceProviderState) -> Self {
|
||||
return Self { observation: std::option::Option::None, state };
|
||||
}
|
||||
|
||||
fn from_observation(observation: crate::MarketPriceObservation, state: crate::MarketPriceProviderState) -> Self {
|
||||
return Self { observation: std::option::Option::Some(observation), state };
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider-agnostic SOL/USD refresh service owning all configured provider adapters.
|
||||
pub struct MarketPriceService {
|
||||
providers: std::vec::Vec<MarketPriceProviderRuntime>,
|
||||
registry: std::sync::Mutex<crate::MarketPriceProviderRegistry>,
|
||||
}
|
||||
|
||||
impl crate::MarketPriceService {
|
||||
/// Builds the service from provider-specific setup supplied by a composition layer.
|
||||
///
|
||||
/// Provider identifiers must be unique. Initial registry state is `Ready` for enabled providers and `Disabled` for disabled providers.
|
||||
pub fn new(setups: std::vec::Vec<crate::MarketPriceProviderSetup>) -> ksp_core_lib::Result<Self> {
|
||||
if setups.len() > MARKET_PRICE_REFRESH_MAX_PROVIDERS {
|
||||
return std::result::Result::Err(refresh_error("provider_count"));
|
||||
}
|
||||
let mut providers = std::vec::Vec::with_capacity(setups.len());
|
||||
let mut entries = std::vec::Vec::with_capacity(setups.len());
|
||||
for setup in setups {
|
||||
let runtime = match MarketPriceProviderRuntime::new(setup) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let availability = if runtime.enabled() {
|
||||
crate::MarketPriceProviderAvailability::Ready
|
||||
} else {
|
||||
crate::MarketPriceProviderAvailability::Disabled
|
||||
};
|
||||
entries.push(crate::MarketPriceProviderRegistryEntry::new(runtime.descriptor().clone(), availability));
|
||||
providers.push(runtime);
|
||||
}
|
||||
let registry = match crate::MarketPriceProviderRegistry::new(entries) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
providers.sort_by(|left, right| return left.provider_id().cmp(right.provider_id()));
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
provider_count = providers.len(),
|
||||
"created generic market-price refresh service"
|
||||
);
|
||||
return std::result::Result::Ok(Self { providers, registry: std::sync::Mutex::new(registry) });
|
||||
}
|
||||
|
||||
/// Returns a detached provider-neutral registry snapshot suitable for HID projection.
|
||||
#[must_use]
|
||||
pub fn registry(&self) -> crate::MarketPriceProviderRegistry {
|
||||
let guard = lock_registry(&self.registry);
|
||||
return guard.clone();
|
||||
}
|
||||
|
||||
/// Refreshes one provider by generic provider identifier.
|
||||
///
|
||||
/// Non-eligible states are returned without network dispatch. Provider transport/application failures are normalized into availability and returned as an
|
||||
/// outcome rather than forcing consumers to parse provider-specific errors.
|
||||
pub async fn refresh(&self, provider_id: &crate::MarketPriceProviderId) -> ksp_core_lib::Result<crate::MarketPriceRefreshOutcome> {
|
||||
let index = match self.provider_index(provider_id) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(provider_not_found_error(provider_id)),
|
||||
};
|
||||
let now = current_timestamp_or_zero();
|
||||
let state = match self.state_snapshot(provider_id) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(provider_not_found_error(provider_id)),
|
||||
};
|
||||
if !state.availability().is_refresh_eligible_at(now) {
|
||||
return std::result::Result::Ok(crate::MarketPriceRefreshOutcome::from_state(state));
|
||||
}
|
||||
let result = self.providers[index].fetch_sol_usd().await;
|
||||
return match result {
|
||||
std::result::Result::Ok(observation) => {
|
||||
let state = self.update_availability(provider_id, crate::MarketPriceProviderAvailability::Ready);
|
||||
std::result::Result::Ok(crate::MarketPriceRefreshOutcome::from_observation(observation, state))
|
||||
},
|
||||
std::result::Result::Err(error) => {
|
||||
let availability = availability_from_error(&error, self.providers[index].descriptor().auth_mode());
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
provider_id = provider_id.as_str(),
|
||||
error_domain = error.code().domain(),
|
||||
error_code = error.code().code(),
|
||||
"classified market-price refresh failure"
|
||||
);
|
||||
let state = self.update_availability(provider_id, availability);
|
||||
std::result::Result::Ok(crate::MarketPriceRefreshOutcome::from_state(state))
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Refreshes a caller-selected provider set in deterministic request order without fallback or consensus.
|
||||
///
|
||||
/// The service never sleeps to wait for a local rate limit. Duplicate or unknown identifiers are rejected before any provider request is attempted.
|
||||
pub async fn refresh_many(&self, provider_ids: &[crate::MarketPriceProviderId]) -> ksp_core_lib::Result<std::vec::Vec<crate::MarketPriceRefreshOutcome>> {
|
||||
if provider_ids.len() > MARKET_PRICE_REFRESH_MAX_PROVIDERS {
|
||||
return std::result::Result::Err(refresh_error("provider_count"));
|
||||
}
|
||||
if let std::result::Result::Err(error) = validate_requested_provider_ids(self, provider_ids) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let mut outcomes = std::vec::Vec::with_capacity(provider_ids.len());
|
||||
for provider_id in provider_ids {
|
||||
let outcome = match self.refresh(provider_id).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
outcomes.push(outcome);
|
||||
}
|
||||
return std::result::Result::Ok(outcomes);
|
||||
}
|
||||
|
||||
/// Refreshes every currently eligible configured provider in stable provider-id order.
|
||||
///
|
||||
/// Disabled, authentication-unavailable, misconfigured, quota-unavailable and not-yet-expired cooldown states are projected without network dispatch.
|
||||
pub async fn refresh_all(&self) -> ksp_core_lib::Result<std::vec::Vec<crate::MarketPriceRefreshOutcome>> {
|
||||
let provider_ids = self.providers.iter().map(|provider| return provider.provider_id().clone()).collect::<std::vec::Vec<_>>();
|
||||
return self.refresh_many(provider_ids.as_slice()).await;
|
||||
}
|
||||
|
||||
fn provider_index(&self, provider_id: &crate::MarketPriceProviderId) -> std::option::Option<usize> {
|
||||
let result = self.providers.binary_search_by(|provider| return provider.provider_id().cmp(provider_id));
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(_) => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn state_snapshot(&self, provider_id: &crate::MarketPriceProviderId) -> std::option::Option<crate::MarketPriceProviderState> {
|
||||
let guard = lock_registry(&self.registry);
|
||||
return guard.state(provider_id).cloned();
|
||||
}
|
||||
|
||||
fn update_availability(
|
||||
&self,
|
||||
provider_id: &crate::MarketPriceProviderId,
|
||||
availability: crate::MarketPriceProviderAvailability,
|
||||
) -> crate::MarketPriceProviderState {
|
||||
let mut guard = lock_registry(&self.registry);
|
||||
if guard.set_availability(provider_id, availability) {
|
||||
if let std::option::Option::Some(state) = guard.state(provider_id) {
|
||||
return state.clone();
|
||||
}
|
||||
}
|
||||
return crate::MarketPriceProviderState::new(provider_id.clone(), availability);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::MarketPriceService {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("MarketPriceService").field("registry", &self.registry()).finish_non_exhaustive();
|
||||
}
|
||||
}
|
||||
|
||||
enum MarketPriceProviderRuntime {
|
||||
Birdeye(crate::MarketPriceBirdeyeProvider),
|
||||
CoinbaseExchange(crate::MarketPriceCoinbaseExchangeProvider),
|
||||
CoinGecko(crate::MarketPriceCoinGeckoProvider),
|
||||
CoinMarketCap(crate::MarketPriceCoinMarketCapProvider),
|
||||
CoinPaprika(crate::MarketPriceCoinPaprikaProvider),
|
||||
DexScreener(crate::MarketPriceDexScreenerProvider),
|
||||
Jupiter(crate::MarketPriceJupiterProvider),
|
||||
Kraken(crate::MarketPriceKrakenProvider),
|
||||
}
|
||||
|
||||
impl MarketPriceProviderRuntime {
|
||||
fn new(setup: crate::MarketPriceProviderSetup) -> ksp_core_lib::Result<Self> {
|
||||
return match setup {
|
||||
crate::MarketPriceProviderSetup::Birdeye(settings) => crate::MarketPriceBirdeyeProvider::new(settings).map(Self::Birdeye),
|
||||
crate::MarketPriceProviderSetup::CoinbaseExchange(settings) => {
|
||||
crate::MarketPriceCoinbaseExchangeProvider::new(settings).map(Self::CoinbaseExchange)
|
||||
},
|
||||
crate::MarketPriceProviderSetup::CoinGecko(settings) => crate::MarketPriceCoinGeckoProvider::new(settings).map(Self::CoinGecko),
|
||||
crate::MarketPriceProviderSetup::CoinMarketCap(settings) => crate::MarketPriceCoinMarketCapProvider::new(settings).map(Self::CoinMarketCap),
|
||||
crate::MarketPriceProviderSetup::CoinPaprika(settings) => crate::MarketPriceCoinPaprikaProvider::new(settings).map(Self::CoinPaprika),
|
||||
crate::MarketPriceProviderSetup::DexScreener(settings) => crate::MarketPriceDexScreenerProvider::new(settings).map(Self::DexScreener),
|
||||
crate::MarketPriceProviderSetup::Jupiter(settings) => crate::MarketPriceJupiterProvider::new(settings).map(Self::Jupiter),
|
||||
crate::MarketPriceProviderSetup::Kraken(settings) => crate::MarketPriceKrakenProvider::new(settings).map(Self::Kraken),
|
||||
};
|
||||
}
|
||||
|
||||
fn descriptor(&self) -> &crate::MarketPriceProviderDescriptor {
|
||||
return match self {
|
||||
Self::Birdeye(provider) => provider.descriptor(),
|
||||
Self::CoinbaseExchange(provider) => provider.descriptor(),
|
||||
Self::CoinGecko(provider) => provider.descriptor(),
|
||||
Self::CoinMarketCap(provider) => provider.descriptor(),
|
||||
Self::CoinPaprika(provider) => provider.descriptor(),
|
||||
Self::DexScreener(provider) => provider.descriptor(),
|
||||
Self::Jupiter(provider) => provider.descriptor(),
|
||||
Self::Kraken(provider) => provider.descriptor(),
|
||||
};
|
||||
}
|
||||
|
||||
fn enabled(&self) -> bool {
|
||||
return match self {
|
||||
Self::Birdeye(provider) => provider.settings().common().enabled(),
|
||||
Self::CoinbaseExchange(provider) => provider.settings().common().enabled(),
|
||||
Self::CoinGecko(provider) => provider.settings().common().enabled(),
|
||||
Self::CoinMarketCap(provider) => provider.settings().common().enabled(),
|
||||
Self::CoinPaprika(provider) => provider.settings().common().enabled(),
|
||||
Self::DexScreener(provider) => provider.settings().common().enabled(),
|
||||
Self::Jupiter(provider) => provider.settings().common().enabled(),
|
||||
Self::Kraken(provider) => provider.settings().common().enabled(),
|
||||
};
|
||||
}
|
||||
|
||||
async fn fetch_sol_usd(&self) -> ksp_core_lib::Result<crate::MarketPriceObservation> {
|
||||
return match self {
|
||||
Self::Birdeye(provider) => provider.fetch_sol_usd().await,
|
||||
Self::CoinbaseExchange(provider) => provider.fetch_sol_usd().await,
|
||||
Self::CoinGecko(provider) => provider.fetch_sol_usd().await,
|
||||
Self::CoinMarketCap(provider) => provider.fetch_sol_usd().await,
|
||||
Self::CoinPaprika(provider) => provider.fetch_sol_usd().await,
|
||||
Self::DexScreener(provider) => provider.fetch_sol_usd().await,
|
||||
Self::Jupiter(provider) => provider.fetch_sol_usd().await,
|
||||
Self::Kraken(provider) => provider.fetch_sol_usd().await,
|
||||
};
|
||||
}
|
||||
|
||||
fn provider_id(&self) -> &crate::MarketPriceProviderId {
|
||||
return self.descriptor().id();
|
||||
}
|
||||
}
|
||||
|
||||
fn availability_from_error(error: &ksp_core_lib::Error, auth_mode: crate::MarketPriceProviderAuthMode) -> crate::MarketPriceProviderAvailability {
|
||||
let code = error.code();
|
||||
if code == crate::ERROR_CODE_MARKET_PRICE_PROVIDER_DISABLED {
|
||||
return crate::MarketPriceProviderAvailability::Disabled;
|
||||
}
|
||||
if code == crate::ERROR_CODE_HTTP_ACCESS_DENIED {
|
||||
return match auth_mode {
|
||||
crate::MarketPriceProviderAuthMode::OptionalApiKey | crate::MarketPriceProviderAuthMode::RequiredApiKey => {
|
||||
crate::MarketPriceProviderAvailability::AuthenticationUnavailable
|
||||
},
|
||||
crate::MarketPriceProviderAuthMode::None => crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::None },
|
||||
};
|
||||
}
|
||||
if code == crate::ERROR_CODE_HTTP_ADMISSION_DEFERRED {
|
||||
let delay = context_u64(error, "retry_after_millis").unwrap_or(MARKET_PRICE_RATE_LIMIT_FALLBACK_MILLIS);
|
||||
return crate::MarketPriceProviderAvailability::CoolingDown { retry_at: timestamp_after_millis(delay) };
|
||||
}
|
||||
if code == crate::ERROR_CODE_HTTP_RATE_LIMITED {
|
||||
let delay = context_u64(error, "retry_after_seconds")
|
||||
.and_then(|seconds| return seconds.checked_mul(1_000))
|
||||
.unwrap_or(MARKET_PRICE_RATE_LIMIT_FALLBACK_MILLIS);
|
||||
return crate::MarketPriceProviderAvailability::CoolingDown { retry_at: timestamp_after_millis(delay) };
|
||||
}
|
||||
if code == crate::ERROR_CODE_HTTP_TEMPORARY_FAILURE {
|
||||
let retry_at = context_u64(error, "retry_after_seconds").and_then(|seconds| return seconds.checked_mul(1_000)).map(timestamp_after_millis);
|
||||
return crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at };
|
||||
}
|
||||
if code == crate::ERROR_CODE_HTTP_CLIENT_BUILD_FAILED
|
||||
|| code == crate::ERROR_CODE_HTTP_RATE_LIMIT_INVALID
|
||||
|| code == crate::ERROR_CODE_HTTP_REQUEST_INVALID
|
||||
|| code == crate::ERROR_CODE_HTTP_SETTINGS_INVALID
|
||||
|| code == crate::ERROR_CODE_MARKET_PRICE_PROVIDER_SETTINGS_INVALID
|
||||
{
|
||||
return crate::MarketPriceProviderAvailability::Misconfigured;
|
||||
}
|
||||
return crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::None };
|
||||
}
|
||||
|
||||
fn context_u64(error: &ksp_core_lib::Error, key: &'static str) -> std::option::Option<u64> {
|
||||
for context in error.context() {
|
||||
if context.key() == key {
|
||||
return match context.value().parse::<u64>() {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(_) => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
}
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
fn current_timestamp_or_zero() -> crate::MarketPriceTimestamp {
|
||||
return match crate::current_timestamp() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => crate::MarketPriceTimestamp::from_unix_millis(0),
|
||||
};
|
||||
}
|
||||
|
||||
fn lock_registry(registry: &std::sync::Mutex<crate::MarketPriceProviderRegistry>) -> std::sync::MutexGuard<'_, crate::MarketPriceProviderRegistry> {
|
||||
return match registry.lock() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
}
|
||||
|
||||
fn provider_not_found_error(provider_id: &crate::MarketPriceProviderId) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_PROVIDER_NOT_FOUND, "Market-price provider is not configured")
|
||||
.with_context("provider_id", provider_id.as_str());
|
||||
}
|
||||
|
||||
fn refresh_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_REFRESH_INVALID, "Market-price refresh request is invalid").with_context("field", field);
|
||||
}
|
||||
|
||||
fn timestamp_after_millis(delay_millis: u64) -> crate::MarketPriceTimestamp {
|
||||
let now = current_timestamp_or_zero().unix_millis();
|
||||
return crate::MarketPriceTimestamp::from_unix_millis(now.saturating_add(delay_millis));
|
||||
}
|
||||
|
||||
fn validate_requested_provider_ids(service: &crate::MarketPriceService, provider_ids: &[crate::MarketPriceProviderId]) -> ksp_core_lib::Result<()> {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for provider_id in provider_ids {
|
||||
if service.provider_index(provider_id).is_none() {
|
||||
return std::result::Result::Err(provider_not_found_error(provider_id));
|
||||
}
|
||||
if !seen.insert(provider_id) {
|
||||
return std::result::Result::Err(refresh_error("provider_id"));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/market_price_service.rs"]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user