v0.2.11-pre.005
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
179
crates/ksp-offchain-transport-lib/src/market_price_kraken.rs
Normal file
179
crates/ksp-offchain-transport-lib/src/market_price_kraken.rs
Normal 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;
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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(());
|
||||
}
|
||||
@@ -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(());
|
||||
}
|
||||
Reference in New Issue
Block a user