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;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/tests/dependency_boundary.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -107,3 +107,21 @@ fn pre_007_birdeye_and_registry_remain_provider_owned_generic_and_fixed_origin()
|
||||
assert!(!registry.contains("Jupiter"));
|
||||
assert!(!registry.contains("Kraken"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_008_refresh_service_owns_dispatch_while_registry_and_consumer_surface_stay_generic() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
let registry = include_str!("../src/market_price_registry.rs");
|
||||
let service = include_str!("../src/market_price_service.rs");
|
||||
assert!(crate_root.contains("mod market_price_service;"));
|
||||
assert!(crate_root.contains("pub use self::market_price_service::MarketPriceService;"));
|
||||
assert!(!registry.contains("MarketPriceProviderSetup::"));
|
||||
assert!(service.contains("pub async fn refresh("));
|
||||
assert!(service.contains("pub async fn refresh_many("));
|
||||
assert!(service.contains("pub async fn refresh_all("));
|
||||
assert!(service.contains("is_refresh_eligible_at"));
|
||||
assert!(!service.contains("std::thread::sleep"));
|
||||
assert!(!service.contains("tokio::time::sleep"));
|
||||
assert!(!service.contains("fallback_provider"));
|
||||
assert!(!service.contains("consensus"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/tests/public_api.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -183,6 +183,29 @@ fn public_pre_007_birdeye_and_provider_registry_are_available_from_crate_root()
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn public_pre_008_generic_refresh_service_is_available_without_provider_branching_at_callsite() -> ksp_core_lib::Result<()> {
|
||||
let settings = match ksp_offchain_transport_lib::MarketPriceCoinPaprikaSettings::new(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let service =
|
||||
match ksp_offchain_transport_lib::MarketPriceService::new(std::vec![ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinPaprika(settings),]) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = service.registry();
|
||||
let provider_id = registry.entries()[0].descriptor().id().clone();
|
||||
let outcome = match service.refresh(&provider_id).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(outcome.provider_id(), &provider_id);
|
||||
assert!(!outcome.refreshed());
|
||||
assert_eq!(outcome.state().availability(), ksp_offchain_transport_lib::MarketPriceProviderAvailability::Disabled);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offchain_error_codes_use_owned_domain() {
|
||||
let codes = [
|
||||
@@ -203,9 +226,11 @@ fn offchain_error_codes_use_owned_domain() {
|
||||
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_PROVIDER_DESCRIPTOR_INVALID,
|
||||
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_PROVIDER_DISABLED,
|
||||
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_PROVIDER_ID_INVALID,
|
||||
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_PROVIDER_NOT_FOUND,
|
||||
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_PROVIDER_RESPONSE_INVALID,
|
||||
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_OBSERVATION_INVALID,
|
||||
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_PROVIDER_SETTINGS_INVALID,
|
||||
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_REFRESH_INVALID,
|
||||
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_REGISTRY_INVALID,
|
||||
];
|
||||
for code in codes {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_provider.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
#[test]
|
||||
fn provider_id_is_opaque_bounded_and_stable() -> ksp_core_lib::Result<()> {
|
||||
@@ -130,3 +130,16 @@ fn provider_availability_keeps_cooldown_and_outage_distinct() -> ksp_core_lib::R
|
||||
assert_ne!(state.availability(), crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::Some(retry_at) });
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_availability_eligibility_at_respects_retry_deadlines_without_consumer_scheduling() {
|
||||
let now = crate::MarketPriceTimestamp::from_unix_millis(100);
|
||||
let future = crate::MarketPriceTimestamp::from_unix_millis(200);
|
||||
let past = crate::MarketPriceTimestamp::from_unix_millis(50);
|
||||
assert!(crate::MarketPriceProviderAvailability::Ready.is_refresh_eligible_at(now));
|
||||
assert!(!crate::MarketPriceProviderAvailability::Disabled.is_refresh_eligible_at(now));
|
||||
assert!(!crate::MarketPriceProviderAvailability::CoolingDown { retry_at: future }.is_refresh_eligible_at(now));
|
||||
assert!(crate::MarketPriceProviderAvailability::CoolingDown { retry_at: past }.is_refresh_eligible_at(now));
|
||||
assert!(crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::None }.is_refresh_eligible_at(now));
|
||||
assert!(crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::Some(past) }.is_refresh_eligible_at(now));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_service.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn service_registry_orders_all_eight_v1_setups_and_keeps_runtime_projection_generic() -> ksp_core_lib::Result<()> {
|
||||
let setups = match all_disabled_setups() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let service = match crate::MarketPriceService::new(setups) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = service.registry();
|
||||
assert_eq!(registry.len(), 8);
|
||||
let ids = registry.entries().iter().map(|entry| return entry.descriptor().id().as_str()).collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(ids, std::vec!["birdeye", "coinbase_exchange", "coingecko", "coinmarketcap", "coinpaprika", "dexscreener", "jupiter", "kraken",]);
|
||||
assert!(registry.entries().iter().all(|entry| return entry.state().availability() == crate::MarketPriceProviderAvailability::Disabled));
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_all_projects_disabled_providers_without_network_dispatch() -> ksp_core_lib::Result<()> {
|
||||
let setups = match all_disabled_setups() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let service = match crate::MarketPriceService::new(setups) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let outcomes = match service.refresh_all().await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(outcomes.len(), 8);
|
||||
assert!(outcomes.iter().all(|outcome| return !outcome.refreshed()));
|
||||
assert!(outcomes.iter().all(|outcome| return outcome.state().availability() == crate::MarketPriceProviderAvailability::Disabled));
|
||||
let ids = outcomes.iter().map(|outcome| return outcome.provider_id().as_str()).collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(ids, std::vec!["birdeye", "coinbase_exchange", "coingecko", "coinmarketcap", "coinpaprika", "dexscreener", "jupiter", "kraken",]);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_many_rejects_duplicates_and_unknown_provider_before_dispatch() -> ksp_core_lib::Result<()> {
|
||||
let settings = match crate::MarketPriceCoinPaprikaSettings::new(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let service = match crate::MarketPriceService::new(std::vec![crate::MarketPriceProviderSetup::CoinPaprika(settings)]) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let provider_id = match crate::MarketPriceProviderId::new("coinpaprika") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let duplicate = service.refresh_many(&[provider_id.clone(), provider_id]).await;
|
||||
let duplicate_error = match duplicate {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("duplicate")),
|
||||
};
|
||||
assert_eq!(duplicate_error.code(), crate::ERROR_CODE_MARKET_PRICE_REFRESH_INVALID);
|
||||
let unknown_id = match crate::MarketPriceProviderId::new("unknown") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let unknown = service.refresh(&unknown_id).await;
|
||||
let unknown_error = match unknown {
|
||||
std::result::Result::Err(error) => error,
|
||||
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("unknown")),
|
||||
};
|
||||
assert_eq!(unknown_error.code(), crate::ERROR_CODE_MARKET_PRICE_PROVIDER_NOT_FOUND);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_error_classification_is_provider_neutral_and_preserves_known_retry_time() {
|
||||
let auth = ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_ACCESS_DENIED, "test");
|
||||
assert_eq!(
|
||||
super::availability_from_error(&auth, crate::MarketPriceProviderAuthMode::RequiredApiKey),
|
||||
crate::MarketPriceProviderAvailability::AuthenticationUnavailable
|
||||
);
|
||||
assert!(matches!(
|
||||
super::availability_from_error(&auth, crate::MarketPriceProviderAuthMode::None),
|
||||
crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::None }
|
||||
));
|
||||
let rate = ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_RATE_LIMITED, "test").with_context("retry_after_seconds", "2");
|
||||
assert!(matches!(
|
||||
super::availability_from_error(&rate, crate::MarketPriceProviderAuthMode::None),
|
||||
crate::MarketPriceProviderAvailability::CoolingDown { .. }
|
||||
));
|
||||
let temporary = ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_TEMPORARY_FAILURE, "test").with_context("retry_after_seconds", "3");
|
||||
let availability = super::availability_from_error(&temporary, crate::MarketPriceProviderAuthMode::None);
|
||||
assert!(matches!(availability, crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::Some(_) }));
|
||||
let local = ksp_core_lib::Error::new(crate::ERROR_CODE_HTTP_REQUEST_INVALID, "test");
|
||||
assert_eq!(super::availability_from_error(&local, crate::MarketPriceProviderAuthMode::None), crate::MarketPriceProviderAvailability::Misconfigured);
|
||||
}
|
||||
|
||||
fn all_disabled_setups() -> ksp_core_lib::Result<std::vec::Vec<crate::MarketPriceProviderSetup>> {
|
||||
let birdeye = match crate::MarketPriceBirdeyeSettings::new(false, std::option::Option::None) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coinbase = match crate::MarketPriceCoinbaseExchangeSettings::new(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coingecko = match crate::MarketPriceCoinGeckoSettings::keyless(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coinmarketcap = match crate::MarketPriceCoinMarketCapSettings::keyless(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coinpaprika = match crate::MarketPriceCoinPaprikaSettings::new(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let dexscreener = match crate::MarketPriceDexScreenerSettings::new(false, "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let jupiter = match crate::MarketPriceJupiterSettings::keyless(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let kraken = match crate::MarketPriceKrakenSettings::new(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(std::vec![
|
||||
crate::MarketPriceProviderSetup::Birdeye(birdeye),
|
||||
crate::MarketPriceProviderSetup::CoinbaseExchange(coinbase),
|
||||
crate::MarketPriceProviderSetup::CoinGecko(coingecko),
|
||||
crate::MarketPriceProviderSetup::CoinMarketCap(coinmarketcap),
|
||||
crate::MarketPriceProviderSetup::CoinPaprika(coinpaprika),
|
||||
crate::MarketPriceProviderSetup::DexScreener(dexscreener),
|
||||
crate::MarketPriceProviderSetup::Jupiter(jupiter),
|
||||
crate::MarketPriceProviderSetup::Kraken(kraken),
|
||||
]);
|
||||
}
|
||||
|
||||
fn test_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_REFRESH_INVALID, "Market-price service test expectation failed")
|
||||
.with_context("field", field);
|
||||
}
|
||||
Reference in New Issue
Block a user