v0.2.11-pre.002-fix.003

This commit is contained in:
2026-08-25 20:37:09 +02:00
parent 126e8abfcd
commit 2dbddfe367
15 changed files with 499 additions and 271 deletions

View File

@@ -1,13 +1,17 @@
// file: crates/ksp-offchain-transport-lib/src/error.rs
// version: 2
// version: 3
/// Stable off-chain transport error for an invalid exact decimal price.
pub const ERROR_CODE_PRICE_DECIMAL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "price_decimal_invalid");
/// Stable off-chain transport error for an invalid market-price provider descriptor.
pub const ERROR_CODE_PROVIDER_DESCRIPTOR_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "provider_descriptor_invalid");
/// Stable off-chain transport error for an invalid market-price provider identifier.
pub const ERROR_CODE_PROVIDER_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "provider_id_invalid");
/// Stable off-chain transport error for an invalid exact market-price decimal.
pub const ERROR_CODE_MARKET_PRICE_DECIMAL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "market_price_decimal_invalid");
/// Stable off-chain transport error for an invalid normalized market-price observation.
pub const ERROR_CODE_PROVIDER_OBSERVATION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "provider_observation_invalid");
pub const ERROR_CODE_MARKET_PRICE_OBSERVATION_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("offchain_transport", "market_price_observation_invalid");
/// Stable off-chain transport error for an invalid market-price provider descriptor.
pub const ERROR_CODE_MARKET_PRICE_PROVIDER_DESCRIPTOR_INVALID: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("offchain_transport", "market_price_provider_descriptor_invalid");
/// 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 for invalid common market-price provider settings.
pub const ERROR_CODE_PROVIDER_SETTINGS_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "provider_settings_invalid");
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");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/src/lib.rs
// version: 2
// version: 3
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -21,61 +21,61 @@ mod market_price_provider;
mod market_price_settings;
/// Stable error code for an invalid exact decimal price.
pub use self::error::ERROR_CODE_PRICE_DECIMAL_INVALID;
/// Stable error code for an invalid provider descriptor.
pub use self::error::ERROR_CODE_PROVIDER_DESCRIPTOR_INVALID;
/// Stable error code for an invalid provider identifier.
pub use self::error::ERROR_CODE_PROVIDER_ID_INVALID;
pub use self::error::ERROR_CODE_MARKET_PRICE_DECIMAL_INVALID;
/// Stable error code for an invalid provider-neutral observation.
pub use self::error::ERROR_CODE_PROVIDER_OBSERVATION_INVALID;
pub use self::error::ERROR_CODE_MARKET_PRICE_OBSERVATION_INVALID;
/// Stable error code for an invalid provider descriptor.
pub use self::error::ERROR_CODE_MARKET_PRICE_PROVIDER_DESCRIPTOR_INVALID;
/// Stable error code for an invalid provider identifier.
pub use self::error::ERROR_CODE_MARKET_PRICE_PROVIDER_ID_INVALID;
/// Stable error code for invalid common provider settings.
pub use self::error::ERROR_CODE_PROVIDER_SETTINGS_INVALID;
pub use self::error::ERROR_CODE_MARKET_PRICE_PROVIDER_SETTINGS_INVALID;
/// Maximum accepted byte length for one textual decimal input.
pub use self::market_price_decimal::PRICE_DECIMAL_MAX_INPUT_BYTES;
pub use self::market_price_decimal::MARKET_PRICE_DECIMAL_MAX_INPUT_BYTES;
/// Maximum decimal scale retained by the canonical SOL/USD price representation.
pub use self::market_price_decimal::PRICE_DECIMAL_MAX_SCALE;
/// Exact positive decimal value used by the public off-chain price contract.
pub use self::market_price_decimal::PriceDecimal;
pub use self::market_price_decimal::MARKET_PRICE_DECIMAL_MAX_SCALE;
/// Exact positive decimal value used by the public market-price contract.
pub use self::market_price_decimal::MarketPriceDecimal;
/// Maximum safe provenance length attached to one normalized observation.
pub use self::market_price_observation::PRICE_PROVENANCE_MAX_BYTES;
/// Safe bounded provenance supplied by one provider adapter.
pub use self::market_price_observation::PriceProvenance;
/// Millisecond UTC timestamp used for request, receipt, provider and cooldown projections.
pub use self::market_price_observation::PriceTimestamp;
pub use self::market_price_observation::MARKET_PRICE_PROVENANCE_MAX_BYTES;
/// Public V1 SOL/USD observation normalized by Off-chain Transport.
pub use self::market_price_observation::SolUsdPriceObservation;
pub use self::market_price_observation::MarketPriceObservation;
/// Safe bounded provenance supplied by one provider adapter.
pub use self::market_price_observation::MarketPriceProvenance;
/// Millisecond UTC timestamp used for request, receipt, provider and cooldown projections.
pub use self::market_price_observation::MarketPriceTimestamp;
/// Maximum provider display-name length accepted by descriptors.
pub use self::market_price_provider::PRICE_PROVIDER_DISPLAY_NAME_MAX_BYTES;
pub use self::market_price_provider::MARKET_PRICE_PROVIDER_DISPLAY_NAME_MAX_BYTES;
/// Maximum opaque provider identifier length accepted by the public contract.
pub use self::market_price_provider::PRICE_PROVIDER_ID_MAX_BYTES;
pub use self::market_price_provider::MARKET_PRICE_PROVIDER_ID_MAX_BYTES;
/// Only price pair exposed by the `0.2.11` V1 public contract.
pub use self::market_price_provider::PricePair;
pub use self::market_price_provider::MarketPricePair;
/// Generic authentication capability exposed by a configured provider descriptor.
pub use self::market_price_provider::PriceProviderAuthMode;
pub use self::market_price_provider::MarketPriceProviderAuthMode;
/// Generic runtime availability state exposed without provider-specific error parsing.
pub use self::market_price_provider::PriceProviderAvailability;
pub use self::market_price_provider::MarketPriceProviderAvailability;
/// Provider capability and presentation descriptor consumed by provider-agnostic callers.
pub use self::market_price_provider::PriceProviderDescriptor;
pub use self::market_price_provider::MarketPriceProviderDescriptor;
/// Opaque validated provider identifier owned by Off-chain Transport.
pub use self::market_price_provider::PriceProviderId;
pub use self::market_price_provider::MarketPriceProviderId;
/// Long-term provider quota descriptor that is informational rather than an authoritative local counter.
pub use self::market_price_provider::PriceProviderLongTermQuota;
pub use self::market_price_provider::MarketPriceProviderLongTermQuota;
/// Period used by a documented long-term provider quota.
pub use self::market_price_provider::PriceProviderQuotaPeriod;
pub use self::market_price_provider::MarketPriceProviderQuotaPeriod;
/// Unit used by a documented long-term provider quota.
pub use self::market_price_provider::PriceProviderQuotaUnit;
pub use self::market_price_provider::MarketPriceProviderQuotaUnit;
/// Generic provider request-limit capability.
pub use self::market_price_provider::PriceProviderRateLimit;
pub use self::market_price_provider::MarketPriceProviderRateLimit;
/// Shape of one generic provider request-limit capability.
pub use self::market_price_provider::PriceProviderRateLimitKind;
pub use self::market_price_provider::MarketPriceProviderRateLimitKind;
/// Scope to which a provider documents one request limit.
pub use self::market_price_provider::PriceProviderRateLimitScope;
pub use self::market_price_provider::MarketPriceProviderRateLimitScope;
/// Current provider-neutral runtime state projection.
pub use self::market_price_provider::PriceProviderState;
/// Price semantics retained so consumers never assume all providers report equivalent market values.
pub use self::market_price_provider::PriceSemantics;
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;
/// Common provider settings shared by provider-specific runtime settings.
pub use self::market_price_settings::PriceProviderCommonSettings;
pub use self::market_price_settings::MarketPriceProviderCommonSettings;
/// Owning tracing target for events emitted by Off-chain Transport.
pub(crate) use self::constants::TRACING_TARGET;

View File

@@ -1,25 +1,25 @@
// file: crates/ksp-offchain-transport-lib/src/market_price_decimal.rs
// version: 2
// version: 3
/// Maximum accepted UTF-8 byte length for one textual decimal input.
pub const PRICE_DECIMAL_MAX_INPUT_BYTES: usize = 96;
pub const MARKET_PRICE_DECIMAL_MAX_INPUT_BYTES: usize = 96;
/// Maximum scale retained by the canonical exact decimal representation.
pub const PRICE_DECIMAL_MAX_SCALE: u8 = 18;
pub const MARKET_PRICE_DECIMAL_MAX_SCALE: u8 = 18;
/// Exact positive decimal value used for one successful V1 SOL/USD observation.
///
/// The value is represented as a positive `u128` coefficient plus a bounded decimal scale. Trailing fractional zeroes are removed during construction, and
/// Serde serialization always emits the canonical decimal string instead of an IEEE-754 number.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PriceDecimal {
pub struct MarketPriceDecimal {
coefficient: u128,
scale: u8,
}
impl PriceDecimal {
impl MarketPriceDecimal {
/// Parses a positive decimal or bounded scientific-notation value without converting through `f64`.
pub fn parse(source: &str) -> ksp_core_lib::Result<Self> {
if source.is_empty() || source.len() > crate::PRICE_DECIMAL_MAX_INPUT_BYTES || source.trim() != source {
if source.is_empty() || source.len() > crate::MARKET_PRICE_DECIMAL_MAX_INPUT_BYTES || source.trim() != source {
return std::result::Result::Err(invalid_decimal_error());
}
let (significand, exponent) = match split_exponent(source) {
@@ -49,7 +49,7 @@ impl PriceDecimal {
};
effective_scale = 0;
}
if effective_scale > i32::from(crate::PRICE_DECIMAL_MAX_SCALE) {
if effective_scale > i32::from(crate::MARKET_PRICE_DECIMAL_MAX_SCALE) {
return std::result::Result::Err(invalid_decimal_error());
}
let mut scale = match u8::try_from(effective_scale) {
@@ -92,21 +92,21 @@ impl PriceDecimal {
}
}
impl std::fmt::Display for PriceDecimal {
impl std::fmt::Display for MarketPriceDecimal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str(self.to_canonical_string().as_str());
}
}
impl std::str::FromStr for PriceDecimal {
impl std::str::FromStr for MarketPriceDecimal {
type Err = ksp_core_lib::Error;
fn from_str(source: &str) -> std::result::Result<Self, Self::Err> {
return crate::PriceDecimal::parse(source);
return crate::MarketPriceDecimal::parse(source);
}
}
impl serde::Serialize for PriceDecimal {
impl serde::Serialize for MarketPriceDecimal {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
@@ -115,19 +115,19 @@ impl serde::Serialize for PriceDecimal {
}
}
impl<'de> serde::Deserialize<'de> for PriceDecimal {
impl<'de> serde::Deserialize<'de> for MarketPriceDecimal {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
return deserializer.deserialize_str(PriceDecimalVisitor);
return deserializer.deserialize_str(MarketPriceDecimalVisitor);
}
}
struct PriceDecimalVisitor;
struct MarketPriceDecimalVisitor;
impl<'de> serde::de::Visitor<'de> for PriceDecimalVisitor {
type Value = crate::PriceDecimal;
impl<'de> serde::de::Visitor<'de> for MarketPriceDecimalVisitor {
type Value = crate::MarketPriceDecimal;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("a canonicalizable positive decimal string");
@@ -137,9 +137,9 @@ impl<'de> serde::de::Visitor<'de> for PriceDecimalVisitor {
where
E: serde::de::Error,
{
return match crate::PriceDecimal::parse(value) {
return match crate::MarketPriceDecimal::parse(value) {
std::result::Result::Ok(decimal) => std::result::Result::Ok(decimal),
std::result::Result::Err(_) => std::result::Result::Err(E::custom("invalid KSP price decimal")),
std::result::Result::Err(_) => std::result::Result::Err(E::custom("invalid KSP market-price decimal")),
};
}
}
@@ -157,7 +157,7 @@ fn checked_multiply_power_of_ten(mut value: u128, exponent: u32) -> std::option:
}
fn invalid_decimal_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_PRICE_DECIMAL_INVALID, "invalid exact price decimal");
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_DECIMAL_INVALID, "invalid exact market-price decimal");
}
fn significand_digits(significand: &str) -> std::result::Result<(std::string::String, u8), ()> {

View File

@@ -1,16 +1,16 @@
// file: crates/ksp-offchain-transport-lib/src/market_price_observation.rs
// version: 2
// version: 3
/// Maximum UTF-8 byte length accepted for safe provider provenance.
pub const PRICE_PROVENANCE_MAX_BYTES: usize = 256;
pub const MARKET_PRICE_PROVENANCE_MAX_BYTES: usize = 256;
/// Millisecond UTC timestamp used by provider-neutral public projections.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)]
pub struct PriceTimestamp {
pub struct MarketPriceTimestamp {
unix_millis: u64,
}
impl PriceTimestamp {
impl MarketPriceTimestamp {
/// Creates a UTC timestamp from whole milliseconds since Unix epoch.
#[must_use]
pub const fn from_unix_millis(unix_millis: u64) -> Self {
@@ -27,9 +27,9 @@ impl PriceTimestamp {
/// Safe bounded provenance supplied by one provider adapter.
#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(try_from = "std::string::String", into = "std::string::String")]
pub struct PriceProvenance(std::string::String);
pub struct MarketPriceProvenance(std::string::String);
impl PriceProvenance {
impl MarketPriceProvenance {
/// Creates bounded non-empty provenance without accepting control characters.
pub fn new(value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
let value = value.into();
@@ -46,49 +46,49 @@ impl PriceProvenance {
}
}
impl std::convert::TryFrom<std::string::String> for PriceProvenance {
impl std::convert::TryFrom<std::string::String> for MarketPriceProvenance {
type Error = ksp_core_lib::Error;
fn try_from(value: std::string::String) -> std::result::Result<Self, Self::Error> {
return crate::PriceProvenance::new(value);
return crate::MarketPriceProvenance::new(value);
}
}
impl std::convert::From<PriceProvenance> for std::string::String {
fn from(value: PriceProvenance) -> Self {
impl std::convert::From<MarketPriceProvenance> for std::string::String {
fn from(value: MarketPriceProvenance) -> Self {
return value.0;
}
}
/// Public V1 SOL/USD observation normalized by Off-chain Transport.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
pub struct SolUsdPriceObservation {
pair: crate::PricePair,
price: crate::PriceDecimal,
provider_id: crate::PriceProviderId,
provider_timestamp: std::option::Option<crate::PriceTimestamp>,
provenance: crate::PriceProvenance,
received_at: crate::PriceTimestamp,
request_started_at: crate::PriceTimestamp,
semantics: crate::PriceSemantics,
pub struct MarketPriceObservation {
pair: crate::MarketPricePair,
price: crate::MarketPriceDecimal,
provider_id: crate::MarketPriceProviderId,
provider_timestamp: std::option::Option<crate::MarketPriceTimestamp>,
provenance: crate::MarketPriceProvenance,
received_at: crate::MarketPriceTimestamp,
request_started_at: crate::MarketPriceTimestamp,
semantics: crate::MarketPriceSemantics,
}
impl SolUsdPriceObservation {
impl MarketPriceObservation {
/// Creates one successful normalized SOL/USD observation.
pub fn new(
provider_id: crate::PriceProviderId,
price: crate::PriceDecimal,
semantics: crate::PriceSemantics,
request_started_at: crate::PriceTimestamp,
received_at: crate::PriceTimestamp,
provider_timestamp: std::option::Option<crate::PriceTimestamp>,
provenance: crate::PriceProvenance,
provider_id: crate::MarketPriceProviderId,
price: crate::MarketPriceDecimal,
semantics: crate::MarketPriceSemantics,
request_started_at: crate::MarketPriceTimestamp,
received_at: crate::MarketPriceTimestamp,
provider_timestamp: std::option::Option<crate::MarketPriceTimestamp>,
provenance: crate::MarketPriceProvenance,
) -> ksp_core_lib::Result<Self> {
if received_at < request_started_at {
return std::result::Result::Err(observation_error());
}
return std::result::Result::Ok(Self {
pair: crate::PricePair::SolUsd,
pair: crate::MarketPricePair::SolUsd,
price,
provider_id,
provider_timestamp,
@@ -101,59 +101,59 @@ impl SolUsdPriceObservation {
/// Returns the only V1 pair represented by this observation.
#[must_use]
pub const fn pair(&self) -> crate::PricePair {
pub const fn pair(&self) -> crate::MarketPricePair {
return self.pair;
}
/// Returns the exact positive SOL/USD price.
#[must_use]
pub const fn price(&self) -> crate::PriceDecimal {
pub const fn price(&self) -> crate::MarketPriceDecimal {
return self.price;
}
/// Returns the opaque provider identifier.
#[must_use]
pub const fn provider_id(&self) -> &crate::PriceProviderId {
pub const fn provider_id(&self) -> &crate::MarketPriceProviderId {
return &self.provider_id;
}
/// Returns a provider timestamp only when the provider adapter has a real price-time field.
#[must_use]
pub const fn provider_timestamp(&self) -> std::option::Option<crate::PriceTimestamp> {
pub const fn provider_timestamp(&self) -> std::option::Option<crate::MarketPriceTimestamp> {
return self.provider_timestamp;
}
/// Returns safe provider provenance.
#[must_use]
pub const fn provenance(&self) -> &crate::PriceProvenance {
pub const fn provenance(&self) -> &crate::MarketPriceProvenance {
return &self.provenance;
}
/// Returns the KSP wall-clock receipt timestamp.
#[must_use]
pub const fn received_at(&self) -> crate::PriceTimestamp {
pub const fn received_at(&self) -> crate::MarketPriceTimestamp {
return self.received_at;
}
/// Returns the KSP wall-clock request-start timestamp.
#[must_use]
pub const fn request_started_at(&self) -> crate::PriceTimestamp {
pub const fn request_started_at(&self) -> crate::MarketPriceTimestamp {
return self.request_started_at;
}
/// Returns the provider-specific semantic class retained by the normalized observation.
#[must_use]
pub const fn semantics(&self) -> crate::PriceSemantics {
pub const fn semantics(&self) -> crate::MarketPriceSemantics {
return self.semantics;
}
}
fn observation_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_PROVIDER_OBSERVATION_INVALID, "invalid off-chain price observation");
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_OBSERVATION_INVALID, "invalid off-chain market-price observation");
}
fn valid_provenance(value: &str) -> bool {
if value.is_empty() || value.len() > crate::PRICE_PROVENANCE_MAX_BYTES || value.trim() != value {
if value.is_empty() || value.len() > crate::MARKET_PRICE_PROVENANCE_MAX_BYTES || value.trim() != value {
return false;
}
for character in value.chars() {

View File

@@ -1,20 +1,20 @@
// file: crates/ksp-offchain-transport-lib/src/market_price_provider.rs
// version: 2
// version: 3
/// Maximum UTF-8 byte length of one provider display name.
pub const PRICE_PROVIDER_DISPLAY_NAME_MAX_BYTES: usize = 96;
pub const MARKET_PRICE_PROVIDER_DISPLAY_NAME_MAX_BYTES: usize = 96;
/// Maximum byte length of one opaque provider identifier.
pub const PRICE_PROVIDER_ID_MAX_BYTES: usize = 64;
pub const MARKET_PRICE_PROVIDER_ID_MAX_BYTES: usize = 64;
/// Only price pair exposed by the `0.2.11` V1 public contract.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PricePair {
pub enum MarketPricePair {
/// Native SOL quoted directly in US dollars according to one provider's documented semantics.
SolUsd,
}
impl PricePair {
impl MarketPricePair {
/// Returns the stable human-readable pair code.
#[must_use]
pub const fn code(&self) -> &'static str {
@@ -24,10 +24,10 @@ impl PricePair {
}
}
/// Price semantics retained so normalized observations do not imply cross-provider equivalence.
/// Market-price semantics retained so normalized observations do not imply cross-provider equivalence.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceSemantics {
pub enum MarketPriceSemantics {
/// Aggregated market price produced by a multi-market data provider.
AggregatedMarket,
/// USD price associated with one explicitly configured DEX pair.
@@ -43,7 +43,7 @@ pub enum PriceSemantics {
/// Generic authentication capability exposed by one configured provider.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceProviderAuthMode {
pub enum MarketPriceProviderAuthMode {
/// No credential is required for the configured access mode.
None,
/// Provider accepts an API key but also supports an unauthenticated mode selected by configuration.
@@ -55,7 +55,7 @@ pub enum PriceProviderAuthMode {
/// Scope to which a provider documents a request limit.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceProviderRateLimitScope {
pub enum MarketPriceProviderRateLimitScope {
/// Limit is associated with the configured account or API key.
Account,
/// Limit is associated with the source IP address.
@@ -69,7 +69,7 @@ pub enum PriceProviderRateLimitScope {
/// Shape of one generic provider request-limit capability.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceProviderRateLimitKind {
pub enum MarketPriceProviderRateLimitKind {
/// Dynamic or server-driven limit that cannot be represented as one safe fixed local cadence.
Dynamic,
/// Locally enforceable fixed request budget over a documented window.
@@ -78,23 +78,28 @@ pub enum PriceProviderRateLimitKind {
/// Generic provider request-limit capability with validated fixed-limit values.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Serialize)]
pub struct PriceProviderRateLimit {
pub struct MarketPriceProviderRateLimit {
burst: std::option::Option<u32>,
kind: crate::PriceProviderRateLimitKind,
kind: crate::MarketPriceProviderRateLimitKind,
requests: std::option::Option<u32>,
scope: crate::PriceProviderRateLimitScope,
scope: crate::MarketPriceProviderRateLimitScope,
window_seconds: std::option::Option<u32>,
}
impl PriceProviderRateLimit {
impl MarketPriceProviderRateLimit {
/// Creates a validated fixed request limit.
pub fn fixed(requests: u32, window_seconds: u32, burst: std::option::Option<u32>, scope: crate::PriceProviderRateLimitScope) -> ksp_core_lib::Result<Self> {
pub fn fixed(
requests: u32,
window_seconds: u32,
burst: std::option::Option<u32>,
scope: crate::MarketPriceProviderRateLimitScope,
) -> ksp_core_lib::Result<Self> {
if requests == 0 || window_seconds == 0 || burst == std::option::Option::Some(0) {
return std::result::Result::Err(provider_descriptor_error());
}
return std::result::Result::Ok(Self {
burst,
kind: crate::PriceProviderRateLimitKind::Fixed,
kind: crate::MarketPriceProviderRateLimitKind::Fixed,
requests: std::option::Option::Some(requests),
scope,
window_seconds: std::option::Option::Some(window_seconds),
@@ -103,10 +108,10 @@ impl PriceProviderRateLimit {
/// Creates a dynamic/server-driven request-limit descriptor.
#[must_use]
pub const fn dynamic(scope: crate::PriceProviderRateLimitScope) -> Self {
pub const fn dynamic(scope: crate::MarketPriceProviderRateLimitScope) -> Self {
return Self {
burst: std::option::Option::None,
kind: crate::PriceProviderRateLimitKind::Dynamic,
kind: crate::MarketPriceProviderRateLimitKind::Dynamic,
requests: std::option::Option::None,
scope,
window_seconds: std::option::Option::None,
@@ -121,7 +126,7 @@ impl PriceProviderRateLimit {
/// Returns whether the limit is fixed or dynamic/server-driven.
#[must_use]
pub const fn kind(&self) -> crate::PriceProviderRateLimitKind {
pub const fn kind(&self) -> crate::MarketPriceProviderRateLimitKind {
return self.kind;
}
@@ -133,7 +138,7 @@ impl PriceProviderRateLimit {
/// Returns the documented limit scope.
#[must_use]
pub const fn scope(&self) -> crate::PriceProviderRateLimitScope {
pub const fn scope(&self) -> crate::MarketPriceProviderRateLimitScope {
return self.scope;
}
@@ -147,7 +152,7 @@ impl PriceProviderRateLimit {
/// Period used by one documented long-term provider quota.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceProviderQuotaPeriod {
pub enum MarketPriceProviderQuotaPeriod {
/// Quota resets on a provider-defined daily period.
Day,
/// Quota resets on a provider-defined monthly period.
@@ -157,7 +162,7 @@ pub enum PriceProviderQuotaPeriod {
/// Unit used by one documented long-term provider quota.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceProviderQuotaUnit {
pub enum MarketPriceProviderQuotaUnit {
/// Provider-specific credits, not assumed to equal HTTP requests.
Credits,
/// HTTP/API requests.
@@ -166,15 +171,15 @@ pub enum PriceProviderQuotaUnit {
/// Long-term provider quota descriptor exposed as non-authoritative capability metadata.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Serialize)]
pub struct PriceProviderLongTermQuota {
pub struct MarketPriceProviderLongTermQuota {
amount: u64,
period: crate::PriceProviderQuotaPeriod,
unit: crate::PriceProviderQuotaUnit,
period: crate::MarketPriceProviderQuotaPeriod,
unit: crate::MarketPriceProviderQuotaUnit,
}
impl PriceProviderLongTermQuota {
impl MarketPriceProviderLongTermQuota {
/// Creates a non-zero documented quota descriptor.
pub fn new(amount: u64, period: crate::PriceProviderQuotaPeriod, unit: crate::PriceProviderQuotaUnit) -> ksp_core_lib::Result<Self> {
pub fn new(amount: u64, period: crate::MarketPriceProviderQuotaPeriod, unit: crate::MarketPriceProviderQuotaUnit) -> ksp_core_lib::Result<Self> {
if amount == 0 {
return std::result::Result::Err(provider_descriptor_error());
}
@@ -189,13 +194,13 @@ impl PriceProviderLongTermQuota {
/// Returns the provider-defined quota period.
#[must_use]
pub const fn period(&self) -> crate::PriceProviderQuotaPeriod {
pub const fn period(&self) -> crate::MarketPriceProviderQuotaPeriod {
return self.period;
}
/// Returns the documented quota unit.
#[must_use]
pub const fn unit(&self) -> crate::PriceProviderQuotaUnit {
pub const fn unit(&self) -> crate::MarketPriceProviderQuotaUnit {
return self.unit;
}
}
@@ -203,9 +208,9 @@ impl PriceProviderLongTermQuota {
/// Opaque validated provider identifier owned by Off-chain Transport.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)]
#[serde(try_from = "std::string::String", into = "std::string::String")]
pub struct PriceProviderId(std::string::String);
pub struct MarketPriceProviderId(std::string::String);
impl PriceProviderId {
impl MarketPriceProviderId {
/// Creates one bounded stable provider identifier.
pub fn new(value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, "validating market-price provider identifier");
@@ -213,7 +218,7 @@ impl PriceProviderId {
if !valid_provider_id(value.as_str()) {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = "provider_id", "rejected invalid market-price provider identifier");
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_PROVIDER_ID_INVALID,
crate::ERROR_CODE_MARKET_PRICE_PROVIDER_ID_INVALID,
"invalid off-chain market-price provider identifier",
));
}
@@ -227,47 +232,47 @@ impl PriceProviderId {
}
}
impl std::fmt::Display for PriceProviderId {
impl std::fmt::Display for MarketPriceProviderId {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str(self.0.as_str());
}
}
impl std::convert::TryFrom<std::string::String> for PriceProviderId {
impl std::convert::TryFrom<std::string::String> for MarketPriceProviderId {
type Error = ksp_core_lib::Error;
fn try_from(value: std::string::String) -> std::result::Result<Self, Self::Error> {
return crate::PriceProviderId::new(value);
return crate::MarketPriceProviderId::new(value);
}
}
impl std::convert::From<PriceProviderId> for std::string::String {
fn from(value: PriceProviderId) -> Self {
impl std::convert::From<MarketPriceProviderId> for std::string::String {
fn from(value: MarketPriceProviderId) -> Self {
return value.0;
}
}
/// Provider capability and presentation descriptor consumed by provider-agnostic callers.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
pub struct PriceProviderDescriptor {
auth_mode: crate::PriceProviderAuthMode,
pub struct MarketPriceProviderDescriptor {
auth_mode: crate::MarketPriceProviderAuthMode,
display_name: std::string::String,
id: crate::PriceProviderId,
long_term_quota: std::option::Option<crate::PriceProviderLongTermQuota>,
rate_limit: crate::PriceProviderRateLimit,
semantics: crate::PriceSemantics,
id: crate::MarketPriceProviderId,
long_term_quota: std::option::Option<crate::MarketPriceProviderLongTermQuota>,
rate_limit: crate::MarketPriceProviderRateLimit,
semantics: crate::MarketPriceSemantics,
supports_sol_usd: bool,
}
impl PriceProviderDescriptor {
impl MarketPriceProviderDescriptor {
/// Creates a validated provider-neutral descriptor.
pub fn new(
id: crate::PriceProviderId,
id: crate::MarketPriceProviderId,
display_name: impl std::convert::Into<std::string::String>,
semantics: crate::PriceSemantics,
auth_mode: crate::PriceProviderAuthMode,
rate_limit: crate::PriceProviderRateLimit,
long_term_quota: std::option::Option<crate::PriceProviderLongTermQuota>,
semantics: crate::MarketPriceSemantics,
auth_mode: crate::MarketPriceProviderAuthMode,
rate_limit: crate::MarketPriceProviderRateLimit,
long_term_quota: std::option::Option<crate::MarketPriceProviderLongTermQuota>,
supports_sol_usd: bool,
) -> ksp_core_lib::Result<Self> {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, "validating market-price provider descriptor");
@@ -281,7 +286,7 @@ impl PriceProviderDescriptor {
/// Returns the configured authentication capability.
#[must_use]
pub const fn auth_mode(&self) -> crate::PriceProviderAuthMode {
pub const fn auth_mode(&self) -> crate::MarketPriceProviderAuthMode {
return self.auth_mode;
}
@@ -293,25 +298,25 @@ impl PriceProviderDescriptor {
/// Returns the opaque provider identifier.
#[must_use]
pub const fn id(&self) -> &crate::PriceProviderId {
pub const fn id(&self) -> &crate::MarketPriceProviderId {
return &self.id;
}
/// Returns optional long-term quota metadata without exposing a local remaining counter.
#[must_use]
pub const fn long_term_quota(&self) -> std::option::Option<crate::PriceProviderLongTermQuota> {
pub const fn long_term_quota(&self) -> std::option::Option<crate::MarketPriceProviderLongTermQuota> {
return self.long_term_quota;
}
/// Returns the configured request-limit capability.
#[must_use]
pub const fn rate_limit(&self) -> crate::PriceProviderRateLimit {
pub const fn rate_limit(&self) -> crate::MarketPriceProviderRateLimit {
return self.rate_limit;
}
/// Returns the documented price semantics.
#[must_use]
pub const fn semantics(&self) -> crate::PriceSemantics {
pub const fn semantics(&self) -> crate::MarketPriceSemantics {
return self.semantics;
}
@@ -325,13 +330,13 @@ impl PriceProviderDescriptor {
/// Generic runtime availability state exposed without provider-specific error parsing.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum PriceProviderAvailability {
pub enum MarketPriceProviderAvailability {
/// Required authentication material is unavailable or rejected.
AuthenticationUnavailable,
/// Provider is locally cooling down until the supplied timestamp.
CoolingDown {
/// Earliest known wall-clock timestamp at which a new attempt may be admitted.
retry_at: crate::PriceTimestamp,
retry_at: crate::MarketPriceTimestamp,
},
/// Provider is disabled by runtime configuration.
Disabled,
@@ -344,43 +349,43 @@ pub enum PriceProviderAvailability {
/// Transport/provider failure is transient; retry time is present only when actually known.
TemporarilyUnavailable {
/// Optional next retry timestamp derived from safe runtime/provider information.
retry_at: std::option::Option<crate::PriceTimestamp>,
retry_at: std::option::Option<crate::MarketPriceTimestamp>,
},
}
/// Current provider-neutral runtime state projection.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
pub struct PriceProviderState {
availability: crate::PriceProviderAvailability,
provider_id: crate::PriceProviderId,
pub struct MarketPriceProviderState {
availability: crate::MarketPriceProviderAvailability,
provider_id: crate::MarketPriceProviderId,
}
impl PriceProviderState {
impl MarketPriceProviderState {
/// Creates one generic state projection for a configured provider.
#[must_use]
pub fn new(provider_id: crate::PriceProviderId, availability: crate::PriceProviderAvailability) -> Self {
pub fn new(provider_id: crate::MarketPriceProviderId, availability: crate::MarketPriceProviderAvailability) -> Self {
return Self { availability, provider_id };
}
/// Returns the generic availability classification.
#[must_use]
pub const fn availability(&self) -> crate::PriceProviderAvailability {
pub const fn availability(&self) -> crate::MarketPriceProviderAvailability {
return self.availability;
}
/// Returns the opaque provider identifier.
#[must_use]
pub const fn provider_id(&self) -> &crate::PriceProviderId {
pub const fn provider_id(&self) -> &crate::MarketPriceProviderId {
return &self.provider_id;
}
}
fn provider_descriptor_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_PROVIDER_DESCRIPTOR_INVALID, "invalid off-chain market-price provider descriptor");
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_PROVIDER_DESCRIPTOR_INVALID, "invalid off-chain market-price provider descriptor");
}
fn valid_display_name(value: &str) -> bool {
if value.is_empty() || value.len() > crate::PRICE_PROVIDER_DISPLAY_NAME_MAX_BYTES || value.trim() != value {
if value.is_empty() || value.len() > crate::MARKET_PRICE_PROVIDER_DISPLAY_NAME_MAX_BYTES || value.trim() != value {
return false;
}
for character in value.chars() {
@@ -392,7 +397,7 @@ fn valid_display_name(value: &str) -> bool {
}
fn valid_provider_id(value: &str) -> bool {
if value.is_empty() || value.len() > crate::PRICE_PROVIDER_ID_MAX_BYTES {
if value.is_empty() || value.len() > crate::MARKET_PRICE_PROVIDER_ID_MAX_BYTES {
return false;
}
for byte in value.bytes() {

View File

@@ -1,20 +1,20 @@
// file: crates/ksp-offchain-transport-lib/src/market_price_settings.rs
// version: 2
// version: 3
/// Common provider settings embedded by future provider-specific runtime settings.
///
/// Provider-specific credentials, pair selectors and access modes intentionally do not live here because providers without those capabilities must not be
/// forced into artificial fields.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
pub struct PriceProviderCommonSettings {
pub struct MarketPriceProviderCommonSettings {
enabled: bool,
provider_id: crate::PriceProviderId,
provider_id: crate::MarketPriceProviderId,
}
impl PriceProviderCommonSettings {
impl MarketPriceProviderCommonSettings {
/// Creates common settings for one uniquely identified runtime provider instance.
#[must_use]
pub fn new(provider_id: crate::PriceProviderId, enabled: bool) -> Self {
pub fn new(provider_id: crate::MarketPriceProviderId, enabled: bool) -> Self {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, enabled = enabled, "constructed common market-price provider settings");
return Self { enabled, provider_id };
}
@@ -27,7 +27,7 @@ impl PriceProviderCommonSettings {
/// Returns the opaque runtime provider identifier.
#[must_use]
pub const fn provider_id(&self) -> &crate::PriceProviderId {
pub const fn provider_id(&self) -> &crate::MarketPriceProviderId {
return &self.provider_id;
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/tests/public_api.rs
// version: 2
// version: 3
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -8,34 +8,34 @@
//! Public API canaries for the first Off-chain Transport market-price capability family.
#[test]
fn public_pre_002_price_foundation_is_available_from_crate_root() -> ksp_core_lib::Result<()> {
assert_eq!(ksp_offchain_transport_lib::PricePair::SolUsd.code(), "SOL/USD");
assert_eq!(ksp_offchain_transport_lib::PRICE_DECIMAL_MAX_SCALE, 18);
let price = match ksp_offchain_transport_lib::PriceDecimal::parse("201.2500") {
fn public_pre_002_market_price_foundation_is_available_from_crate_root() -> ksp_core_lib::Result<()> {
assert_eq!(ksp_offchain_transport_lib::MarketPricePair::SolUsd.code(), "SOL/USD");
assert_eq!(ksp_offchain_transport_lib::MARKET_PRICE_DECIMAL_MAX_SCALE, 18);
let price = match ksp_offchain_transport_lib::MarketPriceDecimal::parse("201.2500") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(price.to_canonical_string(), "201.25");
let id = match ksp_offchain_transport_lib::PriceProviderId::new("provider-canary") {
let id = match ksp_offchain_transport_lib::MarketPriceProviderId::new("provider-canary") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let common = ksp_offchain_transport_lib::PriceProviderCommonSettings::new(id.clone(), true);
let common = ksp_offchain_transport_lib::MarketPriceProviderCommonSettings::new(id.clone(), true);
assert_eq!(common.provider_id(), &id);
let rate_limit = match ksp_offchain_transport_lib::PriceProviderRateLimit::fixed(
let rate_limit = match ksp_offchain_transport_lib::MarketPriceProviderRateLimit::fixed(
1,
1,
std::option::Option::None,
ksp_offchain_transport_lib::PriceProviderRateLimitScope::Ip,
ksp_offchain_transport_lib::MarketPriceProviderRateLimitScope::Ip,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let descriptor = match ksp_offchain_transport_lib::PriceProviderDescriptor::new(
let descriptor = match ksp_offchain_transport_lib::MarketPriceProviderDescriptor::new(
id.clone(),
"Provider Canary",
ksp_offchain_transport_lib::PriceSemantics::AggregatedMarket,
ksp_offchain_transport_lib::PriceProviderAuthMode::None,
ksp_offchain_transport_lib::MarketPriceSemantics::AggregatedMarket,
ksp_offchain_transport_lib::MarketPriceProviderAuthMode::None,
rate_limit,
std::option::Option::None,
true,
@@ -44,15 +44,15 @@ fn public_pre_002_price_foundation_is_available_from_crate_root() -> ksp_core_li
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(descriptor.supports_sol_usd());
let provenance = match ksp_offchain_transport_lib::PriceProvenance::new("canary") {
let provenance = match ksp_offchain_transport_lib::MarketPriceProvenance::new("canary") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let timestamp = ksp_offchain_transport_lib::PriceTimestamp::from_unix_millis(1);
let observation = match ksp_offchain_transport_lib::SolUsdPriceObservation::new(
let timestamp = ksp_offchain_transport_lib::MarketPriceTimestamp::from_unix_millis(1);
let observation = match ksp_offchain_transport_lib::MarketPriceObservation::new(
id,
price,
ksp_offchain_transport_lib::PriceSemantics::AggregatedMarket,
ksp_offchain_transport_lib::MarketPriceSemantics::AggregatedMarket,
timestamp,
timestamp,
std::option::Option::None,
@@ -68,11 +68,11 @@ fn public_pre_002_price_foundation_is_available_from_crate_root() -> ksp_core_li
#[test]
fn offchain_error_codes_use_owned_domain() {
let codes = [
ksp_offchain_transport_lib::ERROR_CODE_PRICE_DECIMAL_INVALID,
ksp_offchain_transport_lib::ERROR_CODE_PROVIDER_DESCRIPTOR_INVALID,
ksp_offchain_transport_lib::ERROR_CODE_PROVIDER_ID_INVALID,
ksp_offchain_transport_lib::ERROR_CODE_PROVIDER_OBSERVATION_INVALID,
ksp_offchain_transport_lib::ERROR_CODE_PROVIDER_SETTINGS_INVALID,
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_DECIMAL_INVALID,
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_PROVIDER_DESCRIPTOR_INVALID,
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_PROVIDER_ID_INVALID,
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_OBSERVATION_INVALID,
ksp_offchain_transport_lib::ERROR_CODE_MARKET_PRICE_PROVIDER_SETTINGS_INVALID,
];
for code in codes {
assert_eq!(code.domain(), "offchain_transport");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_decimal.rs
// version: 2
// version: 3
#[test]
fn decimal_normalizes_fractional_and_scientific_forms_without_f64() -> ksp_core_lib::Result<()> {
@@ -11,7 +11,7 @@ fn decimal_normalizes_fractional_and_scientific_forms_without_f64() -> ksp_core_
("1e-3", "0.001", 1, 3),
];
for (source, expected, coefficient, scale) in cases {
let value = match crate::PriceDecimal::parse(source) {
let value = match crate::MarketPriceDecimal::parse(source) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
@@ -26,34 +26,34 @@ fn decimal_normalizes_fractional_and_scientific_forms_without_f64() -> ksp_core_
fn decimal_rejects_zero_negative_nonfinite_excessive_scale_and_overflow() {
let invalid = ["0", "0.000", "-1", "+1", "NaN", "inf", "1e-19", "1e129", "340282366920938463463374607431768211456", " 1", "1 ", ".1", "1."];
for source in invalid {
let result = crate::PriceDecimal::parse(source);
let result = crate::MarketPriceDecimal::parse(source);
assert!(result.is_err());
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_PRICE_DECIMAL_INVALID);
assert_eq!(error.code(), crate::ERROR_CODE_MARKET_PRICE_DECIMAL_INVALID);
}
}
}
#[test]
fn decimal_serde_is_canonical_string_and_round_trips_exactly() -> ksp_core_lib::Result<()> {
let value = match crate::PriceDecimal::parse("123.4500") {
let value = match crate::MarketPriceDecimal::parse("123.4500") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let encoded = match serde_json::to_string(&value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_PRICE_DECIMAL_INVALID, "test serialization failed"));
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_DECIMAL_INVALID, "test serialization failed"));
},
};
assert_eq!(encoded, "\"123.45\"");
let decoded: crate::PriceDecimal = match serde_json::from_str(encoded.as_str()) {
let decoded: crate::MarketPriceDecimal = match serde_json::from_str(encoded.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_PRICE_DECIMAL_INVALID, "test deserialization failed"));
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_DECIMAL_INVALID, "test deserialization failed"));
},
};
assert_eq!(decoded, value);
assert!(serde_json::from_str::<crate::PriceDecimal>("123.45").is_err());
assert!(serde_json::from_str::<crate::MarketPriceDecimal>("123.45").is_err());
return std::result::Result::Ok(());
}

View File

@@ -1,26 +1,26 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_observation.rs
// version: 2
// version: 3
#[test]
fn observation_preserves_pair_exact_price_semantics_timestamps_and_safe_provenance() -> ksp_core_lib::Result<()> {
let provider_id = match crate::PriceProviderId::new("kraken") {
let provider_id = match crate::MarketPriceProviderId::new("kraken") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let price = match crate::PriceDecimal::parse("204.125") {
let price = match crate::MarketPriceDecimal::parse("204.125") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provenance = match crate::PriceProvenance::new("market=SOL/USD") {
let provenance = match crate::MarketPriceProvenance::new("market=SOL/USD") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let started = crate::PriceTimestamp::from_unix_millis(10_000);
let received = crate::PriceTimestamp::from_unix_millis(10_250);
let observation = match crate::SolUsdPriceObservation::new(
let started = crate::MarketPriceTimestamp::from_unix_millis(10_000);
let received = crate::MarketPriceTimestamp::from_unix_millis(10_250);
let observation = match crate::MarketPriceObservation::new(
provider_id,
price,
crate::PriceSemantics::ExchangeLastTrade,
crate::MarketPriceSemantics::ExchangeLastTrade,
started,
received,
std::option::Option::None,
@@ -29,10 +29,10 @@ fn observation_preserves_pair_exact_price_semantics_timestamps_and_safe_provenan
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(observation.pair(), crate::PricePair::SolUsd);
assert_eq!(observation.pair(), crate::MarketPricePair::SolUsd);
assert_eq!(observation.pair().code(), "SOL/USD");
assert_eq!(observation.price(), price);
assert_eq!(observation.semantics(), crate::PriceSemantics::ExchangeLastTrade);
assert_eq!(observation.semantics(), crate::MarketPriceSemantics::ExchangeLastTrade);
assert_eq!(observation.request_started_at(), started);
assert_eq!(observation.received_at(), received);
assert_eq!(observation.provider_timestamp(), std::option::Option::None);
@@ -42,25 +42,25 @@ fn observation_preserves_pair_exact_price_semantics_timestamps_and_safe_provenan
#[test]
fn observation_rejects_reversed_ksp_timestamps_and_unsafe_provenance() -> ksp_core_lib::Result<()> {
assert!(crate::PriceProvenance::new("line\nbreak").is_err());
let provider_id = match crate::PriceProviderId::new("coingecko") {
assert!(crate::MarketPriceProvenance::new("line\nbreak").is_err());
let provider_id = match crate::MarketPriceProviderId::new("coingecko") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let price = match crate::PriceDecimal::parse("200") {
let price = match crate::MarketPriceDecimal::parse("200") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provenance = match crate::PriceProvenance::new("asset=solana") {
let provenance = match crate::MarketPriceProvenance::new("asset=solana") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = crate::SolUsdPriceObservation::new(
let result = crate::MarketPriceObservation::new(
provider_id,
price,
crate::PriceSemantics::AggregatedMarket,
crate::PriceTimestamp::from_unix_millis(2),
crate::PriceTimestamp::from_unix_millis(1),
crate::MarketPriceSemantics::AggregatedMarket,
crate::MarketPriceTimestamp::from_unix_millis(2),
crate::MarketPriceTimestamp::from_unix_millis(1),
std::option::Option::None,
provenance,
);

View File

@@ -1,39 +1,43 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_provider.rs
// version: 2
// version: 3
#[test]
fn provider_id_is_opaque_bounded_and_stable() -> ksp_core_lib::Result<()> {
let id = match crate::PriceProviderId::new("coingecko-main") {
let id = match crate::MarketPriceProviderId::new("coingecko-main") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(id.as_str(), "coingecko-main");
assert_eq!(id.to_string(), "coingecko-main");
for invalid in ["", "CoinGecko", "coin gecko", "coin/gecko", "é"] {
assert!(crate::PriceProviderId::new(invalid).is_err());
assert!(crate::MarketPriceProviderId::new(invalid).is_err());
}
return std::result::Result::Ok(());
}
#[test]
fn provider_descriptor_preserves_semantics_auth_limits_and_informational_quota() -> ksp_core_lib::Result<()> {
let id = match crate::PriceProviderId::new("provider-a") {
let id = match crate::MarketPriceProviderId::new("provider-a") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rate_limit = match crate::PriceProviderRateLimit::fixed(1, 2, std::option::Option::None, crate::PriceProviderRateLimitScope::Ip) {
let rate_limit = match crate::MarketPriceProviderRateLimit::fixed(1, 2, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Ip) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let quota = match crate::PriceProviderLongTermQuota::new(10_000, crate::PriceProviderQuotaPeriod::Month, crate::PriceProviderQuotaUnit::Credits) {
let quota = match crate::MarketPriceProviderLongTermQuota::new(
10_000,
crate::MarketPriceProviderQuotaPeriod::Month,
crate::MarketPriceProviderQuotaUnit::Credits,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let descriptor = match crate::PriceProviderDescriptor::new(
let descriptor = match crate::MarketPriceProviderDescriptor::new(
id,
"Provider A",
crate::PriceSemantics::AggregatedMarket,
crate::PriceProviderAuthMode::OptionalApiKey,
crate::MarketPriceSemantics::AggregatedMarket,
crate::MarketPriceProviderAuthMode::OptionalApiKey,
rate_limit,
std::option::Option::Some(quota),
true,
@@ -42,8 +46,8 @@ 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.semantics(), crate::PriceSemantics::AggregatedMarket);
assert_eq!(descriptor.auth_mode(), crate::PriceProviderAuthMode::OptionalApiKey);
assert_eq!(descriptor.semantics(), crate::MarketPriceSemantics::AggregatedMarket);
assert_eq!(descriptor.auth_mode(), crate::MarketPriceProviderAuthMode::OptionalApiKey);
assert_eq!(descriptor.rate_limit(), rate_limit);
assert_eq!(descriptor.long_term_quota(), std::option::Option::Some(quota));
assert!(descriptor.supports_sol_usd());
@@ -52,12 +56,12 @@ fn provider_descriptor_preserves_semantics_auth_limits_and_informational_quota()
#[test]
fn provider_limit_descriptors_reject_zero_and_model_dynamic_scope() {
assert!(crate::PriceProviderRateLimit::fixed(0, 1, std::option::Option::None, crate::PriceProviderRateLimitScope::Ip).is_err());
assert!(crate::PriceProviderRateLimit::fixed(1, 0, std::option::Option::None, crate::PriceProviderRateLimitScope::Ip).is_err());
assert!(crate::PriceProviderRateLimit::fixed(1, 1, std::option::Option::Some(0), crate::PriceProviderRateLimitScope::Ip).is_err());
let dynamic = crate::PriceProviderRateLimit::dynamic(crate::PriceProviderRateLimitScope::Ip);
assert_eq!(dynamic.kind(), crate::PriceProviderRateLimitKind::Dynamic);
assert_eq!(dynamic.scope(), crate::PriceProviderRateLimitScope::Ip);
assert!(crate::MarketPriceProviderRateLimit::fixed(0, 1, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Ip).is_err());
assert!(crate::MarketPriceProviderRateLimit::fixed(1, 0, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Ip).is_err());
assert!(crate::MarketPriceProviderRateLimit::fixed(1, 1, std::option::Option::Some(0), crate::MarketPriceProviderRateLimitScope::Ip).is_err());
let dynamic = crate::MarketPriceProviderRateLimit::dynamic(crate::MarketPriceProviderRateLimitScope::Ip);
assert_eq!(dynamic.kind(), crate::MarketPriceProviderRateLimitKind::Dynamic);
assert_eq!(dynamic.scope(), crate::MarketPriceProviderRateLimitScope::Ip);
assert_eq!(dynamic.requests(), std::option::Option::None);
assert_eq!(dynamic.window_seconds(), std::option::Option::None);
assert_eq!(dynamic.burst(), std::option::Option::None);
@@ -65,13 +69,13 @@ fn provider_limit_descriptors_reject_zero_and_model_dynamic_scope() {
#[test]
fn provider_availability_keeps_cooldown_and_outage_distinct() -> ksp_core_lib::Result<()> {
let id = match crate::PriceProviderId::new("jupiter") {
let id = match crate::MarketPriceProviderId::new("jupiter") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let retry_at = crate::PriceTimestamp::from_unix_millis(1_777_777_777_000);
let state = crate::PriceProviderState::new(id, crate::PriceProviderAvailability::CoolingDown { retry_at });
assert_eq!(state.availability(), crate::PriceProviderAvailability::CoolingDown { retry_at });
assert_ne!(state.availability(), crate::PriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::Some(retry_at) });
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_ne!(state.availability(), crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::Some(retry_at) });
return std::result::Result::Ok(());
}

View File

@@ -1,19 +1,19 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_settings.rs
// version: 2
// version: 3
#[test]
fn common_settings_contain_only_generic_identity_and_enablement() -> ksp_core_lib::Result<()> {
let provider_id = match crate::PriceProviderId::new("coinpaprika") {
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 settings = crate::PriceProviderCommonSettings::new(provider_id, true);
let settings = crate::MarketPriceProviderCommonSettings::new(provider_id, true);
assert!(settings.enabled());
assert_eq!(settings.provider_id().as_str(), "coinpaprika");
let serialized = match serde_json::to_value(&settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_PROVIDER_SETTINGS_INVALID, "test serialization failed"));
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_PROVIDER_SETTINGS_INVALID, "test serialization failed"));
},
};
assert!(serialized.get("api_key").is_none());