v0.2.11-pre.007
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/src/error.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
/// 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");
|
||||
@@ -47,3 +47,6 @@ pub const ERROR_CODE_MARKET_PRICE_PROVIDER_RESPONSE_INVALID: ksp_core_lib::Error
|
||||
/// 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 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: 8
|
||||
// version: 9
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -7,10 +7,10 @@
|
||||
|
||||
//! KSP-owned off-chain transport foundation.
|
||||
//!
|
||||
//! `0.2.11-pre.006` keeps the first capability family deliberately narrow (`market_price`, SOL/USD only) and adds Jupiter Price V3 plus DexScreener.
|
||||
//! Jupiter preserves its Solana heuristic semantics and block provenance without inventing a price timestamp. DexScreener is bound to one explicitly
|
||||
//! configured Solana pair address, validates that the returned pair is the configured pair with wrapped SOL as base identity, and consumes only `priceUsd`.
|
||||
//! Provider wire DTOs remain private, no provider SDK is used, and Config plus generic registry/refresh orchestration remain outside this tranche.
|
||||
//! `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.
|
||||
|
||||
mod constants;
|
||||
mod error;
|
||||
@@ -19,6 +19,7 @@ mod http_client;
|
||||
mod http_settings;
|
||||
mod market_price_adapter;
|
||||
mod market_price_api_key;
|
||||
mod market_price_birdeye;
|
||||
mod market_price_coinbase_exchange;
|
||||
mod market_price_coingecko;
|
||||
mod market_price_coinmarketcap;
|
||||
@@ -29,6 +30,7 @@ mod market_price_jupiter;
|
||||
mod market_price_kraken;
|
||||
mod market_price_observation;
|
||||
mod market_price_provider;
|
||||
mod market_price_registry;
|
||||
mod market_price_settings;
|
||||
|
||||
/// Stable error code for HTTP access denial.
|
||||
@@ -71,6 +73,12 @@ pub use self::error::ERROR_CODE_MARKET_PRICE_PROVIDER_ID_INVALID;
|
||||
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 provider registry.
|
||||
pub use self::error::ERROR_CODE_MARKET_PRICE_REGISTRY_INVALID;
|
||||
/// Birdeye Standard SOL/USD provider adapter.
|
||||
pub use self::market_price_birdeye::MarketPriceBirdeyeProvider;
|
||||
/// Birdeye Standard runtime settings.
|
||||
pub use self::market_price_birdeye::MarketPriceBirdeyeSettings;
|
||||
/// Coinbase Exchange SOL/USD provider adapter.
|
||||
pub use self::market_price_coinbase_exchange::MarketPriceCoinbaseExchangeProvider;
|
||||
/// Coinbase Exchange runtime settings.
|
||||
@@ -145,10 +153,16 @@ pub use self::market_price_provider::MarketPriceProviderRateLimit;
|
||||
pub use self::market_price_provider::MarketPriceProviderRateLimitKind;
|
||||
/// Scope to which a provider documents one request limit.
|
||||
pub use self::market_price_provider::MarketPriceProviderRateLimitScope;
|
||||
/// Informational cost of one normalized SOL/USD request in a provider-defined quota unit.
|
||||
pub use self::market_price_provider::MarketPriceProviderRequestCost;
|
||||
/// Current provider-neutral runtime state projection.
|
||||
pub use self::market_price_provider::MarketPriceProviderState;
|
||||
/// Market-price semantics retained so consumers never assume all providers report equivalent market values.
|
||||
pub use self::market_price_provider::MarketPriceSemantics;
|
||||
/// Deterministically ordered registry of configured market-price providers.
|
||||
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;
|
||||
/// Common provider settings shared by provider-specific runtime settings.
|
||||
pub use self::market_price_settings::MarketPriceProviderCommonSettings;
|
||||
|
||||
|
||||
238
crates/ksp-offchain-transport-lib/src/market_price_birdeye.rs
Normal file
238
crates/ksp-offchain-transport-lib/src/market_price_birdeye.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
// file: crates/ksp-offchain-transport-lib/src/market_price_birdeye.rs
|
||||
// version: 1
|
||||
|
||||
//! Birdeye SOL/USD market-price adapter using the official Price Single REST endpoint.
|
||||
|
||||
const BIRDEYE_API_KEY_HEADER: &str = "x-api-key";
|
||||
const BIRDEYE_CHAIN_HEADER: &str = "x-chain";
|
||||
const BIRDEYE_PRICE_URL: &str = "https://public-api.birdeye.so/defi/price";
|
||||
const BIRDEYE_PROVIDER_ID: &str = "birdeye";
|
||||
const BIRDEYE_SOL_MINT: &str = "So11111111111111111111111111111111111111112";
|
||||
|
||||
/// Runtime settings for the Birdeye Standard market-price adapter.
|
||||
pub struct MarketPriceBirdeyeSettings {
|
||||
api_key: std::option::Option<crate::MarketPriceApiKey>,
|
||||
common: crate::MarketPriceProviderCommonSettings,
|
||||
}
|
||||
|
||||
impl crate::MarketPriceBirdeyeSettings {
|
||||
/// Creates Birdeye settings. An API key is mandatory while the provider is enabled.
|
||||
pub fn new(enabled: bool, api_key: std::option::Option<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||
let provider_id = match crate::MarketPriceProviderId::new(BIRDEYE_PROVIDER_ID) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let api_key = match api_key {
|
||||
std::option::Option::Some(value) => match crate::MarketPriceApiKey::new(BIRDEYE_PROVIDER_ID, value) {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
},
|
||||
std::option::Option::None if enabled => return std::result::Result::Err(provider_settings_error("api_key")),
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let common = crate::MarketPriceProviderCommonSettings::new(provider_id, enabled);
|
||||
return std::result::Result::Ok(Self { api_key, common });
|
||||
}
|
||||
|
||||
/// Returns common provider identity and enablement settings.
|
||||
#[must_use]
|
||||
pub const fn common(&self) -> &crate::MarketPriceProviderCommonSettings {
|
||||
return &self.common;
|
||||
}
|
||||
|
||||
fn api_key(&self) -> std::option::Option<&crate::MarketPriceApiKey> {
|
||||
return self.api_key.as_ref();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::MarketPriceBirdeyeSettings {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("MarketPriceBirdeyeSettings")
|
||||
.field("api_key_present", &self.api_key.is_some())
|
||||
.field("common", &self.common)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Birdeye SOL/USD provider adapter backed by the Standard Price Single endpoint.
|
||||
pub struct MarketPriceBirdeyeProvider {
|
||||
admission: crate::HttpAdmissionController,
|
||||
descriptor: crate::MarketPriceProviderDescriptor,
|
||||
http: crate::HttpRestClient,
|
||||
settings: crate::MarketPriceBirdeyeSettings,
|
||||
}
|
||||
|
||||
impl crate::MarketPriceBirdeyeProvider {
|
||||
/// Builds one Birdeye provider from validated runtime settings.
|
||||
pub fn new(settings: crate::MarketPriceBirdeyeSettings) -> ksp_core_lib::Result<Self> {
|
||||
let descriptor = match descriptor_for(settings.common().provider_id().clone()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let runtime = match crate::provider_http_runtime(descriptor.rate_limit()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self { admission: runtime.1, descriptor, http: runtime.0, settings });
|
||||
}
|
||||
|
||||
/// Returns the provider-neutral Birdeye capability descriptor.
|
||||
#[must_use]
|
||||
pub const fn descriptor(&self) -> &crate::MarketPriceProviderDescriptor {
|
||||
return &self.descriptor;
|
||||
}
|
||||
|
||||
/// Returns the validated Birdeye settings without exposing credential material.
|
||||
#[must_use]
|
||||
pub const fn settings(&self) -> &crate::MarketPriceBirdeyeSettings {
|
||||
return &self.settings;
|
||||
}
|
||||
|
||||
/// Fetches one normalized SOL/USD observation from Birdeye Price Single.
|
||||
pub async fn fetch_sol_usd(&self) -> ksp_core_lib::Result<crate::MarketPriceObservation> {
|
||||
if !self.settings.common().enabled() {
|
||||
return std::result::Result::Err(crate::provider_disabled_error(BIRDEYE_PROVIDER_ID));
|
||||
}
|
||||
if let std::result::Result::Err(error) = crate::admit_request(BIRDEYE_PROVIDER_ID, &self.admission) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let request_started_at = match crate::current_timestamp() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let request = match build_request(&self.settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let document = match crate::get_json(&self.http, &self.admission, BIRDEYE_PROVIDER_ID, request).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let received_at = match crate::current_timestamp() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return parse_response(document.as_bytes(), self.settings.common().provider_id().clone(), request_started_at, received_at);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_request(settings: &crate::MarketPriceBirdeyeSettings) -> ksp_core_lib::Result<crate::HttpGetRequest> {
|
||||
let mut request = match crate::HttpGetRequest::new_https(BIRDEYE_PRICE_URL) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
request.append_query_pair("address", BIRDEYE_SOL_MINT);
|
||||
if let std::result::Result::Err(error) = request.insert_sensitive_header(BIRDEYE_CHAIN_HEADER, "solana") {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let api_key = match settings.api_key() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(provider_settings_error("api_key")),
|
||||
};
|
||||
if let std::result::Result::Err(error) = request.insert_sensitive_header(BIRDEYE_API_KEY_HEADER, api_key.as_str()) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(request);
|
||||
}
|
||||
|
||||
fn descriptor_for(provider_id: crate::MarketPriceProviderId) -> ksp_core_lib::Result<crate::MarketPriceProviderDescriptor> {
|
||||
let rate_limit = match crate::MarketPriceProviderRateLimit::fixed(1, 1, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Account) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let quota = match crate::MarketPriceProviderLongTermQuota::new(
|
||||
30_000,
|
||||
crate::MarketPriceProviderQuotaPeriod::Month,
|
||||
crate::MarketPriceProviderQuotaUnit::ComputeUnits,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let request_cost = match crate::MarketPriceProviderRequestCost::new(3, crate::MarketPriceProviderQuotaUnit::ComputeUnits) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let descriptor = match crate::MarketPriceProviderDescriptor::new(
|
||||
provider_id,
|
||||
"Birdeye",
|
||||
crate::MarketPriceSemantics::SolanaSpot,
|
||||
crate::MarketPriceProviderAuthMode::RequiredApiKey,
|
||||
rate_limit,
|
||||
std::option::Option::Some(quota),
|
||||
true,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return descriptor.with_sol_usd_request_cost(request_cost);
|
||||
}
|
||||
|
||||
fn parse_response(
|
||||
bytes: &[u8],
|
||||
provider_id: crate::MarketPriceProviderId,
|
||||
request_started_at: crate::MarketPriceTimestamp,
|
||||
received_at: crate::MarketPriceTimestamp,
|
||||
) -> ksp_core_lib::Result<crate::MarketPriceObservation> {
|
||||
let wire = match serde_json::from_slice::<BirdeyeWireResponse>(bytes) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(crate::invalid_provider_response_with_source(BIRDEYE_PROVIDER_ID, "response", error));
|
||||
},
|
||||
};
|
||||
if !wire.success {
|
||||
return std::result::Result::Err(crate::invalid_provider_response(BIRDEYE_PROVIDER_ID, "success"));
|
||||
}
|
||||
let data = match wire.data {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(BIRDEYE_PROVIDER_ID, "data")),
|
||||
};
|
||||
let price_raw = match data.value.as_deref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(BIRDEYE_PROVIDER_ID, "data.value")),
|
||||
};
|
||||
let price = match crate::MarketPriceDecimal::parse_json_raw(price_raw) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let provider_timestamp = match data.update_unix_time.and_then(crate::market_price_timestamp_from_unix_seconds) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(BIRDEYE_PROVIDER_ID, "data.updateUnixTime")),
|
||||
};
|
||||
let provenance = match crate::MarketPriceProvenance::new("birdeye:solana:wsol:value") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::MarketPriceObservation::new(
|
||||
provider_id,
|
||||
price,
|
||||
crate::MarketPriceSemantics::SolanaSpot,
|
||||
request_started_at,
|
||||
received_at,
|
||||
std::option::Option::Some(provider_timestamp),
|
||||
provenance,
|
||||
);
|
||||
}
|
||||
|
||||
fn provider_settings_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_PROVIDER_SETTINGS_INVALID, "Birdeye market-price settings are invalid")
|
||||
.with_context("provider", BIRDEYE_PROVIDER_ID)
|
||||
.with_context("field", field);
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct BirdeyeWireData {
|
||||
#[serde(rename = "updateUnixTime")]
|
||||
update_unix_time: std::option::Option<u64>,
|
||||
value: std::option::Option<std::boxed::Box<serde_json::value::RawValue>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct BirdeyeWireResponse {
|
||||
data: std::option::Option<BirdeyeWireData>,
|
||||
success: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/market_price_birdeye.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/src/market_price_provider.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
/// Maximum UTF-8 byte length of one provider display name.
|
||||
pub const MARKET_PRICE_PROVIDER_DISPLAY_NAME_MAX_BYTES: usize = 96;
|
||||
@@ -163,6 +163,8 @@ pub enum MarketPriceProviderQuotaPeriod {
|
||||
#[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.
|
||||
@@ -205,6 +207,35 @@ impl MarketPriceProviderLongTermQuota {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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")]
|
||||
@@ -261,6 +292,7 @@ pub struct MarketPriceProviderDescriptor {
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -281,7 +313,27 @@ impl MarketPriceProviderDescriptor {
|
||||
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, supports_sol_usd });
|
||||
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.
|
||||
@@ -320,6 +372,12 @@ impl MarketPriceProviderDescriptor {
|
||||
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 {
|
||||
@@ -353,6 +411,24 @@ pub enum MarketPriceProviderAvailability {
|
||||
},
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
104
crates/ksp-offchain-transport-lib/src/market_price_registry.rs
Normal file
104
crates/ksp-offchain-transport-lib/src/market_price_registry.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
// file: crates/ksp-offchain-transport-lib/src/market_price_registry.rs
|
||||
// version: 1
|
||||
|
||||
//! Provider-neutral registry projection for configured market-price providers.
|
||||
|
||||
const MARKET_PRICE_PROVIDER_REGISTRY_MAX_ENTRIES: usize = 64;
|
||||
|
||||
/// Immutable provider descriptor plus its current generic runtime state.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct MarketPriceProviderRegistryEntry {
|
||||
descriptor: crate::MarketPriceProviderDescriptor,
|
||||
state: crate::MarketPriceProviderState,
|
||||
}
|
||||
|
||||
impl crate::MarketPriceProviderRegistryEntry {
|
||||
/// Returns the provider capability descriptor.
|
||||
#[must_use]
|
||||
pub const fn descriptor(&self) -> &crate::MarketPriceProviderDescriptor {
|
||||
return &self.descriptor;
|
||||
}
|
||||
|
||||
/// Returns the provider-neutral runtime state.
|
||||
#[must_use]
|
||||
pub const fn state(&self) -> &crate::MarketPriceProviderState {
|
||||
return &self.state;
|
||||
}
|
||||
|
||||
/// Creates one generic registry entry while keeping descriptor and state identity synchronized.
|
||||
#[must_use]
|
||||
pub fn new(descriptor: crate::MarketPriceProviderDescriptor, availability: crate::MarketPriceProviderAvailability) -> Self {
|
||||
let state = crate::MarketPriceProviderState::new(descriptor.id().clone(), availability);
|
||||
return Self { descriptor, state };
|
||||
}
|
||||
}
|
||||
|
||||
/// Deterministically ordered registry of configured market-price providers.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize)]
|
||||
pub struct MarketPriceProviderRegistry {
|
||||
entries: std::vec::Vec<crate::MarketPriceProviderRegistryEntry>,
|
||||
}
|
||||
|
||||
impl crate::MarketPriceProviderRegistry {
|
||||
/// Returns a provider descriptor by opaque identifier.
|
||||
#[must_use]
|
||||
pub fn descriptor(&self, provider_id: &crate::MarketPriceProviderId) -> std::option::Option<&crate::MarketPriceProviderDescriptor> {
|
||||
return self.entry(provider_id).map(crate::MarketPriceProviderRegistryEntry::descriptor);
|
||||
}
|
||||
|
||||
/// Returns all configured provider entries in stable provider-id order.
|
||||
#[must_use]
|
||||
pub fn entries(&self) -> &[crate::MarketPriceProviderRegistryEntry] {
|
||||
return self.entries.as_slice();
|
||||
}
|
||||
|
||||
/// Returns one configured registry entry by opaque identifier.
|
||||
#[must_use]
|
||||
pub fn entry(&self, provider_id: &crate::MarketPriceProviderId) -> std::option::Option<&crate::MarketPriceProviderRegistryEntry> {
|
||||
let index = self.entries.binary_search_by(|entry| return entry.descriptor().id().cmp(provider_id));
|
||||
return match index {
|
||||
std::result::Result::Ok(value) => self.entries.get(value),
|
||||
std::result::Result::Err(_) => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
/// Reports whether no provider is currently registered.
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
return self.entries.is_empty();
|
||||
}
|
||||
|
||||
/// Returns the configured provider count.
|
||||
#[must_use]
|
||||
pub fn len(&self) -> usize {
|
||||
return self.entries.len();
|
||||
}
|
||||
|
||||
/// Returns one provider-neutral runtime state by opaque identifier.
|
||||
#[must_use]
|
||||
pub fn state(&self, provider_id: &crate::MarketPriceProviderId) -> std::option::Option<&crate::MarketPriceProviderState> {
|
||||
return self.entry(provider_id).map(crate::MarketPriceProviderRegistryEntry::state);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
return std::result::Result::Err(registry_error("entry_count"));
|
||||
}
|
||||
entries.sort_by(|left, right| return left.descriptor().id().cmp(right.descriptor().id()));
|
||||
for index in 1..entries.len() {
|
||||
if entries[index - 1].descriptor().id() == entries[index].descriptor().id() {
|
||||
return std::result::Result::Err(registry_error("provider_id"));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(Self { entries });
|
||||
}
|
||||
}
|
||||
|
||||
fn registry_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_REGISTRY_INVALID, "Market-price provider registry is invalid").with_context("field", field);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/market_price_registry.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/tests/dependency_boundary.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -87,3 +87,23 @@ fn pre_006_jupiter_and_dexscreener_stay_market_price_scoped_fixed_origin_and_dis
|
||||
assert!(!dexscreener.contains("std::env"));
|
||||
assert!(!jupiter.contains("std::env"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_007_birdeye_and_registry_remain_provider_owned_generic_and_fixed_origin() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
assert!(crate_root.contains("mod market_price_birdeye;"));
|
||||
assert!(crate_root.contains("mod market_price_registry;"));
|
||||
let birdeye = include_str!("../src/market_price_birdeye.rs");
|
||||
let registry = include_str!("../src/market_price_registry.rs");
|
||||
assert!(birdeye.contains("https://public-api.birdeye.so/defi/price"));
|
||||
assert!(birdeye.contains("x-api-key"));
|
||||
assert!(birdeye.contains("x-chain"));
|
||||
assert!(birdeye.contains("So11111111111111111111111111111111111111112"));
|
||||
assert!(!birdeye.contains("std::env"));
|
||||
assert!(!registry.contains("CoinGecko"));
|
||||
assert!(!registry.contains("CoinMarketCap"));
|
||||
assert!(!registry.contains("CoinPaprika"));
|
||||
assert!(!registry.contains("DexScreener"));
|
||||
assert!(!registry.contains("Jupiter"));
|
||||
assert!(!registry.contains("Kraken"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/tests/public_api.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -147,6 +147,42 @@ fn public_pre_006_jupiter_and_dexscreener_settings_and_adapters_are_available_fr
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_pre_007_birdeye_and_provider_registry_are_available_from_crate_root() -> ksp_core_lib::Result<()> {
|
||||
let settings = match ksp_offchain_transport_lib::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 provider = match ksp_offchain_transport_lib::MarketPriceBirdeyeProvider::new(settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(provider.descriptor().id().as_str(), "birdeye");
|
||||
assert_eq!(provider.descriptor().semantics(), ksp_offchain_transport_lib::MarketPriceSemantics::SolanaSpot);
|
||||
let request_cost = match provider.descriptor().sol_usd_request_cost() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_PROVIDER_DESCRIPTOR_INVALID,
|
||||
"Birdeye descriptor is missing request-cost metadata",
|
||||
));
|
||||
},
|
||||
};
|
||||
assert_eq!(request_cost.amount(), 3);
|
||||
assert_eq!(request_cost.unit(), ksp_offchain_transport_lib::MarketPriceProviderQuotaUnit::ComputeUnits);
|
||||
let ready = ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready;
|
||||
assert!(ready.is_refresh_eligible());
|
||||
assert_eq!(ready.retry_at(), std::option::Option::None);
|
||||
let entry = ksp_offchain_transport_lib::MarketPriceProviderRegistryEntry::new(provider.descriptor().clone(), ready);
|
||||
let registry = match ksp_offchain_transport_lib::MarketPriceProviderRegistry::new(std::vec![entry]) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(registry.len(), 1);
|
||||
assert_eq!(registry.entries()[0].descriptor().id().as_str(), "birdeye");
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offchain_error_codes_use_owned_domain() {
|
||||
let codes = [
|
||||
@@ -170,6 +206,7 @@ fn offchain_error_codes_use_owned_domain() {
|
||||
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_REGISTRY_INVALID,
|
||||
];
|
||||
for code in codes {
|
||||
assert_eq!(code.domain(), "offchain_transport");
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_birdeye.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn birdeye_fixture_maps_exact_spot_price_and_provider_update_time() -> ksp_core_lib::Result<()> {
|
||||
let provider_id = match crate::MarketPriceProviderId::new(super::BIRDEYE_PROVIDER_ID) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let fixture = br#"{"data":{"value":151.987654321012345678,"updateUnixTime":1778248899,"updateHumanTime":"2026-05-08T19:01:39"},"success":true}"#;
|
||||
let started = crate::MarketPriceTimestamp::from_unix_millis(10);
|
||||
let received = crate::MarketPriceTimestamp::from_unix_millis(20);
|
||||
let observation = match super::parse_response(fixture, provider_id, started, received) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(observation.price().to_canonical_string(), "151.987654321012345678");
|
||||
assert_eq!(observation.semantics(), crate::MarketPriceSemantics::SolanaSpot);
|
||||
assert_eq!(observation.provider_timestamp().map(|value| return value.unix_millis()), std::option::Option::Some(1_778_248_899_000));
|
||||
assert_eq!(observation.provenance().as_str(), "birdeye:solana:wsol:value");
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn birdeye_standard_descriptor_models_account_rate_compute_quota_and_request_cost() -> ksp_core_lib::Result<()> {
|
||||
let settings = match crate::MarketPriceBirdeyeSettings::new(true, std::option::Option::Some("birdeye-standard-canary-secret".to_owned())) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert!(!std::format!("{settings:?}").contains("birdeye-standard-canary-secret"));
|
||||
let request = match super::build_request(&settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert!(request.has_header_for_test(super::BIRDEYE_API_KEY_HEADER));
|
||||
assert!(request.has_header_for_test(super::BIRDEYE_CHAIN_HEADER));
|
||||
assert_eq!(request.url_for_test().query(), std::option::Option::Some("address=So11111111111111111111111111111111111111112"));
|
||||
let provider = match crate::MarketPriceBirdeyeProvider::new(settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let descriptor = provider.descriptor();
|
||||
assert_eq!(descriptor.auth_mode(), crate::MarketPriceProviderAuthMode::RequiredApiKey);
|
||||
assert_eq!(descriptor.semantics(), crate::MarketPriceSemantics::SolanaSpot);
|
||||
assert_eq!(descriptor.rate_limit().requests(), std::option::Option::Some(1));
|
||||
assert_eq!(descriptor.rate_limit().window_seconds(), std::option::Option::Some(1));
|
||||
assert_eq!(descriptor.rate_limit().scope(), crate::MarketPriceProviderRateLimitScope::Account);
|
||||
let quota = match descriptor.long_term_quota() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(test_error("quota")),
|
||||
};
|
||||
assert_eq!(quota.amount(), 30_000);
|
||||
assert_eq!(quota.period(), crate::MarketPriceProviderQuotaPeriod::Month);
|
||||
assert_eq!(quota.unit(), crate::MarketPriceProviderQuotaUnit::ComputeUnits);
|
||||
let request_cost = match descriptor.sol_usd_request_cost() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(test_error("request_cost")),
|
||||
};
|
||||
assert_eq!(request_cost.amount(), 3);
|
||||
assert_eq!(request_cost.unit(), crate::MarketPriceProviderQuotaUnit::ComputeUnits);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn birdeye_rejects_missing_key_unsuccessful_missing_price_and_missing_update_time() -> ksp_core_lib::Result<()> {
|
||||
assert!(crate::MarketPriceBirdeyeSettings::new(true, std::option::Option::None).is_err());
|
||||
assert!(crate::MarketPriceBirdeyeSettings::new(false, std::option::Option::None).is_ok());
|
||||
let provider_id = match crate::MarketPriceProviderId::new(super::BIRDEYE_PROVIDER_ID) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let timestamp = crate::MarketPriceTimestamp::from_unix_millis(10);
|
||||
let unsuccessful = br#"{"data":{"value":151.9,"updateUnixTime":1778248899},"success":false}"#;
|
||||
assert!(super::parse_response(unsuccessful, provider_id.clone(), timestamp, timestamp).is_err());
|
||||
let missing_price = br#"{"data":{"value":null,"updateUnixTime":1778248899},"success":true}"#;
|
||||
assert!(super::parse_response(missing_price, provider_id.clone(), timestamp, timestamp).is_err());
|
||||
let missing_update_time = br#"{"data":{"value":151.9},"success":true}"#;
|
||||
assert!(super::parse_response(missing_update_time, provider_id, timestamp, timestamp).is_err());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn test_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_PROVIDER_DESCRIPTOR_INVALID, "Birdeye test expectation failed")
|
||||
.with_context("field", field);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_provider.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
#[test]
|
||||
fn provider_id_is_opaque_bounded_and_stable() -> ksp_core_lib::Result<()> {
|
||||
@@ -46,6 +46,7 @@ fn provider_descriptor_preserves_semantics_auth_limits_and_informational_quota()
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(descriptor.display_name(), "Provider A");
|
||||
assert_eq!(descriptor.sol_usd_request_cost(), std::option::Option::None);
|
||||
assert_eq!(descriptor.semantics(), crate::MarketPriceSemantics::AggregatedMarket);
|
||||
assert_eq!(descriptor.auth_mode(), crate::MarketPriceProviderAuthMode::OptionalApiKey);
|
||||
assert_eq!(descriptor.rate_limit(), rate_limit);
|
||||
@@ -67,6 +68,54 @@ fn provider_limit_descriptors_reject_zero_and_model_dynamic_scope() {
|
||||
assert_eq!(dynamic.burst(), std::option::Option::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_request_cost_matches_quota_unit_and_rejects_zero() -> ksp_core_lib::Result<()> {
|
||||
assert!(crate::MarketPriceProviderRequestCost::new(0, crate::MarketPriceProviderQuotaUnit::ComputeUnits).is_err());
|
||||
let id = match crate::MarketPriceProviderId::new("provider-cost") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let rate_limit = match crate::MarketPriceProviderRateLimit::fixed(1, 1, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Account) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let quota = match crate::MarketPriceProviderLongTermQuota::new(
|
||||
30_000,
|
||||
crate::MarketPriceProviderQuotaPeriod::Month,
|
||||
crate::MarketPriceProviderQuotaUnit::ComputeUnits,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let descriptor = match crate::MarketPriceProviderDescriptor::new(
|
||||
id,
|
||||
"Provider Cost",
|
||||
crate::MarketPriceSemantics::SolanaSpot,
|
||||
crate::MarketPriceProviderAuthMode::RequiredApiKey,
|
||||
rate_limit,
|
||||
std::option::Option::Some(quota),
|
||||
true,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let request_cost = match crate::MarketPriceProviderRequestCost::new(3, crate::MarketPriceProviderQuotaUnit::ComputeUnits) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let descriptor = match descriptor.with_sol_usd_request_cost(request_cost) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(descriptor.sol_usd_request_cost(), std::option::Option::Some(request_cost));
|
||||
let wrong_unit = match crate::MarketPriceProviderRequestCost::new(3, crate::MarketPriceProviderQuotaUnit::Credits) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert!(descriptor.with_sol_usd_request_cost(wrong_unit).is_err());
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_availability_keeps_cooldown_and_outage_distinct() -> ksp_core_lib::Result<()> {
|
||||
let id = match crate::MarketPriceProviderId::new("jupiter") {
|
||||
@@ -76,6 +125,8 @@ fn provider_availability_keeps_cooldown_and_outage_distinct() -> ksp_core_lib::R
|
||||
let retry_at = crate::MarketPriceTimestamp::from_unix_millis(1_777_777_777_000);
|
||||
let state = crate::MarketPriceProviderState::new(id, crate::MarketPriceProviderAvailability::CoolingDown { retry_at });
|
||||
assert_eq!(state.availability(), crate::MarketPriceProviderAvailability::CoolingDown { retry_at });
|
||||
assert!(!state.availability().is_refresh_eligible());
|
||||
assert_eq!(state.availability().retry_at(), std::option::Option::Some(retry_at));
|
||||
assert_ne!(state.availability(), crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::Some(retry_at) });
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_registry.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn registry_orders_all_eight_v1_providers_and_exposes_only_generic_entries() -> ksp_core_lib::Result<()> {
|
||||
let entries = match all_v1_entries() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = match crate::MarketPriceProviderRegistry::new(entries) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
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(());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_rejects_duplicate_provider_ids_and_updates_generic_availability() -> ksp_core_lib::Result<()> {
|
||||
let descriptor = match simple_descriptor("provider-a") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let duplicate = std::vec![
|
||||
crate::MarketPriceProviderRegistryEntry::new(descriptor.clone(), crate::MarketPriceProviderAvailability::Disabled),
|
||||
crate::MarketPriceProviderRegistryEntry::new(descriptor, crate::MarketPriceProviderAvailability::Ready),
|
||||
];
|
||||
assert!(crate::MarketPriceProviderRegistry::new(duplicate).is_err());
|
||||
let descriptor = match simple_descriptor("provider-b") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let provider_id = descriptor.id().clone();
|
||||
let retry_at = crate::MarketPriceTimestamp::from_unix_millis(42);
|
||||
let entry = crate::MarketPriceProviderRegistryEntry::new(descriptor, crate::MarketPriceProviderAvailability::CoolingDown { retry_at });
|
||||
let registry = match crate::MarketPriceProviderRegistry::new(std::vec![entry]) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let state = match registry.state(&provider_id) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(test_error("state")),
|
||||
};
|
||||
assert!(!state.availability().is_refresh_eligible());
|
||||
assert_eq!(state.availability().retry_at(), std::option::Option::Some(retry_at));
|
||||
assert_eq!(registry.descriptor(&provider_id).map(|value| return value.id()), std::option::Option::Some(&provider_id));
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn all_v1_entries() -> ksp_core_lib::Result<std::vec::Vec<crate::MarketPriceProviderRegistryEntry>> {
|
||||
let birdeye_settings = 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 birdeye = match crate::MarketPriceBirdeyeProvider::new(birdeye_settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coinbase_settings = match crate::MarketPriceCoinbaseExchangeSettings::new(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coinbase = match crate::MarketPriceCoinbaseExchangeProvider::new(coinbase_settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coingecko_settings = match crate::MarketPriceCoinGeckoSettings::keyless(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coingecko = match crate::MarketPriceCoinGeckoProvider::new(coingecko_settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coinmarketcap_settings = match crate::MarketPriceCoinMarketCapSettings::keyless(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coinmarketcap = match crate::MarketPriceCoinMarketCapProvider::new(coinmarketcap_settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coinpaprika_settings = match crate::MarketPriceCoinPaprikaSettings::new(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coinpaprika = match crate::MarketPriceCoinPaprikaProvider::new(coinpaprika_settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let dexscreener_settings = match crate::MarketPriceDexScreenerSettings::new(false, "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let dexscreener = match crate::MarketPriceDexScreenerProvider::new(dexscreener_settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let jupiter_settings = match crate::MarketPriceJupiterSettings::keyless(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let jupiter = match crate::MarketPriceJupiterProvider::new(jupiter_settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let kraken_settings = match crate::MarketPriceKrakenSettings::new(false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let kraken = match crate::MarketPriceKrakenProvider::new(kraken_settings) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(std::vec![
|
||||
disabled_entry(birdeye.descriptor()),
|
||||
disabled_entry(coinbase.descriptor()),
|
||||
disabled_entry(coingecko.descriptor()),
|
||||
disabled_entry(coinmarketcap.descriptor()),
|
||||
disabled_entry(coinpaprika.descriptor()),
|
||||
disabled_entry(dexscreener.descriptor()),
|
||||
disabled_entry(jupiter.descriptor()),
|
||||
disabled_entry(kraken.descriptor()),
|
||||
]);
|
||||
}
|
||||
|
||||
fn disabled_entry(descriptor: &crate::MarketPriceProviderDescriptor) -> crate::MarketPriceProviderRegistryEntry {
|
||||
return crate::MarketPriceProviderRegistryEntry::new(descriptor.clone(), crate::MarketPriceProviderAvailability::Disabled);
|
||||
}
|
||||
|
||||
fn simple_descriptor(provider_id: &'static str) -> ksp_core_lib::Result<crate::MarketPriceProviderDescriptor> {
|
||||
let provider_id = match crate::MarketPriceProviderId::new(provider_id) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let rate_limit = match crate::MarketPriceProviderRateLimit::fixed(1, 1, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Unspecified) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::MarketPriceProviderDescriptor::new(
|
||||
provider_id,
|
||||
"Provider",
|
||||
crate::MarketPriceSemantics::AggregatedMarket,
|
||||
crate::MarketPriceProviderAuthMode::None,
|
||||
rate_limit,
|
||||
std::option::Option::None,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
fn test_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_REGISTRY_INVALID, "Market-price registry test expectation failed")
|
||||
.with_context("field", field);
|
||||
}
|
||||
Reference in New Issue
Block a user