v0.2.11-pre.007
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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(());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user