v0.2.11-pre.005

This commit is contained in:
2026-08-25 22:32:07 +02:00
parent 5c97a772be
commit 4021d08541
11 changed files with 746 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/src/lib.rs
// version: 6
// version: 7
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -7,10 +7,10 @@
//! KSP-owned off-chain transport foundation.
//!
//! `0.2.11-pre.004` keeps the first capability family deliberately narrow (`market_price`, SOL/USD only), activates the crate-wide hardened `http_*`
//! primitives in production, and introduces the first three direct-REST adapters: CoinGecko, CoinMarketCap and CoinPaprika. Provider wire DTOs remain
//! private, no provider SDK is used, and the `http_*` internals are not re-exported as a generic consumer HTTP client. Config integration and generic
//! registry/refresh orchestration remain outside this tranche.
//! `0.2.11-pre.005` keeps the first capability family deliberately narrow (`market_price`, SOL/USD only) and extends the direct-REST surface with
//! Kraken Spot and Coinbase Exchange. Exchange adapters preserve `ExchangeLastTrade` semantics instead of pretending to be aggregated market prices.
//! Provider wire DTOs remain private, no provider SDK is used, and the crate-wide hardened `http_*` internals remain unavailable as a generic consumer
//! HTTP client. Config integration and generic registry/refresh orchestration remain outside this tranche.
mod constants;
mod error;
@@ -19,10 +19,12 @@ mod http_client;
mod http_settings;
mod market_price_adapter;
mod market_price_api_key;
mod market_price_coinbase_exchange;
mod market_price_coingecko;
mod market_price_coinmarketcap;
mod market_price_coinpaprika;
mod market_price_decimal;
mod market_price_kraken;
mod market_price_observation;
mod market_price_provider;
mod market_price_settings;
@@ -67,6 +69,10 @@ 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;
/// Coinbase Exchange SOL/USD provider adapter.
pub use self::market_price_coinbase_exchange::MarketPriceCoinbaseExchangeProvider;
/// Coinbase Exchange runtime settings.
pub use self::market_price_coinbase_exchange::MarketPriceCoinbaseExchangeSettings;
/// CoinGecko V1 access mode.
pub use self::market_price_coingecko::MarketPriceCoinGeckoAccessMode;
/// CoinGecko SOL/USD provider adapter.
@@ -89,6 +95,10 @@ pub use self::market_price_decimal::MARKET_PRICE_DECIMAL_MAX_INPUT_BYTES;
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;
/// Kraken Spot SOL/USD provider adapter.
pub use self::market_price_kraken::MarketPriceKrakenProvider;
/// Kraken Spot runtime settings.
pub use self::market_price_kraken::MarketPriceKrakenSettings;
/// Maximum safe provenance length attached to one normalized observation.
pub use self::market_price_observation::MARKET_PRICE_PROVENANCE_MAX_BYTES;
/// Public V1 SOL/USD observation normalized by Off-chain Transport.

View File

@@ -0,0 +1,159 @@
// file: crates/ksp-offchain-transport-lib/src/market_price_coinbase_exchange.rs
// version: 1
//! Coinbase Exchange SOL/USD market-price adapter using the public product ticker directly through `reqwest`.
const COINBASE_EXCHANGE_PROVIDER_ID: &str = "coinbase_exchange";
const COINBASE_EXCHANGE_SOL_USD_TICKER_URL: &str = "https://api.exchange.coinbase.com/products/SOL-USD/ticker";
/// Runtime settings for the keyless Coinbase Exchange market-price adapter.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MarketPriceCoinbaseExchangeSettings {
common: crate::MarketPriceProviderCommonSettings,
}
impl crate::MarketPriceCoinbaseExchangeSettings {
/// Creates Coinbase Exchange settings for the public keyless REST surface.
pub fn new(enabled: bool) -> ksp_core_lib::Result<Self> {
let provider_id = match crate::MarketPriceProviderId::new(COINBASE_EXCHANGE_PROVIDER_ID) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(Self { common: crate::MarketPriceProviderCommonSettings::new(provider_id, enabled) });
}
/// Returns common provider identity and enablement settings.
#[must_use]
pub const fn common(&self) -> &crate::MarketPriceProviderCommonSettings {
return &self.common;
}
}
/// Coinbase Exchange SOL/USD provider adapter.
pub struct MarketPriceCoinbaseExchangeProvider {
admission: crate::HttpAdmissionController,
descriptor: crate::MarketPriceProviderDescriptor,
http: crate::HttpRestClient,
settings: crate::MarketPriceCoinbaseExchangeSettings,
}
impl crate::MarketPriceCoinbaseExchangeProvider {
/// Builds one Coinbase Exchange provider from validated runtime settings.
pub fn new(settings: crate::MarketPriceCoinbaseExchangeSettings) -> 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 Coinbase Exchange capability descriptor.
#[must_use]
pub const fn descriptor(&self) -> &crate::MarketPriceProviderDescriptor {
return &self.descriptor;
}
/// Returns the validated Coinbase Exchange runtime settings.
#[must_use]
pub const fn settings(&self) -> &crate::MarketPriceCoinbaseExchangeSettings {
return &self.settings;
}
/// Fetches one normalized SOL/USD last-trade observation from Coinbase Exchange.
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(COINBASE_EXCHANGE_PROVIDER_ID));
}
if let std::result::Result::Err(error) = crate::admit_request(COINBASE_EXCHANGE_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() {
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, COINBASE_EXCHANGE_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() -> ksp_core_lib::Result<crate::HttpGetRequest> {
return crate::HttpGetRequest::new_https(COINBASE_EXCHANGE_SOL_USD_TICKER_URL);
}
fn descriptor_for(provider_id: crate::MarketPriceProviderId) -> ksp_core_lib::Result<crate::MarketPriceProviderDescriptor> {
let rate_limit = match crate::MarketPriceProviderRateLimit::fixed(10, 1, std::option::Option::Some(15), crate::MarketPriceProviderRateLimitScope::Ip) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::MarketPriceProviderDescriptor::new(
provider_id,
"Coinbase Exchange",
crate::MarketPriceSemantics::ExchangeLastTrade,
crate::MarketPriceProviderAuthMode::None,
rate_limit,
std::option::Option::None,
true,
);
}
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::<CoinbaseExchangeWireTicker>(bytes) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(crate::invalid_provider_response_with_source(COINBASE_EXCHANGE_PROVIDER_ID, "response", error));
},
};
let price = match crate::MarketPriceDecimal::parse_json_raw(wire.price.as_ref()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provider_timestamp = match crate::market_price_timestamp_from_rfc3339(wire.time.as_str()) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(crate::invalid_provider_response(COINBASE_EXCHANGE_PROVIDER_ID, "time"));
},
};
let provenance = match crate::MarketPriceProvenance::new("coinbase_exchange:SOL-USD:last_trade") {
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::ExchangeLastTrade,
request_started_at,
received_at,
std::option::Option::Some(provider_timestamp),
provenance,
);
}
#[derive(serde::Deserialize)]
struct CoinbaseExchangeWireTicker {
price: std::boxed::Box<serde_json::value::RawValue>,
time: std::string::String,
}
#[cfg(test)]
#[path = "../unit_tests/market_price_coinbase_exchange.rs"]
mod tests;

View File

@@ -0,0 +1,179 @@
// file: crates/ksp-offchain-transport-lib/src/market_price_kraken.rs
// version: 1
//! Kraken Spot SOL/USD market-price adapter using the public REST ticker directly through `reqwest`.
const KRAKEN_PROVIDER_ID: &str = "kraken";
const KRAKEN_SOL_USD_PAIR: &str = "SOLUSD";
const KRAKEN_TICKER_URL: &str = "https://api.kraken.com/0/public/Ticker";
/// Runtime settings for the keyless Kraken Spot market-price adapter.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MarketPriceKrakenSettings {
common: crate::MarketPriceProviderCommonSettings,
}
impl crate::MarketPriceKrakenSettings {
/// Creates Kraken Spot settings for the public keyless REST surface.
pub fn new(enabled: bool) -> ksp_core_lib::Result<Self> {
let provider_id = match crate::MarketPriceProviderId::new(KRAKEN_PROVIDER_ID) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(Self { common: crate::MarketPriceProviderCommonSettings::new(provider_id, enabled) });
}
/// Returns common provider identity and enablement settings.
#[must_use]
pub const fn common(&self) -> &crate::MarketPriceProviderCommonSettings {
return &self.common;
}
}
/// Kraken Spot SOL/USD provider adapter.
pub struct MarketPriceKrakenProvider {
admission: crate::HttpAdmissionController,
descriptor: crate::MarketPriceProviderDescriptor,
http: crate::HttpRestClient,
settings: crate::MarketPriceKrakenSettings,
}
impl crate::MarketPriceKrakenProvider {
/// Builds one Kraken Spot provider from validated runtime settings.
pub fn new(settings: crate::MarketPriceKrakenSettings) -> 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 Kraken Spot capability descriptor.
#[must_use]
pub const fn descriptor(&self) -> &crate::MarketPriceProviderDescriptor {
return &self.descriptor;
}
/// Returns the validated Kraken Spot runtime settings.
#[must_use]
pub const fn settings(&self) -> &crate::MarketPriceKrakenSettings {
return &self.settings;
}
/// Fetches one normalized SOL/USD last-trade observation from Kraken Spot.
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(KRAKEN_PROVIDER_ID));
}
if let std::result::Result::Err(error) = crate::admit_request(KRAKEN_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() {
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, KRAKEN_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() -> ksp_core_lib::Result<crate::HttpGetRequest> {
let mut request = match crate::HttpGetRequest::new_https(KRAKEN_TICKER_URL) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
request.append_query_pair("pair", KRAKEN_SOL_USD_PAIR);
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::Ip) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::MarketPriceProviderDescriptor::new(
provider_id,
"Kraken",
crate::MarketPriceSemantics::ExchangeLastTrade,
crate::MarketPriceProviderAuthMode::None,
rate_limit,
std::option::Option::None,
true,
);
}
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::<KrakenWireResponse>(bytes) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(crate::invalid_provider_response_with_source(KRAKEN_PROVIDER_ID, "response", error));
},
};
if !wire.error.is_empty() {
return std::result::Result::Err(crate::invalid_provider_response(KRAKEN_PROVIDER_ID, "error"));
}
if wire.result.len() != 1 {
return std::result::Result::Err(crate::invalid_provider_response(KRAKEN_PROVIDER_ID, "result"));
}
let ticker = match wire.result.get(KRAKEN_SOL_USD_PAIR) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(KRAKEN_PROVIDER_ID, "identity")),
};
let last_trade = match ticker.last_trade.first() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(crate::invalid_provider_response(KRAKEN_PROVIDER_ID, "last_trade")),
};
let price = match crate::MarketPriceDecimal::parse_json_raw(last_trade.as_ref()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provenance = match crate::MarketPriceProvenance::new("kraken:SOLUSD:last_trade") {
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::ExchangeLastTrade,
request_started_at,
received_at,
std::option::Option::None,
provenance,
);
}
#[derive(serde::Deserialize)]
struct KrakenWireResponse {
error: std::vec::Vec<std::string::String>,
result: std::collections::BTreeMap<std::string::String, KrakenWireTicker>,
}
#[derive(serde::Deserialize)]
struct KrakenWireTicker {
#[serde(rename = "c")]
last_trade: std::vec::Vec<std::boxed::Box<serde_json::value::RawValue>>,
}
#[cfg(test)]
#[path = "../unit_tests/market_price_kraken.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/tests/dependency_boundary.rs
// version: 5
// version: 6
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -43,19 +43,28 @@ fn pre_004_http_runtime_is_production_active_but_not_a_public_generic_client() {
}
#[test]
fn pre_004_provider_modules_are_market_price_scoped_and_fixed_origin() {
fn pre_005_provider_modules_are_market_price_scoped_and_fixed_origin() {
let crate_root = include_str!("../src/lib.rs");
assert!(crate_root.contains("mod market_price_coinbase_exchange;"));
assert!(crate_root.contains("mod market_price_coingecko;"));
assert!(crate_root.contains("mod market_price_coinmarketcap;"));
assert!(crate_root.contains("mod market_price_coinpaprika;"));
assert!(crate_root.contains("mod market_price_kraken;"));
let coinbase = include_str!("../src/market_price_coinbase_exchange.rs");
let coingecko = include_str!("../src/market_price_coingecko.rs");
let coinmarketcap = include_str!("../src/market_price_coinmarketcap.rs");
let coinpaprika = include_str!("../src/market_price_coinpaprika.rs");
let kraken = include_str!("../src/market_price_kraken.rs");
assert!(coinbase.contains("https://api.exchange.coinbase.com/products/SOL-USD/ticker"));
assert!(coingecko.contains("https://api.coingecko.com/api/v3/simple/price"));
assert!(coinmarketcap.contains("https://pro-api.coinmarketcap.com/public-api/v2/simple/price"));
assert!(coinmarketcap.contains("https://pro-api.coinmarketcap.com/v2/simple/price"));
assert!(coinpaprika.contains("https://api.coinpaprika.com/v1/tickers/sol-solana"));
assert!(kraken.contains("https://api.kraken.com/0/public/Ticker"));
assert!(kraken.contains("SOLUSD"));
assert!(!coinbase.contains("std::env"));
assert!(!coingecko.contains("std::env"));
assert!(!coinmarketcap.contains("std::env"));
assert!(!coinpaprika.contains("std::env"));
assert!(!kraken.contains("std::env"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/tests/public_api.rs
// version: 5
// version: 6
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -97,6 +97,31 @@ fn public_pre_004_aggregator_settings_and_adapters_are_available_from_crate_root
return std::result::Result::Ok(());
}
#[test]
fn public_pre_005_exchange_settings_and_adapters_are_available_from_crate_root() -> ksp_core_lib::Result<()> {
let kraken = match ksp_offchain_transport_lib::MarketPriceKrakenSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let kraken_provider = match ksp_offchain_transport_lib::MarketPriceKrakenProvider::new(kraken) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(kraken_provider.descriptor().id().as_str(), "kraken");
assert_eq!(kraken_provider.descriptor().semantics(), ksp_offchain_transport_lib::MarketPriceSemantics::ExchangeLastTrade);
let coinbase = match ksp_offchain_transport_lib::MarketPriceCoinbaseExchangeSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinbase_provider = match ksp_offchain_transport_lib::MarketPriceCoinbaseExchangeProvider::new(coinbase) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(coinbase_provider.descriptor().id().as_str(), "coinbase_exchange");
assert_eq!(coinbase_provider.descriptor().semantics(), ksp_offchain_transport_lib::MarketPriceSemantics::ExchangeLastTrade);
return std::result::Result::Ok(());
}
#[test]
fn offchain_error_codes_use_owned_domain() {
let codes = [

View File

@@ -0,0 +1,59 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_coinbase_exchange.rs
// version: 1
#[test]
fn coinbase_exchange_fixture_maps_exact_last_trade_and_provider_timestamp() -> ksp_core_lib::Result<()> {
let provider_id = match crate::MarketPriceProviderId::new(super::COINBASE_EXCHANGE_PROVIDER_ID) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture = concat!(
r#"{"trade_id":86326522,"price":"151.987654321012345678","size":"0.25","#,
r#""time":"2026-08-25T19:00:01.123Z","bid":"151.98","ask":"151.99","volume":"12345.67"}"#
)
.as_bytes();
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::ExchangeLastTrade);
assert_eq!(observation.provider_timestamp().map(|value| return value.unix_millis()), std::option::Option::Some(1_787_684_401_123));
assert_eq!(observation.provenance().as_str(), "coinbase_exchange:SOL-USD:last_trade");
return std::result::Result::Ok(());
}
#[test]
fn coinbase_exchange_descriptor_matches_public_token_bucket() -> ksp_core_lib::Result<()> {
let settings = match crate::MarketPriceCoinbaseExchangeSettings::new(true) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provider = match crate::MarketPriceCoinbaseExchangeProvider::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(), super::COINBASE_EXCHANGE_PROVIDER_ID);
assert_eq!(provider.descriptor().auth_mode(), crate::MarketPriceProviderAuthMode::None);
assert_eq!(provider.descriptor().semantics(), crate::MarketPriceSemantics::ExchangeLastTrade);
assert_eq!(provider.descriptor().rate_limit().requests(), std::option::Option::Some(10));
assert_eq!(provider.descriptor().rate_limit().window_seconds(), std::option::Option::Some(1));
assert_eq!(provider.descriptor().rate_limit().burst(), std::option::Option::Some(15));
assert_eq!(provider.descriptor().rate_limit().scope(), crate::MarketPriceProviderRateLimitScope::Ip);
assert_eq!(provider.descriptor().long_term_quota(), std::option::Option::None);
return std::result::Result::Ok(());
}
#[test]
fn coinbase_exchange_rejects_invalid_timestamp() -> ksp_core_lib::Result<()> {
let provider_id = match crate::MarketPriceProviderId::new(super::COINBASE_EXCHANGE_PROVIDER_ID) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture = br#"{"price":"151.98","time":"not-a-timestamp"}"#;
let timestamp = crate::MarketPriceTimestamp::from_unix_millis(10);
assert!(super::parse_response(fixture, provider_id, timestamp, timestamp).is_err());
return std::result::Result::Ok(());
}

View File

@@ -0,0 +1,57 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_kraken.rs
// version: 1
#[test]
fn kraken_fixture_maps_exact_last_trade_without_inventing_provider_timestamp() -> ksp_core_lib::Result<()> {
let provider_id = match crate::MarketPriceProviderId::new(super::KRAKEN_PROVIDER_ID) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture = br#"{"error":[],"result":{"SOLUSD":{"a":["152.10","1","1.0"],"b":["152.00","1","1.0"],"c":["152.012345678901234567","0.25"]}}}"#;
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(), "152.012345678901234567");
assert_eq!(observation.semantics(), crate::MarketPriceSemantics::ExchangeLastTrade);
assert_eq!(observation.provider_timestamp(), std::option::Option::None);
assert_eq!(observation.provenance().as_str(), "kraken:SOLUSD:last_trade");
return std::result::Result::Ok(());
}
#[test]
fn kraken_descriptor_is_keyless_exchange_last_trade_with_conservative_public_cadence() -> ksp_core_lib::Result<()> {
let settings = match crate::MarketPriceKrakenSettings::new(true) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provider = match crate::MarketPriceKrakenProvider::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(), super::KRAKEN_PROVIDER_ID);
assert_eq!(provider.descriptor().auth_mode(), crate::MarketPriceProviderAuthMode::None);
assert_eq!(provider.descriptor().semantics(), crate::MarketPriceSemantics::ExchangeLastTrade);
assert_eq!(provider.descriptor().rate_limit().requests(), std::option::Option::Some(1));
assert_eq!(provider.descriptor().rate_limit().window_seconds(), std::option::Option::Some(1));
assert_eq!(provider.descriptor().rate_limit().burst(), std::option::Option::None);
assert_eq!(provider.descriptor().rate_limit().scope(), crate::MarketPriceProviderRateLimitScope::Ip);
assert_eq!(provider.descriptor().long_term_quota(), std::option::Option::None);
return std::result::Result::Ok(());
}
#[test]
fn kraken_rejects_provider_error_and_wrong_result_identity() -> ksp_core_lib::Result<()> {
let provider_id = match crate::MarketPriceProviderId::new(super::KRAKEN_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 provider_error = br#"{"error":["EGeneral:Temporary lockout"],"result":{}}"#;
assert!(super::parse_response(provider_error, provider_id.clone(), timestamp, timestamp).is_err());
let wrong_identity = br#"{"error":[],"result":{"SOLEUR":{"c":["152.01","0.25"]}}}"#;
assert!(super::parse_response(wrong_identity, provider_id, timestamp, timestamp).is_err());
return std::result::Result::Ok(());
}