v0.2.11-pre.007

This commit is contained in:
2026-08-26 10:06:21 +02:00
parent 98093d859b
commit 5aeff5ca14
14 changed files with 1142 additions and 45 deletions

View File

@@ -0,0 +1,85 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_birdeye.rs
// version: 1
#[test]
fn birdeye_fixture_maps_exact_spot_price_and_provider_update_time() -> ksp_core_lib::Result<()> {
let provider_id = match crate::MarketPriceProviderId::new(super::BIRDEYE_PROVIDER_ID) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fixture = br#"{"data":{"value":151.987654321012345678,"updateUnixTime":1778248899,"updateHumanTime":"2026-05-08T19:01:39"},"success":true}"#;
let started = crate::MarketPriceTimestamp::from_unix_millis(10);
let received = crate::MarketPriceTimestamp::from_unix_millis(20);
let observation = match super::parse_response(fixture, provider_id, started, received) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(observation.price().to_canonical_string(), "151.987654321012345678");
assert_eq!(observation.semantics(), crate::MarketPriceSemantics::SolanaSpot);
assert_eq!(observation.provider_timestamp().map(|value| return value.unix_millis()), std::option::Option::Some(1_778_248_899_000));
assert_eq!(observation.provenance().as_str(), "birdeye:solana:wsol:value");
return std::result::Result::Ok(());
}
#[test]
fn birdeye_standard_descriptor_models_account_rate_compute_quota_and_request_cost() -> ksp_core_lib::Result<()> {
let settings = match crate::MarketPriceBirdeyeSettings::new(true, std::option::Option::Some("birdeye-standard-canary-secret".to_owned())) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(!std::format!("{settings:?}").contains("birdeye-standard-canary-secret"));
let request = match super::build_request(&settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(request.has_header_for_test(super::BIRDEYE_API_KEY_HEADER));
assert!(request.has_header_for_test(super::BIRDEYE_CHAIN_HEADER));
assert_eq!(request.url_for_test().query(), std::option::Option::Some("address=So11111111111111111111111111111111111111112"));
let provider = match crate::MarketPriceBirdeyeProvider::new(settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let descriptor = provider.descriptor();
assert_eq!(descriptor.auth_mode(), crate::MarketPriceProviderAuthMode::RequiredApiKey);
assert_eq!(descriptor.semantics(), crate::MarketPriceSemantics::SolanaSpot);
assert_eq!(descriptor.rate_limit().requests(), std::option::Option::Some(1));
assert_eq!(descriptor.rate_limit().window_seconds(), std::option::Option::Some(1));
assert_eq!(descriptor.rate_limit().scope(), crate::MarketPriceProviderRateLimitScope::Account);
let quota = match descriptor.long_term_quota() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("quota")),
};
assert_eq!(quota.amount(), 30_000);
assert_eq!(quota.period(), crate::MarketPriceProviderQuotaPeriod::Month);
assert_eq!(quota.unit(), crate::MarketPriceProviderQuotaUnit::ComputeUnits);
let request_cost = match descriptor.sol_usd_request_cost() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("request_cost")),
};
assert_eq!(request_cost.amount(), 3);
assert_eq!(request_cost.unit(), crate::MarketPriceProviderQuotaUnit::ComputeUnits);
return std::result::Result::Ok(());
}
#[test]
fn birdeye_rejects_missing_key_unsuccessful_missing_price_and_missing_update_time() -> ksp_core_lib::Result<()> {
assert!(crate::MarketPriceBirdeyeSettings::new(true, std::option::Option::None).is_err());
assert!(crate::MarketPriceBirdeyeSettings::new(false, std::option::Option::None).is_ok());
let provider_id = match crate::MarketPriceProviderId::new(super::BIRDEYE_PROVIDER_ID) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let timestamp = crate::MarketPriceTimestamp::from_unix_millis(10);
let unsuccessful = br#"{"data":{"value":151.9,"updateUnixTime":1778248899},"success":false}"#;
assert!(super::parse_response(unsuccessful, provider_id.clone(), timestamp, timestamp).is_err());
let missing_price = br#"{"data":{"value":null,"updateUnixTime":1778248899},"success":true}"#;
assert!(super::parse_response(missing_price, provider_id.clone(), timestamp, timestamp).is_err());
let missing_update_time = br#"{"data":{"value":151.9},"success":true}"#;
assert!(super::parse_response(missing_update_time, provider_id, timestamp, timestamp).is_err());
return std::result::Result::Ok(());
}
fn test_error(field: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_PROVIDER_DESCRIPTOR_INVALID, "Birdeye test expectation failed")
.with_context("field", field);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_provider.rs
// version: 3
// version: 4
#[test]
fn provider_id_is_opaque_bounded_and_stable() -> ksp_core_lib::Result<()> {
@@ -46,6 +46,7 @@ fn provider_descriptor_preserves_semantics_auth_limits_and_informational_quota()
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(descriptor.display_name(), "Provider A");
assert_eq!(descriptor.sol_usd_request_cost(), std::option::Option::None);
assert_eq!(descriptor.semantics(), crate::MarketPriceSemantics::AggregatedMarket);
assert_eq!(descriptor.auth_mode(), crate::MarketPriceProviderAuthMode::OptionalApiKey);
assert_eq!(descriptor.rate_limit(), rate_limit);
@@ -67,6 +68,54 @@ fn provider_limit_descriptors_reject_zero_and_model_dynamic_scope() {
assert_eq!(dynamic.burst(), std::option::Option::None);
}
#[test]
fn provider_request_cost_matches_quota_unit_and_rejects_zero() -> ksp_core_lib::Result<()> {
assert!(crate::MarketPriceProviderRequestCost::new(0, crate::MarketPriceProviderQuotaUnit::ComputeUnits).is_err());
let id = match crate::MarketPriceProviderId::new("provider-cost") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rate_limit = match crate::MarketPriceProviderRateLimit::fixed(1, 1, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Account) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let quota = match crate::MarketPriceProviderLongTermQuota::new(
30_000,
crate::MarketPriceProviderQuotaPeriod::Month,
crate::MarketPriceProviderQuotaUnit::ComputeUnits,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let descriptor = match crate::MarketPriceProviderDescriptor::new(
id,
"Provider Cost",
crate::MarketPriceSemantics::SolanaSpot,
crate::MarketPriceProviderAuthMode::RequiredApiKey,
rate_limit,
std::option::Option::Some(quota),
true,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let request_cost = match crate::MarketPriceProviderRequestCost::new(3, crate::MarketPriceProviderQuotaUnit::ComputeUnits) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let descriptor = match descriptor.with_sol_usd_request_cost(request_cost) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(descriptor.sol_usd_request_cost(), std::option::Option::Some(request_cost));
let wrong_unit = match crate::MarketPriceProviderRequestCost::new(3, crate::MarketPriceProviderQuotaUnit::Credits) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(descriptor.with_sol_usd_request_cost(wrong_unit).is_err());
return std::result::Result::Ok(());
}
#[test]
fn provider_availability_keeps_cooldown_and_outage_distinct() -> ksp_core_lib::Result<()> {
let id = match crate::MarketPriceProviderId::new("jupiter") {
@@ -76,6 +125,8 @@ fn provider_availability_keeps_cooldown_and_outage_distinct() -> ksp_core_lib::R
let retry_at = crate::MarketPriceTimestamp::from_unix_millis(1_777_777_777_000);
let state = crate::MarketPriceProviderState::new(id, crate::MarketPriceProviderAvailability::CoolingDown { retry_at });
assert_eq!(state.availability(), crate::MarketPriceProviderAvailability::CoolingDown { retry_at });
assert!(!state.availability().is_refresh_eligible());
assert_eq!(state.availability().retry_at(), std::option::Option::Some(retry_at));
assert_ne!(state.availability(), crate::MarketPriceProviderAvailability::TemporarilyUnavailable { retry_at: std::option::Option::Some(retry_at) });
return std::result::Result::Ok(());
}

View File

@@ -0,0 +1,157 @@
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_registry.rs
// version: 1
#[test]
fn registry_orders_all_eight_v1_providers_and_exposes_only_generic_entries() -> ksp_core_lib::Result<()> {
let entries = match all_v1_entries() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = match crate::MarketPriceProviderRegistry::new(entries) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(registry.len(), 8);
let ids = registry.entries().iter().map(|entry| return entry.descriptor().id().as_str()).collect::<std::vec::Vec<_>>();
assert_eq!(ids, std::vec!["birdeye", "coinbase_exchange", "coingecko", "coinmarketcap", "coinpaprika", "dexscreener", "jupiter", "kraken",]);
assert!(registry.entries().iter().all(|entry| return entry.state().availability() == crate::MarketPriceProviderAvailability::Disabled));
return std::result::Result::Ok(());
}
#[test]
fn registry_rejects_duplicate_provider_ids_and_updates_generic_availability() -> ksp_core_lib::Result<()> {
let descriptor = match simple_descriptor("provider-a") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let duplicate = std::vec![
crate::MarketPriceProviderRegistryEntry::new(descriptor.clone(), crate::MarketPriceProviderAvailability::Disabled),
crate::MarketPriceProviderRegistryEntry::new(descriptor, crate::MarketPriceProviderAvailability::Ready),
];
assert!(crate::MarketPriceProviderRegistry::new(duplicate).is_err());
let descriptor = match simple_descriptor("provider-b") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let provider_id = descriptor.id().clone();
let retry_at = crate::MarketPriceTimestamp::from_unix_millis(42);
let entry = crate::MarketPriceProviderRegistryEntry::new(descriptor, crate::MarketPriceProviderAvailability::CoolingDown { retry_at });
let registry = match crate::MarketPriceProviderRegistry::new(std::vec![entry]) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let state = match registry.state(&provider_id) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("state")),
};
assert!(!state.availability().is_refresh_eligible());
assert_eq!(state.availability().retry_at(), std::option::Option::Some(retry_at));
assert_eq!(registry.descriptor(&provider_id).map(|value| return value.id()), std::option::Option::Some(&provider_id));
return std::result::Result::Ok(());
}
fn all_v1_entries() -> ksp_core_lib::Result<std::vec::Vec<crate::MarketPriceProviderRegistryEntry>> {
let birdeye_settings = match crate::MarketPriceBirdeyeSettings::new(false, std::option::Option::None) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let birdeye = match crate::MarketPriceBirdeyeProvider::new(birdeye_settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinbase_settings = match crate::MarketPriceCoinbaseExchangeSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinbase = match crate::MarketPriceCoinbaseExchangeProvider::new(coinbase_settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coingecko_settings = match crate::MarketPriceCoinGeckoSettings::keyless(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coingecko = match crate::MarketPriceCoinGeckoProvider::new(coingecko_settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinmarketcap_settings = match crate::MarketPriceCoinMarketCapSettings::keyless(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinmarketcap = match crate::MarketPriceCoinMarketCapProvider::new(coinmarketcap_settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinpaprika_settings = match crate::MarketPriceCoinPaprikaSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let coinpaprika = match crate::MarketPriceCoinPaprikaProvider::new(coinpaprika_settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let dexscreener_settings = match crate::MarketPriceDexScreenerSettings::new(false, "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let dexscreener = match crate::MarketPriceDexScreenerProvider::new(dexscreener_settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let jupiter_settings = match crate::MarketPriceJupiterSettings::keyless(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let jupiter = match crate::MarketPriceJupiterProvider::new(jupiter_settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let kraken_settings = match crate::MarketPriceKrakenSettings::new(false) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let kraken = match crate::MarketPriceKrakenProvider::new(kraken_settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(std::vec![
disabled_entry(birdeye.descriptor()),
disabled_entry(coinbase.descriptor()),
disabled_entry(coingecko.descriptor()),
disabled_entry(coinmarketcap.descriptor()),
disabled_entry(coinpaprika.descriptor()),
disabled_entry(dexscreener.descriptor()),
disabled_entry(jupiter.descriptor()),
disabled_entry(kraken.descriptor()),
]);
}
fn disabled_entry(descriptor: &crate::MarketPriceProviderDescriptor) -> crate::MarketPriceProviderRegistryEntry {
return crate::MarketPriceProviderRegistryEntry::new(descriptor.clone(), crate::MarketPriceProviderAvailability::Disabled);
}
fn simple_descriptor(provider_id: &'static str) -> ksp_core_lib::Result<crate::MarketPriceProviderDescriptor> {
let provider_id = match crate::MarketPriceProviderId::new(provider_id) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rate_limit = match crate::MarketPriceProviderRateLimit::fixed(1, 1, std::option::Option::None, crate::MarketPriceProviderRateLimitScope::Unspecified) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::MarketPriceProviderDescriptor::new(
provider_id,
"Provider",
crate::MarketPriceSemantics::AggregatedMarket,
crate::MarketPriceProviderAuthMode::None,
rate_limit,
std::option::Option::None,
true,
);
}
fn test_error(field: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_MARKET_PRICE_REGISTRY_INVALID, "Market-price registry test expectation failed")
.with_context("field", field);
}