v0.2.11-pre.002

This commit is contained in:
2026-08-25 19:45:06 +02:00
parent f42fa8f4f1
commit 4f92fb03f1
17 changed files with 1610 additions and 44 deletions

View File

@@ -0,0 +1,225 @@
// file: crates/ksp-offchain-transport-lib/src/decimal.rs
// version: 1
/// Maximum accepted UTF-8 byte length for one textual decimal input.
pub const PRICE_DECIMAL_MAX_INPUT_BYTES: usize = 96;
/// Maximum scale retained by the canonical exact decimal representation.
pub const 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 {
coefficient: u128,
scale: u8,
}
impl PriceDecimal {
/// 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 {
return std::result::Result::Err(invalid_decimal_error());
}
let (significand, exponent) = match split_exponent(source) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(()) => return std::result::Result::Err(invalid_decimal_error()),
};
let (digits, fractional_digits) = match significand_digits(significand) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(()) => return std::result::Result::Err(invalid_decimal_error()),
};
let mut coefficient = match digits.parse::<u128>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(invalid_decimal_error()),
};
if coefficient == 0 {
return std::result::Result::Err(invalid_decimal_error());
}
let mut effective_scale = i32::from(fractional_digits) - exponent;
if effective_scale < 0 {
let multiplication_power = match u32::try_from(-effective_scale) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(invalid_decimal_error()),
};
coefficient = match checked_multiply_power_of_ten(coefficient, multiplication_power) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(invalid_decimal_error()),
};
effective_scale = 0;
}
if effective_scale > i32::from(crate::PRICE_DECIMAL_MAX_SCALE) {
return std::result::Result::Err(invalid_decimal_error());
}
let mut scale = match u8::try_from(effective_scale) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(invalid_decimal_error()),
};
while scale > 0 && coefficient % 10 == 0 {
coefficient /= 10;
scale -= 1;
}
return std::result::Result::Ok(Self { coefficient, scale });
}
/// Returns the normalized integer coefficient.
#[must_use]
pub const fn coefficient(&self) -> u128 {
return self.coefficient;
}
/// Returns the normalized decimal scale.
#[must_use]
pub const fn scale(&self) -> u8 {
return self.scale;
}
/// Returns the canonical non-scientific decimal representation.
#[must_use]
pub fn to_canonical_string(&self) -> std::string::String {
let digits = self.coefficient.to_string();
if self.scale == 0 {
return digits;
}
let scale = usize::from(self.scale);
if digits.len() > scale {
let split = digits.len() - scale;
return std::format!("{}.{}", &digits[..split], &digits[split..]);
}
let zero_count = scale - digits.len();
return std::format!("0.{}{}", "0".repeat(zero_count), digits);
}
}
impl std::fmt::Display for PriceDecimal {
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 {
type Err = ksp_core_lib::Error;
fn from_str(source: &str) -> std::result::Result<Self, Self::Err> {
return crate::PriceDecimal::parse(source);
}
}
impl serde::Serialize for PriceDecimal {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
return serializer.serialize_str(self.to_canonical_string().as_str());
}
}
impl<'de> serde::Deserialize<'de> for PriceDecimal {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
return deserializer.deserialize_str(PriceDecimalVisitor);
}
}
struct PriceDecimalVisitor;
impl<'de> serde::de::Visitor<'de> for PriceDecimalVisitor {
type Value = crate::PriceDecimal;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("a canonicalizable positive decimal string");
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
where
E: serde::de::Error,
{
return match crate::PriceDecimal::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")),
};
}
}
fn checked_multiply_power_of_ten(mut value: u128, exponent: u32) -> std::option::Option<u128> {
let mut remaining = exponent;
while remaining > 0 {
value = match value.checked_mul(10) {
std::option::Option::Some(next) => next,
std::option::Option::None => return std::option::Option::None,
};
remaining -= 1;
}
return std::option::Option::Some(value);
}
fn invalid_decimal_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_PRICE_DECIMAL_INVALID, "invalid exact price decimal");
}
fn significand_digits(significand: &str) -> std::result::Result<(std::string::String, u8), ()> {
if significand.is_empty() || significand.starts_with('-') || significand.starts_with('+') {
return std::result::Result::Err(());
}
let mut digits = std::string::String::with_capacity(significand.len());
let mut fractional_digits: usize = 0;
let mut decimal_seen = false;
let mut digit_seen = false;
for character in significand.chars() {
if character.is_ascii_digit() {
digits.push(character);
digit_seen = true;
if decimal_seen {
fractional_digits += 1;
}
} else if character == '.' && !decimal_seen {
decimal_seen = true;
} else {
return std::result::Result::Err(());
}
}
if !digit_seen || significand.ends_with('.') || significand.starts_with('.') {
return std::result::Result::Err(());
}
let fractional_digits = match u8::try_from(fractional_digits) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(()),
};
return std::result::Result::Ok((digits, fractional_digits));
}
fn split_exponent(source: &str) -> std::result::Result<(&str, i32), ()> {
let mut separator_index = std::option::Option::None;
for (index, character) in source.char_indices() {
if character == 'e' || character == 'E' {
if separator_index.is_some() {
return std::result::Result::Err(());
}
separator_index = std::option::Option::Some(index);
}
}
let index = match separator_index {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok((source, 0)),
};
let significand = &source[..index];
let exponent_source = &source[index + 1..];
if exponent_source.is_empty() || exponent_source.len() > 4 {
return std::result::Result::Err(());
}
let exponent = match exponent_source.parse::<i32>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(()),
};
if !(-128..=128).contains(&exponent) {
return std::result::Result::Err(());
}
return std::result::Result::Ok((significand, exponent));
}
#[cfg(test)]
#[path = "../unit_tests/decimal.rs"]
mod tests;

View File

@@ -0,0 +1,13 @@
// file: crates/ksp-offchain-transport-lib/src/error.rs
// version: 1
/// 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 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 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 normalized observation.
pub const ERROR_CODE_PROVIDER_OBSERVATION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "provider_observation_invalid");
/// Stable off-chain transport error for invalid common provider settings.
pub const ERROR_CODE_PROVIDER_SETTINGS_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("offchain_transport", "provider_settings_invalid");

View File

@@ -0,0 +1,75 @@
// file: crates/ksp-offchain-transport-lib/src/lib.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! KSP-owned off-chain transport foundation.
//!
//! `0.2.11-pre.002` opens the first deliberately narrow public surface: exact SOL/USD price values, provider-neutral observations, provider descriptors,
//! common provider settings and generic availability/rate-limit metadata. No provider wire type, HTTP client, provider SDK, Config dependency or active
//! rate limiter is introduced by this tranche.
mod decimal;
mod error;
mod observation;
mod provider;
mod settings;
/// Maximum accepted byte length for one textual decimal input.
pub use self::decimal::PRICE_DECIMAL_MAX_INPUT_BYTES;
/// Maximum decimal scale retained by the canonical SOL/USD price representation.
pub use self::decimal::PRICE_DECIMAL_MAX_SCALE;
/// Exact positive decimal value used by the public off-chain price contract.
pub use self::decimal::PriceDecimal;
/// 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;
/// Stable error code for an invalid provider-neutral observation.
pub use self::error::ERROR_CODE_PROVIDER_OBSERVATION_INVALID;
/// Stable error code for invalid common provider settings.
pub use self::error::ERROR_CODE_PROVIDER_SETTINGS_INVALID;
/// Maximum safe provenance length attached to one normalized observation.
pub use self::observation::PRICE_PROVENANCE_MAX_BYTES;
/// Safe bounded provenance supplied by one provider adapter.
pub use self::observation::PriceProvenance;
/// Millisecond UTC timestamp used for request, receipt, provider and cooldown projections.
pub use self::observation::PriceTimestamp;
/// Public V1 SOL/USD observation normalized by Off-chain Transport.
pub use self::observation::SolUsdPriceObservation;
/// Maximum provider display-name length accepted by descriptors.
pub use self::provider::PRICE_PROVIDER_DISPLAY_NAME_MAX_BYTES;
/// Maximum opaque provider identifier length accepted by the public contract.
pub use self::provider::PRICE_PROVIDER_ID_MAX_BYTES;
/// Only price pair exposed by the `0.2.11` V1 public contract.
pub use self::provider::PricePair;
/// Generic authentication capability exposed by a configured provider descriptor.
pub use self::provider::PriceProviderAuthMode;
/// Generic runtime availability state exposed without provider-specific error parsing.
pub use self::provider::PriceProviderAvailability;
/// Provider capability and presentation descriptor consumed by provider-agnostic callers.
pub use self::provider::PriceProviderDescriptor;
/// Opaque validated provider identifier owned by Off-chain Transport.
pub use self::provider::PriceProviderId;
/// Long-term provider quota descriptor that is informational rather than an authoritative local counter.
pub use self::provider::PriceProviderLongTermQuota;
/// Period used by a documented long-term provider quota.
pub use self::provider::PriceProviderQuotaPeriod;
/// Unit used by a documented long-term provider quota.
pub use self::provider::PriceProviderQuotaUnit;
/// Generic provider request-limit capability.
pub use self::provider::PriceProviderRateLimit;
/// Shape of one generic provider request-limit capability.
pub use self::provider::PriceProviderRateLimitKind;
/// Scope to which a provider documents one request limit.
pub use self::provider::PriceProviderRateLimitScope;
/// Current provider-neutral runtime state projection.
pub use self::provider::PriceProviderState;
/// Price semantics retained so consumers never assume all providers report equivalent market values.
pub use self::provider::PriceSemantics;
/// Common provider settings shared by provider-specific runtime settings.
pub use self::settings::PriceProviderCommonSettings;

View File

@@ -0,0 +1,169 @@
// file: crates/ksp-offchain-transport-lib/src/observation.rs
// version: 1
/// Maximum UTF-8 byte length accepted for safe provider provenance.
pub const 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 {
unix_millis: u64,
}
impl PriceTimestamp {
/// Creates a UTC timestamp from whole milliseconds since Unix epoch.
#[must_use]
pub const fn from_unix_millis(unix_millis: u64) -> Self {
return Self { unix_millis };
}
/// Returns whole milliseconds since Unix epoch.
#[must_use]
pub const fn unix_millis(&self) -> u64 {
return self.unix_millis;
}
}
/// 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);
impl PriceProvenance {
/// 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();
if !valid_provenance(value.as_str()) {
return std::result::Result::Err(observation_error());
}
return std::result::Result::Ok(Self(value));
}
/// Returns the safe provider provenance.
#[must_use]
pub fn as_str(&self) -> &str {
return self.0.as_str();
}
}
impl std::convert::TryFrom<std::string::String> for PriceProvenance {
type Error = ksp_core_lib::Error;
fn try_from(value: std::string::String) -> std::result::Result<Self, Self::Error> {
return crate::PriceProvenance::new(value);
}
}
impl std::convert::From<PriceProvenance> for std::string::String {
fn from(value: PriceProvenance) -> 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,
}
impl SolUsdPriceObservation {
/// 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,
) -> 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,
price,
provider_id,
provider_timestamp,
provenance,
received_at,
request_started_at,
semantics,
});
}
/// Returns the only V1 pair represented by this observation.
#[must_use]
pub const fn pair(&self) -> crate::PricePair {
return self.pair;
}
/// Returns the exact positive SOL/USD price.
#[must_use]
pub const fn price(&self) -> crate::PriceDecimal {
return self.price;
}
/// Returns the opaque provider identifier.
#[must_use]
pub const fn provider_id(&self) -> &crate::PriceProviderId {
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> {
return self.provider_timestamp;
}
/// Returns safe provider provenance.
#[must_use]
pub const fn provenance(&self) -> &crate::PriceProvenance {
return &self.provenance;
}
/// Returns the KSP wall-clock receipt timestamp.
#[must_use]
pub const fn received_at(&self) -> crate::PriceTimestamp {
return self.received_at;
}
/// Returns the KSP wall-clock request-start timestamp.
#[must_use]
pub const fn request_started_at(&self) -> crate::PriceTimestamp {
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 {
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");
}
fn valid_provenance(value: &str) -> bool {
if value.is_empty() || value.len() > crate::PRICE_PROVENANCE_MAX_BYTES || value.trim() != value {
return false;
}
for character in value.chars() {
if character.is_control() {
return false;
}
}
return true;
}
#[cfg(test)]
#[path = "../unit_tests/observation.rs"]
mod tests;

View File

@@ -0,0 +1,401 @@
// file: crates/ksp-offchain-transport-lib/src/provider.rs
// version: 1
/// Maximum UTF-8 byte length of one provider display name.
pub const 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;
/// 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 {
/// Native SOL quoted directly in US dollars according to one provider's documented semantics.
SolUsd,
}
impl PricePair {
/// Returns the stable human-readable pair code.
#[must_use]
pub const fn code(&self) -> &'static str {
return match self {
Self::SolUsd => "SOL/USD",
};
}
}
/// 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 {
/// Aggregated market price produced by a multi-market data provider.
AggregatedMarket,
/// USD price associated with one explicitly configured DEX pair.
DexPairUsd,
/// Last-trade price reported by one centralized exchange market.
ExchangeLastTrade,
/// Heuristic USD price derived from Solana swap/liquidity activity.
SolanaHeuristic,
/// Direct Solana-oriented spot price supplied by an on-chain market data provider.
SolanaSpot,
}
/// Generic authentication capability exposed by one configured provider.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceProviderAuthMode {
/// No credential is required for the configured access mode.
None,
/// Provider accepts an API key but also supports an unauthenticated mode selected by configuration.
OptionalApiKey,
/// An API key is required for the configured access mode.
RequiredApiKey,
}
/// Scope to which a provider documents a request limit.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceProviderRateLimitScope {
/// Limit is associated with the configured account or API key.
Account,
/// Limit is associated with the source IP address.
Ip,
/// Limit is associated with an organization or project wider than one key.
Organization,
/// Provider documentation does not expose a stronger stable scope.
Unspecified,
}
/// Shape of one generic provider request-limit capability.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceProviderRateLimitKind {
/// Dynamic or server-driven limit that cannot be represented as one safe fixed local cadence.
Dynamic,
/// Locally enforceable fixed request budget over a documented window.
Fixed,
}
/// Generic provider request-limit capability with validated fixed-limit values.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Serialize)]
pub struct PriceProviderRateLimit {
burst: std::option::Option<u32>,
kind: crate::PriceProviderRateLimitKind,
requests: std::option::Option<u32>,
scope: crate::PriceProviderRateLimitScope,
window_seconds: std::option::Option<u32>,
}
impl PriceProviderRateLimit {
/// 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> {
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,
requests: std::option::Option::Some(requests),
scope,
window_seconds: std::option::Option::Some(window_seconds),
});
}
/// Creates a dynamic/server-driven request-limit descriptor.
#[must_use]
pub const fn dynamic(scope: crate::PriceProviderRateLimitScope) -> Self {
return Self {
burst: std::option::Option::None,
kind: crate::PriceProviderRateLimitKind::Dynamic,
requests: std::option::Option::None,
scope,
window_seconds: std::option::Option::None,
};
}
/// Returns optional documented burst capacity.
#[must_use]
pub const fn burst(&self) -> std::option::Option<u32> {
return self.burst;
}
/// Returns whether the limit is fixed or dynamic/server-driven.
#[must_use]
pub const fn kind(&self) -> crate::PriceProviderRateLimitKind {
return self.kind;
}
/// Returns the request budget for a fixed limit, or `None` for a dynamic limit.
#[must_use]
pub const fn requests(&self) -> std::option::Option<u32> {
return self.requests;
}
/// Returns the documented limit scope.
#[must_use]
pub const fn scope(&self) -> crate::PriceProviderRateLimitScope {
return self.scope;
}
/// Returns the fixed window duration in seconds, or `None` for a dynamic limit.
#[must_use]
pub const fn window_seconds(&self) -> std::option::Option<u32> {
return self.window_seconds;
}
}
/// Period used by one documented long-term provider quota.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceProviderQuotaPeriod {
/// Quota resets on a provider-defined daily period.
Day,
/// Quota resets on a provider-defined monthly period.
Month,
}
/// Unit used by one documented long-term provider quota.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PriceProviderQuotaUnit {
/// Provider-specific credits, not assumed to equal HTTP requests.
Credits,
/// HTTP/API requests.
Requests,
}
/// Long-term provider quota descriptor exposed as non-authoritative capability metadata.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Serialize)]
pub struct PriceProviderLongTermQuota {
amount: u64,
period: crate::PriceProviderQuotaPeriod,
unit: crate::PriceProviderQuotaUnit,
}
impl PriceProviderLongTermQuota {
/// Creates a non-zero documented quota descriptor.
pub fn new(amount: u64, period: crate::PriceProviderQuotaPeriod, unit: crate::PriceProviderQuotaUnit) -> ksp_core_lib::Result<Self> {
if amount == 0 {
return std::result::Result::Err(provider_descriptor_error());
}
return std::result::Result::Ok(Self { amount, period, unit });
}
/// Returns the documented amount without treating it as a local remaining counter.
#[must_use]
pub const fn amount(&self) -> u64 {
return self.amount;
}
/// Returns the provider-defined quota period.
#[must_use]
pub const fn period(&self) -> crate::PriceProviderQuotaPeriod {
return self.period;
}
/// Returns the documented quota unit.
#[must_use]
pub const fn unit(&self) -> crate::PriceProviderQuotaUnit {
return self.unit;
}
}
/// Opaque validated provider identifier owned by Off-chain Transport.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, serde::Deserialize, serde::Serialize)]
#[serde(try_from = "std::string::String", into = "std::string::String")]
pub struct PriceProviderId(std::string::String);
impl PriceProviderId {
/// Creates one bounded stable provider identifier.
pub fn new(value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
let value = value.into();
if !valid_provider_id(value.as_str()) {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_PROVIDER_ID_INVALID, "invalid off-chain provider identifier"));
}
return std::result::Result::Ok(Self(value));
}
/// Returns the opaque identifier as a stable string.
#[must_use]
pub fn as_str(&self) -> &str {
return self.0.as_str();
}
}
impl std::fmt::Display for PriceProviderId {
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 {
type Error = ksp_core_lib::Error;
fn try_from(value: std::string::String) -> std::result::Result<Self, Self::Error> {
return crate::PriceProviderId::new(value);
}
}
impl std::convert::From<PriceProviderId> for std::string::String {
fn from(value: PriceProviderId) -> 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,
display_name: std::string::String,
id: crate::PriceProviderId,
long_term_quota: std::option::Option<crate::PriceProviderLongTermQuota>,
rate_limit: crate::PriceProviderRateLimit,
semantics: crate::PriceSemantics,
supports_sol_usd: bool,
}
impl PriceProviderDescriptor {
/// Creates a validated provider-neutral descriptor.
pub fn new(
id: crate::PriceProviderId,
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>,
supports_sol_usd: bool,
) -> ksp_core_lib::Result<Self> {
let display_name = display_name.into();
if !valid_display_name(display_name.as_str()) {
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 });
}
/// Returns the configured authentication capability.
#[must_use]
pub const fn auth_mode(&self) -> crate::PriceProviderAuthMode {
return self.auth_mode;
}
/// Returns the safe display name.
#[must_use]
pub fn display_name(&self) -> &str {
return self.display_name.as_str();
}
/// Returns the opaque provider identifier.
#[must_use]
pub const fn id(&self) -> &crate::PriceProviderId {
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> {
return self.long_term_quota;
}
/// Returns the configured request-limit capability.
#[must_use]
pub const fn rate_limit(&self) -> crate::PriceProviderRateLimit {
return self.rate_limit;
}
/// Returns the documented price semantics.
#[must_use]
pub const fn semantics(&self) -> crate::PriceSemantics {
return self.semantics;
}
/// Reports whether this descriptor can serve the V1 SOL/USD pair.
#[must_use]
pub const fn supports_sol_usd(&self) -> bool {
return self.supports_sol_usd;
}
}
/// Generic runtime availability state exposed without provider-specific error parsing.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum PriceProviderAvailability {
/// 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,
},
/// Provider is disabled by runtime configuration.
Disabled,
/// Runtime settings do not satisfy the provider adapter contract.
Misconfigured,
/// Provider-reported quota prevents current use.
QuotaUnavailable,
/// Provider is eligible for a new request.
Ready,
/// Transport/provider failure is transient; retry time is present only when actually known.
TemporarilyUnavailable {
/// Optional next retry timestamp derived from safe runtime/provider information.
retry_at: std::option::Option<crate::PriceTimestamp>,
},
}
/// Current provider-neutral runtime state projection.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
pub struct PriceProviderState {
availability: crate::PriceProviderAvailability,
provider_id: crate::PriceProviderId,
}
impl PriceProviderState {
/// Creates one generic state projection for a configured provider.
#[must_use]
pub fn new(provider_id: crate::PriceProviderId, availability: crate::PriceProviderAvailability) -> Self {
return Self { availability, provider_id };
}
/// Returns the generic availability classification.
#[must_use]
pub const fn availability(&self) -> crate::PriceProviderAvailability {
return self.availability;
}
/// Returns the opaque provider identifier.
#[must_use]
pub const fn provider_id(&self) -> &crate::PriceProviderId {
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 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 {
return false;
}
for character in value.chars() {
if character.is_control() {
return false;
}
}
return true;
}
fn valid_provider_id(value: &str) -> bool {
if value.is_empty() || value.len() > crate::PRICE_PROVIDER_ID_MAX_BYTES {
return false;
}
for byte in value.bytes() {
if !(byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-' || byte == b'_') {
return false;
}
}
return true;
}
#[cfg(test)]
#[path = "../unit_tests/provider.rs"]
mod tests;

View File

@@ -0,0 +1,36 @@
// file: crates/ksp-offchain-transport-lib/src/settings.rs
// version: 1
/// 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 {
enabled: bool,
provider_id: crate::PriceProviderId,
}
impl PriceProviderCommonSettings {
/// Creates common settings for one uniquely identified runtime provider instance.
#[must_use]
pub fn new(provider_id: crate::PriceProviderId, enabled: bool) -> Self {
return Self { enabled, provider_id };
}
/// Reports whether this provider instance is enabled.
#[must_use]
pub const fn enabled(&self) -> bool {
return self.enabled;
}
/// Returns the opaque runtime provider identifier.
#[must_use]
pub const fn provider_id(&self) -> &crate::PriceProviderId {
return &self.provider_id;
}
}
#[cfg(test)]
#[path = "../unit_tests/settings.rs"]
mod tests;