v0.2.11-pre.009
This commit is contained in:
@@ -55,10 +55,12 @@
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
|
||||
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
|
||||
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
|
||||
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/tests/desktop_contract.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Desktop build/shell contract audits for Config Desk.
|
||||
|
||||
@@ -150,8 +150,16 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_activates_shared_writab
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some(), "packaged Config resources map must exist");
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 8);
|
||||
assert_eq!(resources.len(), 10);
|
||||
assert_eq!(resources.get("../../config/std.logging.json").and_then(serde_json::Value::as_str), std::option::Option::Some("config/std.logging.json"));
|
||||
assert_eq!(
|
||||
resources.get("../../config/std.offchain_transport.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/std.offchain_transport.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/schemas/std.offchain_transport.schema.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/schemas/std.offchain_transport.schema.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/schemas/std.wallet.schema.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/schemas/std.wallet.schema.json"),
|
||||
|
||||
@@ -55,10 +55,12 @@
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.offchain_transport.json": "config/std.offchain_transport.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
|
||||
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
|
||||
"../../config/schemas/std.offchain_transport.schema.json": "config/schemas/std.offchain_transport.schema.json",
|
||||
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
|
||||
// version: 22
|
||||
// version: 23
|
||||
|
||||
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
|
||||
|
||||
@@ -472,7 +472,7 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_versi
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some(), "packaged Wallet Desk Config resources map must exist");
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 8);
|
||||
assert_eq!(resources.len(), 10);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-wallet-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-wallet-desk.json"),
|
||||
@@ -481,6 +481,14 @@ fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_versi
|
||||
resources.get("../../config/schemas/composite.schema.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/schemas/composite.schema.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/std.offchain_transport.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/std.offchain_transport.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/schemas/std.offchain_transport.schema.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/schemas/std.offchain_transport.schema.json"),
|
||||
);
|
||||
}
|
||||
let main = read_text(root.join("frontend/main.html").as_path());
|
||||
assert!(main.contains(packaged_version));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Release-wide deterministic compliance canaries for Wallet Desk.
|
||||
|
||||
@@ -198,7 +198,7 @@ fn packaged_resources_include_only_registered_config_sources_and_schemas() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert_eq!(resources.len(), 8);
|
||||
assert_eq!(resources.len(), 10);
|
||||
for (source, destination) in resources {
|
||||
let destination = destination.as_str();
|
||||
assert!(destination.is_some(), "resource destination must be textual");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: crates/ksp-config-lib/Cargo.toml
|
||||
# version: 8
|
||||
# version: 9
|
||||
|
||||
[package]
|
||||
name = "ksp-config-lib"
|
||||
@@ -12,6 +12,7 @@ directories.workspace = true
|
||||
jsonschema.workspace = true
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
ksp-offchain-transport-lib = { path = "../ksp-offchain-transport-lib" }
|
||||
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 18
|
||||
// version: 19
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -21,6 +21,7 @@ mod environment;
|
||||
mod error;
|
||||
mod logging;
|
||||
mod management;
|
||||
mod offchain_transport;
|
||||
mod packaging;
|
||||
mod persistence;
|
||||
mod profile;
|
||||
@@ -127,6 +128,8 @@ pub use self::management::LoggingOutputFilterConfig;
|
||||
pub use self::management::LoggingProfileConfig;
|
||||
/// Typed source contract for one global Logging target override.
|
||||
pub use self::management::LoggingTargetFilterConfig;
|
||||
/// Effective standard Off-chain Transport configuration resolved from Config.
|
||||
pub use self::offchain_transport::ResolvedOffchainTransportConfig;
|
||||
/// Writable KSP runtime roots prepared from packaged Config resources.
|
||||
pub use self::packaging::PackagedRuntimeLayout;
|
||||
/// Prepares the shared writable KSP desktop runtime from immutable packaged resources.
|
||||
@@ -155,6 +158,10 @@ pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME;
|
||||
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
|
||||
/// Default physical filename for the standard Logging JSON Schema document.
|
||||
pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
|
||||
/// Default physical filename for the standard Off-chain Transport configuration document.
|
||||
pub use self::registry::DEFAULT_STD_OFFCHAIN_TRANSPORT_FILENAME;
|
||||
/// Default physical filename for the standard Off-chain Transport JSON Schema document.
|
||||
pub use self::registry::DEFAULT_STD_OFFCHAIN_TRANSPORT_SCHEMA_FILENAME;
|
||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
|
||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||
@@ -169,12 +176,16 @@ pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
|
||||
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
|
||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
|
||||
/// Logical file identifier for the standard Off-chain Transport JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT;
|
||||
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
|
||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
|
||||
/// Logical file identifier for the standard Logging configuration document.
|
||||
pub use self::registry::FILE_ID_STD_LOGGING;
|
||||
/// Logical file identifier for the standard Off-chain Transport configuration document.
|
||||
pub use self::registry::FILE_ID_STD_OFFCHAIN_TRANSPORT;
|
||||
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||
pub use self::registry::FILE_ID_STD_TRANSPORT;
|
||||
/// Logical file identifier for the standard Wallet configuration document.
|
||||
|
||||
499
crates/ksp-config-lib/src/offchain_transport.rs
Normal file
499
crates/ksp-config-lib/src/offchain_transport.rs
Normal file
@@ -0,0 +1,499 @@
|
||||
// file: crates/ksp-config-lib/src/offchain_transport.rs
|
||||
// version: 1
|
||||
|
||||
//! Adapter from Config-owned Off-chain Transport documents to the provider-agnostic market-price runtime service.
|
||||
|
||||
/// Effective standard Off-chain Transport configuration resolved from Config.
|
||||
pub struct ResolvedOffchainTransportConfig {
|
||||
effective: crate::ResolvedConfigJson,
|
||||
file_id: crate::ConfigFileId,
|
||||
profile_id: String,
|
||||
selection_source: crate::ConfigProfileSelectionSource,
|
||||
service: ksp_offchain_transport_lib::MarketPriceService,
|
||||
source_path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl crate::ResolvedOffchainTransportConfig {
|
||||
/// Returns the detailed environment-resolved effective Config view with secret-safe diagnostics.
|
||||
#[must_use]
|
||||
pub const fn effective(&self) -> &crate::ResolvedConfigJson {
|
||||
return &self.effective;
|
||||
}
|
||||
|
||||
/// Returns the logical Config document identifier used by this runtime configuration.
|
||||
#[must_use]
|
||||
pub const fn file_id(&self) -> &crate::ConfigFileId {
|
||||
return &self.file_id;
|
||||
}
|
||||
|
||||
/// Returns the selected standard Off-chain Transport profile identifier.
|
||||
#[must_use]
|
||||
pub fn profile_id(&self) -> &str {
|
||||
return self.profile_id.as_str();
|
||||
}
|
||||
|
||||
/// Returns the source that selected the standard Off-chain Transport profile.
|
||||
#[must_use]
|
||||
pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource {
|
||||
return self.selection_source;
|
||||
}
|
||||
|
||||
/// Returns the provider-agnostic market-price runtime service.
|
||||
#[must_use]
|
||||
pub const fn service(&self) -> &ksp_offchain_transport_lib::MarketPriceService {
|
||||
return &self.service;
|
||||
}
|
||||
|
||||
/// Returns the physical source Config document path.
|
||||
#[must_use]
|
||||
pub fn source_path(&self) -> &std::path::Path {
|
||||
return self.source_path.as_path();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::ResolvedOffchainTransportConfig {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let registry = self.service.registry();
|
||||
return formatter
|
||||
.debug_struct("ResolvedOffchainTransportConfig")
|
||||
.field("effective", &self.effective)
|
||||
.field("file_id", &self.file_id)
|
||||
.field("profile_id", &self.profile_id)
|
||||
.field("provider_registry", ®istry)
|
||||
.field("selection_source", &self.selection_source)
|
||||
.field("source_path", &self.source_path)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::ConfigDocumentEngine {
|
||||
/// Loads the standard Off-chain Transport document and maps one profile to the generic market-price runtime service.
|
||||
pub fn load_resolved_offchain_transport_config(
|
||||
&self,
|
||||
requested_profile: std::option::Option<&str>,
|
||||
environment: &crate::ConfigEnvironment,
|
||||
) -> ksp_core_lib::Result<crate::ResolvedOffchainTransportConfig> {
|
||||
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let profile = self.load_resolved_profile(&file_id, requested_profile);
|
||||
let profile = match profile {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return resolve_offchain_transport_profile(&profile, environment);
|
||||
}
|
||||
|
||||
/// Maps an already resolved standard Off-chain Transport profile to the runtime service while preserving composite selection provenance.
|
||||
pub fn resolve_offchain_transport_config_profile(
|
||||
&self,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
environment: &crate::ConfigEnvironment,
|
||||
) -> ksp_core_lib::Result<crate::ResolvedOffchainTransportConfig> {
|
||||
if profile.file_id().as_str() != crate::FILE_ID_STD_OFFCHAIN_TRANSPORT {
|
||||
return std::result::Result::Err(effective_error(profile, "resolved Config profile does not reference the standard Off-chain Transport document"));
|
||||
}
|
||||
let descriptor = self.registry().descriptor(profile.file_id());
|
||||
if let std::result::Result::Err(error) = descriptor {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return resolve_offchain_transport_profile(profile, environment);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveOffchainTransportSource {
|
||||
format_version: u32,
|
||||
market_price: EffectiveMarketPriceSource,
|
||||
profile_id: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveMarketPriceSource {
|
||||
birdeye: EffectiveBirdeyeSource,
|
||||
coinbase_exchange: EffectiveEnabledSource,
|
||||
coingecko: EffectiveCoinGeckoSource,
|
||||
coinmarketcap: EffectiveCoinMarketCapSource,
|
||||
coinpaprika: EffectiveEnabledSource,
|
||||
dexscreener: EffectiveDexScreenerSource,
|
||||
jupiter: EffectiveJupiterSource,
|
||||
kraken: EffectiveEnabledSource,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveEnabledSource {
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveBirdeyeSource {
|
||||
api_key: std::option::Option<String>,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveCoinGeckoSource {
|
||||
access_mode: ksp_offchain_transport_lib::MarketPriceCoinGeckoAccessMode,
|
||||
api_key: std::option::Option<String>,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveCoinMarketCapSource {
|
||||
access_mode: ksp_offchain_transport_lib::MarketPriceCoinMarketCapAccessMode,
|
||||
api_key: std::option::Option<String>,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveDexScreenerSource {
|
||||
enabled: bool,
|
||||
sol_usd_pair_address: std::option::Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveJupiterSource {
|
||||
access_mode: ksp_offchain_transport_lib::MarketPriceJupiterAccessMode,
|
||||
api_key: std::option::Option<String>,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
fn resolve_offchain_transport_profile(
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
environment: &crate::ConfigEnvironment,
|
||||
) -> ksp_core_lib::Result<crate::ResolvedOffchainTransportConfig> {
|
||||
ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET,
|
||||
profile_id = profile.profile_id(),
|
||||
"mapping standard Off-chain Transport Config profile"
|
||||
);
|
||||
let effective = profile.resolve_effective_environment_detailed(environment);
|
||||
let effective = match effective {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let source = serde_json::from_value::<EffectiveOffchainTransportSource>(effective.value().clone());
|
||||
let source = match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
effective_error(profile, "effective Off-chain Transport Config cannot be decoded into the runtime adapter contract").with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
if source.format_version != 1 {
|
||||
return std::result::Result::Err(effective_error(profile, "effective Off-chain Transport format_version is unsupported"));
|
||||
}
|
||||
if source.profile_id != profile.profile_id() {
|
||||
return std::result::Result::Err(effective_error(profile, "effective Off-chain Transport profile_id does not match the selected profile"));
|
||||
}
|
||||
let setups = map_market_price_setups(source.market_price, &effective, profile);
|
||||
let setups = match setups {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let service = ksp_offchain_transport_lib::MarketPriceService::new(setups);
|
||||
let service = match service {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(offchain_contract_error(
|
||||
profile,
|
||||
"effective market-price providers fail the Off-chain Transport runtime contract",
|
||||
&error,
|
||||
));
|
||||
},
|
||||
};
|
||||
let provider_count = service.registry().entries().len();
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
profile_id = profile.profile_id(),
|
||||
format_version = source.format_version,
|
||||
provider_count,
|
||||
"mapped standard Off-chain Transport Config to market-price service"
|
||||
);
|
||||
return std::result::Result::Ok(crate::ResolvedOffchainTransportConfig {
|
||||
effective,
|
||||
file_id: profile.file_id().clone(),
|
||||
profile_id: profile.profile_id().to_owned(),
|
||||
selection_source: profile.selection_source(),
|
||||
service,
|
||||
source_path: profile.path().to_path_buf(),
|
||||
});
|
||||
}
|
||||
|
||||
fn map_market_price_setups(
|
||||
source: EffectiveMarketPriceSource,
|
||||
effective: &crate::ResolvedConfigJson,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
) -> ksp_core_lib::Result<std::vec::Vec<ksp_offchain_transport_lib::MarketPriceProviderSetup>> {
|
||||
let birdeye = map_birdeye(source.birdeye, effective, profile);
|
||||
let birdeye = match birdeye {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coinbase_exchange = ksp_offchain_transport_lib::MarketPriceCoinbaseExchangeSettings::new(source.coinbase_exchange.enabled);
|
||||
let coinbase_exchange = match coinbase_exchange {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(provider_contract_error(profile, "coinbase_exchange", &error)),
|
||||
};
|
||||
let coingecko = map_coingecko(source.coingecko, effective, profile);
|
||||
let coingecko = match coingecko {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coinmarketcap = map_coinmarketcap(source.coinmarketcap, effective, profile);
|
||||
let coinmarketcap = match coinmarketcap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let coinpaprika = ksp_offchain_transport_lib::MarketPriceCoinPaprikaSettings::new(source.coinpaprika.enabled);
|
||||
let coinpaprika = match coinpaprika {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(provider_contract_error(profile, "coinpaprika", &error)),
|
||||
};
|
||||
let dexscreener = map_dexscreener(source.dexscreener, effective, profile);
|
||||
let dexscreener = match dexscreener {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let jupiter = map_jupiter(source.jupiter, effective, profile);
|
||||
let jupiter = match jupiter {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let kraken = ksp_offchain_transport_lib::MarketPriceKrakenSettings::new(source.kraken.enabled);
|
||||
let kraken = match kraken {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(provider_contract_error(profile, "kraken", &error)),
|
||||
};
|
||||
return std::result::Result::Ok(std::vec![
|
||||
ksp_offchain_transport_lib::MarketPriceProviderSetup::Birdeye(birdeye),
|
||||
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinbaseExchange(coinbase_exchange),
|
||||
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinGecko(coingecko),
|
||||
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinMarketCap(coinmarketcap),
|
||||
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinPaprika(coinpaprika),
|
||||
ksp_offchain_transport_lib::MarketPriceProviderSetup::DexScreener(dexscreener),
|
||||
ksp_offchain_transport_lib::MarketPriceProviderSetup::Jupiter(jupiter),
|
||||
ksp_offchain_transport_lib::MarketPriceProviderSetup::Kraken(kraken),
|
||||
]);
|
||||
}
|
||||
|
||||
fn map_birdeye(
|
||||
source: EffectiveBirdeyeSource,
|
||||
effective: &crate::ResolvedConfigJson,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
) -> ksp_core_lib::Result<ksp_offchain_transport_lib::MarketPriceBirdeyeSettings> {
|
||||
if source.api_key.is_some()
|
||||
&& let std::result::Result::Err(error) = validate_secret_field_provenance(effective, "/market_price/birdeye/api_key", profile, "birdeye")
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let settings = ksp_offchain_transport_lib::MarketPriceBirdeyeSettings::new(source.enabled, source.api_key);
|
||||
return match settings {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(provider_contract_error(profile, "birdeye", &error)),
|
||||
};
|
||||
}
|
||||
|
||||
fn map_coingecko(
|
||||
source: EffectiveCoinGeckoSource,
|
||||
effective: &crate::ResolvedConfigJson,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
) -> ksp_core_lib::Result<ksp_offchain_transport_lib::MarketPriceCoinGeckoSettings> {
|
||||
if source.api_key.is_some()
|
||||
&& let std::result::Result::Err(error) = validate_secret_field_provenance(effective, "/market_price/coingecko/api_key", profile, "coingecko")
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let settings = match source.access_mode {
|
||||
ksp_offchain_transport_lib::MarketPriceCoinGeckoAccessMode::Demo => {
|
||||
ksp_offchain_transport_lib::MarketPriceCoinGeckoSettings::demo(source.enabled, source.api_key)
|
||||
},
|
||||
ksp_offchain_transport_lib::MarketPriceCoinGeckoAccessMode::Keyless => {
|
||||
ksp_offchain_transport_lib::MarketPriceCoinGeckoSettings::keyless(source.enabled)
|
||||
},
|
||||
};
|
||||
return match settings {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(provider_contract_error(profile, "coingecko", &error)),
|
||||
};
|
||||
}
|
||||
|
||||
fn map_coinmarketcap(
|
||||
source: EffectiveCoinMarketCapSource,
|
||||
effective: &crate::ResolvedConfigJson,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
) -> ksp_core_lib::Result<ksp_offchain_transport_lib::MarketPriceCoinMarketCapSettings> {
|
||||
if source.api_key.is_some()
|
||||
&& let std::result::Result::Err(error) = validate_secret_field_provenance(effective, "/market_price/coinmarketcap/api_key", profile, "coinmarketcap")
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let settings = match source.access_mode {
|
||||
ksp_offchain_transport_lib::MarketPriceCoinMarketCapAccessMode::Basic => {
|
||||
ksp_offchain_transport_lib::MarketPriceCoinMarketCapSettings::basic(source.enabled, source.api_key)
|
||||
},
|
||||
ksp_offchain_transport_lib::MarketPriceCoinMarketCapAccessMode::Keyless => {
|
||||
ksp_offchain_transport_lib::MarketPriceCoinMarketCapSettings::keyless(source.enabled)
|
||||
},
|
||||
};
|
||||
return match settings {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(provider_contract_error(profile, "coinmarketcap", &error)),
|
||||
};
|
||||
}
|
||||
|
||||
fn map_dexscreener(
|
||||
source: EffectiveDexScreenerSource,
|
||||
effective: &crate::ResolvedConfigJson,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
) -> ksp_core_lib::Result<ksp_offchain_transport_lib::MarketPriceDexScreenerSettings> {
|
||||
if source.sol_usd_pair_address.is_some()
|
||||
&& let std::result::Result::Err(error) =
|
||||
validate_public_field_provenance(effective, "/market_price/dexscreener/sol_usd_pair_address", profile, "dexscreener")
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let settings = ksp_offchain_transport_lib::MarketPriceDexScreenerSettings::new(source.enabled, source.sol_usd_pair_address);
|
||||
return match settings {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(provider_contract_error(profile, "dexscreener", &error)),
|
||||
};
|
||||
}
|
||||
|
||||
fn map_jupiter(
|
||||
source: EffectiveJupiterSource,
|
||||
effective: &crate::ResolvedConfigJson,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
) -> ksp_core_lib::Result<ksp_offchain_transport_lib::MarketPriceJupiterSettings> {
|
||||
if source.api_key.is_some()
|
||||
&& let std::result::Result::Err(error) = validate_secret_field_provenance(effective, "/market_price/jupiter/api_key", profile, "jupiter")
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let settings = match source.access_mode {
|
||||
ksp_offchain_transport_lib::MarketPriceJupiterAccessMode::Free => {
|
||||
ksp_offchain_transport_lib::MarketPriceJupiterSettings::free(source.enabled, source.api_key)
|
||||
},
|
||||
ksp_offchain_transport_lib::MarketPriceJupiterAccessMode::Keyless => ksp_offchain_transport_lib::MarketPriceJupiterSettings::keyless(source.enabled),
|
||||
};
|
||||
return match settings {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(provider_contract_error(profile, "jupiter", &error)),
|
||||
};
|
||||
}
|
||||
|
||||
fn validate_secret_field_provenance(
|
||||
effective: &crate::ResolvedConfigJson,
|
||||
pointer: &str,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
provider: &'static str,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let provenance = match effective.provenance_at(pointer) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
effective_error(profile, "Off-chain Transport secret provenance is unavailable")
|
||||
.with_context("provider", provider)
|
||||
.with_context("field", pointer),
|
||||
);
|
||||
},
|
||||
};
|
||||
let mut has_secret_environment = false;
|
||||
for item in provenance {
|
||||
let variable_name = match item.variable_name() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
let sensitivity = crate::ConfigSensitivity::from_variable_name(variable_name);
|
||||
let sensitivity = match sensitivity {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !sensitivity.is_secret() {
|
||||
return std::result::Result::Err(
|
||||
effective_error(profile, "Off-chain Transport credentials may reference only secret environment variables")
|
||||
.with_context("provider", provider)
|
||||
.with_context("field", pointer),
|
||||
);
|
||||
}
|
||||
has_secret_environment = true;
|
||||
}
|
||||
if !has_secret_environment {
|
||||
return std::result::Result::Err(
|
||||
effective_error(profile, "Off-chain Transport credentials require secret environment provenance")
|
||||
.with_context("provider", provider)
|
||||
.with_context("field", pointer),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_public_field_provenance(
|
||||
effective: &crate::ResolvedConfigJson,
|
||||
pointer: &str,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
provider: &'static str,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let provenance = match effective.provenance_at(pointer) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
effective_error(profile, "Off-chain Transport public provenance is unavailable")
|
||||
.with_context("provider", provider)
|
||||
.with_context("field", pointer),
|
||||
);
|
||||
},
|
||||
};
|
||||
for item in provenance {
|
||||
let variable_name = match item.variable_name() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
let sensitivity = crate::ConfigSensitivity::from_variable_name(variable_name);
|
||||
let sensitivity = match sensitivity {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if sensitivity != crate::ConfigSensitivity::Public {
|
||||
return std::result::Result::Err(
|
||||
effective_error(profile, "public Off-chain Transport fields may reference only public environment variables")
|
||||
.with_context("provider", provider)
|
||||
.with_context("field", pointer),
|
||||
);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn provider_contract_error(profile: &crate::ResolvedConfigProfile, provider: &'static str, error: &ksp_core_lib::Error) -> ksp_core_lib::Error {
|
||||
return offchain_contract_error(profile, "effective market-price provider settings fail the Off-chain Transport runtime contract", error)
|
||||
.with_context("provider", provider);
|
||||
}
|
||||
|
||||
fn offchain_contract_error(profile: &crate::ResolvedConfigProfile, reason: &'static str, error: &ksp_core_lib::Error) -> ksp_core_lib::Error {
|
||||
return effective_error(profile, reason)
|
||||
.with_context("offchain_transport_error_domain", error.code().domain())
|
||||
.with_context("offchain_transport_error_code", error.code().code());
|
||||
}
|
||||
|
||||
fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract")
|
||||
.with_context("file_id", profile.file_id().as_str())
|
||||
.with_context("profile_id", profile.profile_id())
|
||||
.with_context("reason", reason);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/offchain_transport.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/registry.rs
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||
pub const ARG_FILE_MAP: &str = "--filemap";
|
||||
@@ -11,6 +11,10 @@ pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
|
||||
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
|
||||
/// Default physical filename for the standard Logging JSON Schema document.
|
||||
pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
|
||||
/// Default physical filename for the standard Off-chain Transport configuration document.
|
||||
pub const DEFAULT_STD_OFFCHAIN_TRANSPORT_FILENAME: &str = "std.offchain_transport.json";
|
||||
/// Default physical filename for the standard Off-chain Transport JSON Schema document.
|
||||
pub const DEFAULT_STD_OFFCHAIN_TRANSPORT_SCHEMA_FILENAME: &str = "std.offchain_transport.schema.json";
|
||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
|
||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||
@@ -25,12 +29,16 @@ pub const FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK: &str = "cfg.composite.ksp-app-w
|
||||
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
|
||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
|
||||
/// Logical file identifier for the standard Off-chain Transport JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT: &str = "schema.std.offchain_transport";
|
||||
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
|
||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
|
||||
/// Logical file identifier for the standard Logging configuration document.
|
||||
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
|
||||
/// Logical file identifier for the standard Off-chain Transport configuration document.
|
||||
pub const FILE_ID_STD_OFFCHAIN_TRANSPORT: &str = "cfg.std.offchain_transport";
|
||||
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
|
||||
/// Logical file identifier for the standard Wallet configuration document.
|
||||
@@ -172,6 +180,26 @@ impl ConfigFileRegistry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let offchain_transport = ConfigFileDescriptor::new(
|
||||
FILE_ID_STD_OFFCHAIN_TRANSPORT,
|
||||
ConfigFileKind::Config,
|
||||
DEFAULT_STD_OFFCHAIN_TRANSPORT_FILENAME,
|
||||
std::option::Option::Some(FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT),
|
||||
);
|
||||
let offchain_transport = match offchain_transport {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let offchain_transport_schema = ConfigFileDescriptor::new(
|
||||
FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT,
|
||||
ConfigFileKind::Schema,
|
||||
DEFAULT_STD_OFFCHAIN_TRANSPORT_SCHEMA_FILENAME,
|
||||
std::option::Option::None,
|
||||
);
|
||||
let offchain_transport_schema = match offchain_transport_schema {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let transport = ConfigFileDescriptor::new(
|
||||
FILE_ID_STD_TRANSPORT,
|
||||
ConfigFileKind::Config,
|
||||
@@ -204,7 +232,18 @@ impl ConfigFileRegistry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::build_registry([composite, composite_schema, logging, logging_schema, transport, transport_schema, wallet, wallet_schema]);
|
||||
return crate::build_registry([
|
||||
composite,
|
||||
composite_schema,
|
||||
logging,
|
||||
logging_schema,
|
||||
offchain_transport,
|
||||
offchain_transport_schema,
|
||||
transport,
|
||||
transport_schema,
|
||||
wallet,
|
||||
wallet_schema,
|
||||
]);
|
||||
}
|
||||
|
||||
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/tests/ownership.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Workspace ownership audits for KSP application configuration boundaries.
|
||||
|
||||
@@ -177,7 +177,7 @@ fn has_preceding_explanatory_comment(source: &str, assignment_line_index: usize)
|
||||
#[test]
|
||||
fn foundational_dependency_direction_does_not_point_back_to_config() {
|
||||
let root = workspace_root();
|
||||
for crate_name in ["ksp-core-lib", "ksp-logging-lib"] {
|
||||
for crate_name in ["ksp-core-lib", "ksp-logging-lib", "ksp-offchain-transport-lib"] {
|
||||
let manifest_path = root.join("crates").join(crate_name).join("Cargo.toml");
|
||||
let manifest = std::fs::read_to_string(manifest_path.as_path());
|
||||
assert!(manifest.is_ok(), "unable to read {}", manifest_path.display());
|
||||
@@ -266,6 +266,8 @@ fn workspace_crates_do_not_hardcode_config_managed_physical_files() {
|
||||
"\"composite.schema.json\"",
|
||||
"\"std.logging.json\"",
|
||||
"\"std.logging.schema.json\"",
|
||||
"\"std.offchain_transport.json\"",
|
||||
"\"std.offchain_transport.schema.json\"",
|
||||
"\"std.transport.json\"",
|
||||
"\"std.transport.schema.json\"",
|
||||
"\"std.wallet.json\"",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
||||
// version: 23
|
||||
// version: 24
|
||||
|
||||
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
|
||||
//! Logging/Transport adapters and management contracts.
|
||||
@@ -80,16 +80,18 @@ fn registry_descriptor_inventory_is_available_from_crate_root() {
|
||||
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
|
||||
assert_eq!(descriptors.len(), 8);
|
||||
assert_eq!(descriptors.len(), 10);
|
||||
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
|
||||
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
|
||||
let schema_file_id = descriptors[3].schema_file_id();
|
||||
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[8].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[9].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
|
||||
let schema_file_id = descriptors[4].schema_file_id();
|
||||
assert!(schema_file_id.is_some(), "public Wallet descriptor should preserve schema association");
|
||||
if let std::option::Option::Some(schema_file_id) = schema_file_id {
|
||||
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_WALLET);
|
||||
@@ -276,6 +278,18 @@ fn wallet_adapter_contract_is_available_from_crate_root() {
|
||||
assert_eq!(ksp_config_lib::DEFAULT_STD_WALLET_SCHEMA_FILENAME, "std.wallet.schema.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offchain_transport_adapter_contract_is_available_from_crate_root() {
|
||||
let _loader = ksp_config_lib::ConfigDocumentEngine::load_resolved_offchain_transport_config;
|
||||
let _composite_loader = ksp_config_lib::ConfigDocumentEngine::resolve_offchain_transport_config_profile;
|
||||
assert!(std::mem::size_of::<ksp_config_lib::ResolvedOffchainTransportConfig>() > 0);
|
||||
let _service = ksp_config_lib::ResolvedOffchainTransportConfig::service;
|
||||
assert_eq!(ksp_config_lib::FILE_ID_STD_OFFCHAIN_TRANSPORT, "cfg.std.offchain_transport");
|
||||
assert_eq!(ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT, "schema.std.offchain_transport");
|
||||
assert_eq!(ksp_config_lib::DEFAULT_STD_OFFCHAIN_TRANSPORT_FILENAME, "std.offchain_transport.json");
|
||||
assert_eq!(ksp_config_lib::DEFAULT_STD_OFFCHAIN_TRANSPORT_SCHEMA_FILENAME, "std.offchain_transport.schema.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packaged_runtime_layout_contract_is_available_from_crate_root() {
|
||||
let prepare: fn(&std::path::Path) -> ksp_core_lib::Result<ksp_config_lib::PackagedRuntimeLayout> = ksp_config_lib::prepare_packaged_runtime;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"default_profile": "public_keyless",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "public_keyless",
|
||||
"market_price": {
|
||||
"birdeye": {
|
||||
"enabled": false
|
||||
},
|
||||
"coinbase_exchange": {
|
||||
"enabled": true
|
||||
},
|
||||
"coingecko": {
|
||||
"enabled": true,
|
||||
"access_mode": "keyless"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"enabled": true,
|
||||
"access_mode": "keyless"
|
||||
},
|
||||
"coinpaprika": {
|
||||
"enabled": true
|
||||
},
|
||||
"dexscreener": {
|
||||
"enabled": false
|
||||
},
|
||||
"jupiter": {
|
||||
"enabled": true,
|
||||
"access_mode": "keyless"
|
||||
},
|
||||
"kraken": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile_id": "all_free",
|
||||
"market_price": {
|
||||
"birdeye": {
|
||||
"enabled": true,
|
||||
"api_key": "${KSP_SECRET_BIRDEYE_API_KEY}"
|
||||
},
|
||||
"coinbase_exchange": {
|
||||
"enabled": true
|
||||
},
|
||||
"coingecko": {
|
||||
"enabled": true,
|
||||
"access_mode": "demo",
|
||||
"api_key": "${KSP_SECRET_COINGECKO_DEMO_API_KEY}"
|
||||
},
|
||||
"coinmarketcap": {
|
||||
"enabled": true,
|
||||
"access_mode": "basic",
|
||||
"api_key": "${KSP_SECRET_COINMARKETCAP_API_KEY}"
|
||||
},
|
||||
"coinpaprika": {
|
||||
"enabled": true
|
||||
},
|
||||
"dexscreener": {
|
||||
"enabled": true,
|
||||
"sol_usd_pair_address": "${KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS}"
|
||||
},
|
||||
"jupiter": {
|
||||
"enabled": true,
|
||||
"access_mode": "free",
|
||||
"api_key": "${KSP_SECRET_JUPITER_API_KEY}"
|
||||
},
|
||||
"kraken": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
239
crates/ksp-config-lib/unit_tests/offchain_transport.rs
Normal file
239
crates/ksp-config-lib/unit_tests/offchain_transport.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/offchain_transport.rs
|
||||
// version: 1
|
||||
|
||||
const TEST_PAIR: &str = "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE";
|
||||
|
||||
#[test]
|
||||
fn committed_public_keyless_profile_maps_all_eight_providers_without_secret_environment() {
|
||||
let engine = committed_engine();
|
||||
let engine = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let resolved = engine.load_resolved_offchain_transport_config(std::option::Option::None, &environment);
|
||||
assert!(resolved.is_ok(), "committed public_keyless Off-chain Transport profile should map: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(resolved.profile_id(), "public_keyless");
|
||||
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
|
||||
let registry = resolved.service().registry();
|
||||
assert_eq!(registry.len(), 8);
|
||||
let ids: std::vec::Vec<&str> = registry.entries().iter().map(|entry| return entry.descriptor().id().as_str()).collect();
|
||||
assert_eq!(ids, ["birdeye", "coinbase_exchange", "coingecko", "coinmarketcap", "coinpaprika", "dexscreener", "jupiter", "kraken"]);
|
||||
assert_provider_availability(®istry, "birdeye", ksp_offchain_transport_lib::MarketPriceProviderAvailability::Disabled);
|
||||
assert_provider_availability(®istry, "dexscreener", ksp_offchain_transport_lib::MarketPriceProviderAvailability::Disabled);
|
||||
for provider_id in ["coinbase_exchange", "coingecko", "coinmarketcap", "coinpaprika", "jupiter", "kraken"] {
|
||||
assert_provider_availability(®istry, provider_id, ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready);
|
||||
}
|
||||
let debug = format!("{resolved:?}");
|
||||
assert!(!debug.contains("api_key"), "safe Debug should not expose credential field contents from the selected keyless profile");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn committed_all_free_profile_requires_config_owned_secrets_and_public_pair_provenance() {
|
||||
let engine = committed_engine();
|
||||
let engine = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let canaries = [
|
||||
("KSP_SECRET_BIRDEYE_API_KEY", "birdeye-secret-canary"),
|
||||
("KSP_SECRET_COINGECKO_DEMO_API_KEY", "coingecko-secret-canary"),
|
||||
("KSP_SECRET_COINMARKETCAP_API_KEY", "coinmarketcap-secret-canary"),
|
||||
("KSP_SECRET_JUPITER_API_KEY", "jupiter-secret-canary"),
|
||||
("KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS", TEST_PAIR),
|
||||
];
|
||||
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||
for (name, value) in canaries {
|
||||
process.insert(name.to_owned(), value.to_owned());
|
||||
}
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
let resolved = engine.load_resolved_offchain_transport_config(std::option::Option::Some("all_free"), &environment);
|
||||
assert!(resolved.is_ok(), "committed all_free Off-chain Transport profile should map from Config-owned environment: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.profile_id(), "all_free");
|
||||
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
|
||||
let registry = resolved.service().registry();
|
||||
assert_eq!(registry.len(), 8);
|
||||
for entry in registry.entries() {
|
||||
assert_eq!(entry.state().availability(), ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready);
|
||||
}
|
||||
assert!(resolved.effective().sensitivity().is_secret());
|
||||
let safe = resolved.effective().safe_value().to_string();
|
||||
for secret in ["birdeye-secret-canary", "coingecko-secret-canary", "coinmarketcap-secret-canary", "jupiter-secret-canary"] {
|
||||
assert!(!safe.contains(secret), "safe effective Config must redact provider credential canary");
|
||||
}
|
||||
assert!(safe.contains(TEST_PAIR), "public DexScreener pair should remain visible in the safe effective Config");
|
||||
let debug = format!("{resolved:?}");
|
||||
for secret in ["birdeye-secret-canary", "coingecko-secret-canary", "coinmarketcap-secret-canary", "jupiter-secret-canary"] {
|
||||
assert!(!debug.contains(secret), "ResolvedOffchainTransportConfig Debug must redact provider credential canary");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_or_nonsecret_provider_credentials_are_rejected_by_effective_adapter() {
|
||||
let fixture = tempfile::tempdir();
|
||||
assert!(fixture.is_ok(), "temporary Config root should be creatable: {fixture:?}");
|
||||
let fixture = match fixture {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let source = committed_document_value();
|
||||
let mut source = match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let profiles = source.get_mut("profiles").and_then(serde_json::Value::as_array_mut);
|
||||
assert!(profiles.is_some(), "fixture should expose profiles");
|
||||
if let std::option::Option::Some(profiles) = profiles {
|
||||
let all_free = profiles
|
||||
.iter_mut()
|
||||
.find(|profile| return profile.get("profile_id").and_then(serde_json::Value::as_str) == std::option::Option::Some("all_free"));
|
||||
assert!(all_free.is_some(), "fixture should contain all_free profile");
|
||||
if let std::option::Option::Some(all_free) = all_free {
|
||||
all_free["market_price"]["birdeye"]["api_key"] = serde_json::Value::String("literal-secret".to_owned());
|
||||
}
|
||||
}
|
||||
let engine = fixture_engine_with_document(fixture.path(), &source);
|
||||
assert!(engine.is_ok(), "literal-secret fixture engine should be constructible: {engine:?}");
|
||||
let engine = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let mut process = all_free_environment();
|
||||
process.insert("KSP_SECRET_BIRDEYE_API_KEY".to_owned(), "unused-secret".to_owned());
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
let resolved = engine.load_resolved_offchain_transport_config(std::option::Option::Some("all_free"), &environment);
|
||||
assert!(resolved.is_err(), "literal provider credential must be rejected even though the JSON Schema accepts a non-empty string");
|
||||
if let std::result::Result::Err(error) = resolved {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
|
||||
assert!(!format!("{error:?}").contains("literal-secret"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dexscreener_pair_environment_must_use_public_namespace_and_disabled_pair_may_be_absent() {
|
||||
let fixture = tempfile::tempdir();
|
||||
assert!(fixture.is_ok(), "temporary Config root should be creatable: {fixture:?}");
|
||||
let fixture = match fixture {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let source = committed_document_value();
|
||||
let mut source = match source {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let profiles = source.get_mut("profiles").and_then(serde_json::Value::as_array_mut);
|
||||
if let std::option::Option::Some(profiles) = profiles {
|
||||
let all_free = profiles
|
||||
.iter_mut()
|
||||
.find(|profile| return profile.get("profile_id").and_then(serde_json::Value::as_str) == std::option::Option::Some("all_free"));
|
||||
if let std::option::Option::Some(all_free) = all_free {
|
||||
all_free["market_price"]["dexscreener"]["sol_usd_pair_address"] = serde_json::Value::String("${KSP_SECRET_DEXSCREENER_PAIR}".to_owned());
|
||||
}
|
||||
}
|
||||
let engine = fixture_engine_with_document(fixture.path(), &source);
|
||||
assert!(engine.is_ok(), "secret-pair fixture engine should be constructible: {engine:?}");
|
||||
let engine = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let mut process = all_free_environment();
|
||||
process.remove("KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS");
|
||||
process.insert("KSP_SECRET_DEXSCREENER_PAIR".to_owned(), TEST_PAIR.to_owned());
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
let resolved = engine.load_resolved_offchain_transport_config(std::option::Option::Some("all_free"), &environment);
|
||||
assert!(resolved.is_err(), "DexScreener pair environment must not use secret provenance");
|
||||
if let std::result::Result::Err(error) = resolved {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
|
||||
}
|
||||
let disabled = ksp_offchain_transport_lib::MarketPriceDexScreenerSettings::new(false, std::option::Option::None);
|
||||
assert!(disabled.is_ok(), "disabled DexScreener runtime settings should accept an absent pair after pre.009 capability reconciliation: {disabled:?}");
|
||||
}
|
||||
|
||||
fn assert_provider_availability(
|
||||
registry: &ksp_offchain_transport_lib::MarketPriceProviderRegistry,
|
||||
provider_id: &str,
|
||||
expected: ksp_offchain_transport_lib::MarketPriceProviderAvailability,
|
||||
) {
|
||||
let provider_id = ksp_offchain_transport_lib::MarketPriceProviderId::new(provider_id);
|
||||
assert!(provider_id.is_ok(), "provider id fixture should be valid: {provider_id:?}");
|
||||
if let std::result::Result::Ok(provider_id) = provider_id {
|
||||
let state = registry.state(&provider_id);
|
||||
assert!(state.is_some(), "provider should be present in Config-produced service registry");
|
||||
if let std::option::Option::Some(state) = state {
|
||||
assert_eq!(state.availability(), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||
let workspace = workspace_root();
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let registry = match registry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
|
||||
}
|
||||
|
||||
fn fixture_engine_with_document(root: &std::path::Path, document: &serde_json::Value) -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||
let config_root = root.join("config");
|
||||
let create = std::fs::create_dir_all(config_root.as_path());
|
||||
if let std::result::Result::Err(error) = create {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "test Config root cannot be created").with_source(error),
|
||||
);
|
||||
}
|
||||
let bytes = serde_json::to_vec_pretty(document);
|
||||
let bytes = match bytes {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_SYNTAX_INVALID, "test Config cannot be encoded").with_source(error),
|
||||
);
|
||||
},
|
||||
};
|
||||
let path = config_root.join(crate::DEFAULT_STD_OFFCHAIN_TRANSPORT_FILENAME);
|
||||
if let std::result::Result::Err(error) = std::fs::write(path.as_path(), bytes) {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "test Config cannot be written").with_source(error));
|
||||
}
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(config_root, workspace_root().join("config/schemas"));
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let registry = match registry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
|
||||
}
|
||||
|
||||
fn committed_document_value() -> std::result::Result<serde_json::Value, serde_json::Error> {
|
||||
return serde_json::from_str(include_str!("../../../config/std.offchain_transport.json"));
|
||||
}
|
||||
|
||||
fn all_free_environment() -> std::collections::BTreeMap<String, String> {
|
||||
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||
process.insert("KSP_SECRET_BIRDEYE_API_KEY".to_owned(), "birdeye-test".to_owned());
|
||||
process.insert("KSP_SECRET_COINGECKO_DEMO_API_KEY".to_owned(), "coingecko-test".to_owned());
|
||||
process.insert("KSP_SECRET_COINMARKETCAP_API_KEY".to_owned(), "coinmarketcap-test".to_owned());
|
||||
process.insert("KSP_SECRET_JUPITER_API_KEY".to_owned(), "jupiter-test".to_owned());
|
||||
process.insert("KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS".to_owned(), TEST_PAIR.to_owned());
|
||||
return process;
|
||||
}
|
||||
|
||||
fn workspace_root() -> std::path::PathBuf {
|
||||
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/registry.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
#[test]
|
||||
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
||||
@@ -7,21 +7,23 @@ fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect();
|
||||
assert_eq!(descriptors.len(), 8);
|
||||
assert_eq!(descriptors.len(), 10);
|
||||
assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK);
|
||||
assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_KSP_APP_WALLET_DESK_FILENAME));
|
||||
assert_eq!(descriptors[0].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_COMPOSITE));
|
||||
assert_eq!(descriptors[1].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[3].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
|
||||
assert_eq!(descriptors[3].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET));
|
||||
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET);
|
||||
assert!(descriptors[0..4].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
|
||||
assert!(descriptors[4..8].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
|
||||
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_STD_WALLET);
|
||||
assert_eq!(descriptors[4].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
|
||||
assert_eq!(descriptors[4].schema_file_id().map(crate::ConfigFileId::as_str), std::option::Option::Some(crate::FILE_ID_SCHEMA_STD_WALLET));
|
||||
assert_eq!(descriptors[5].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[6].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[9].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_WALLET);
|
||||
assert!(descriptors[0..5].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
|
||||
assert!(descriptors[5..10].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +87,31 @@ fn defaults_register_logging_document_and_schema_with_distinct_roots() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_register_offchain_transport_document_and_schema_with_distinct_roots() {
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let config_id = crate::ConfigFileId::new(crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||
let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||
assert!(config_id.is_ok(), "Off-chain Transport file_id should be valid: {config_id:?}");
|
||||
assert!(schema_id.is_ok(), "Off-chain Transport schema file_id should be valid: {schema_id:?}");
|
||||
if let (std::result::Result::Ok(config_id), std::result::Result::Ok(schema_id)) = (config_id, schema_id) {
|
||||
let config = registry.descriptor(&config_id);
|
||||
let schema = registry.descriptor(&schema_id);
|
||||
assert!(config.is_ok(), "Off-chain Transport descriptor should exist: {config:?}");
|
||||
assert!(schema.is_ok(), "Off-chain Transport schema descriptor should exist: {schema:?}");
|
||||
if let (std::result::Result::Ok(config), std::result::Result::Ok(schema)) = (config, schema) {
|
||||
assert_eq!(config.kind(), crate::ConfigFileKind::Config);
|
||||
assert_eq!(config.filename(), std::path::Path::new(crate::DEFAULT_STD_OFFCHAIN_TRANSPORT_FILENAME));
|
||||
assert_eq!(config.schema_file_id(), std::option::Option::Some(&schema_id));
|
||||
assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
|
||||
assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_STD_OFFCHAIN_TRANSPORT_SCHEMA_FILENAME));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_register_transport_document_and_schema_with_distinct_roots() {
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/src/market_price_dexscreener.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! DexScreener SOL/USD adapter bound to one explicitly configured Solana pair address.
|
||||
|
||||
@@ -11,19 +11,26 @@ const DEXSCREENER_SOL_MINT: &str = "So11111111111111111111111111111111111111112"
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MarketPriceDexScreenerSettings {
|
||||
common: crate::MarketPriceProviderCommonSettings,
|
||||
pair_address: ksp_core_lib::Pubkey,
|
||||
pair_address: std::option::Option<ksp_core_lib::Pubkey>,
|
||||
}
|
||||
|
||||
impl crate::MarketPriceDexScreenerSettings {
|
||||
/// Creates DexScreener settings for one explicit Solana pair address.
|
||||
pub fn new(enabled: bool, pair_address: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||
/// Creates DexScreener settings for one optional explicit Solana pair address.
|
||||
///
|
||||
/// An enabled provider requires a valid pair address. A disabled provider may omit it so Config does not need to fabricate provider data merely to keep the
|
||||
/// provider visible in the generic registry.
|
||||
pub fn new(enabled: bool, pair_address: std::option::Option<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||
let provider_id = match crate::MarketPriceProviderId::new(DEXSCREENER_PROVIDER_ID) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let pair_address = match pair_address.into().parse::<ksp_core_lib::Pubkey>() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(provider_settings_error("pair_address")),
|
||||
let pair_address = match pair_address {
|
||||
std::option::Option::Some(value) => match value.parse::<ksp_core_lib::Pubkey>() {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(provider_settings_error("pair_address")),
|
||||
},
|
||||
std::option::Option::None if enabled => return std::result::Result::Err(provider_settings_error("pair_address")),
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let common = crate::MarketPriceProviderCommonSettings::new(provider_id, enabled);
|
||||
return std::result::Result::Ok(Self { common, pair_address });
|
||||
@@ -35,10 +42,10 @@ impl crate::MarketPriceDexScreenerSettings {
|
||||
return &self.common;
|
||||
}
|
||||
|
||||
/// Returns the explicit Solana pair address used by this adapter.
|
||||
/// Returns the explicit Solana pair address when one is configured.
|
||||
#[must_use]
|
||||
pub const fn pair_address(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.pair_address;
|
||||
pub const fn pair_address(&self) -> std::option::Option<&ksp_core_lib::Pubkey> {
|
||||
return self.pair_address.as_ref();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,11 +91,15 @@ impl crate::MarketPriceDexScreenerProvider {
|
||||
if let std::result::Result::Err(error) = crate::admit_request(DEXSCREENER_PROVIDER_ID, &self.admission) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let pair_address = match self.settings.pair_address() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(provider_settings_error("pair_address")),
|
||||
};
|
||||
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(self.settings.pair_address()) {
|
||||
let request = match build_request(pair_address) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
@@ -100,13 +111,7 @@ impl crate::MarketPriceDexScreenerProvider {
|
||||
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(),
|
||||
self.settings.pair_address(),
|
||||
request_started_at,
|
||||
received_at,
|
||||
);
|
||||
return parse_response(document.as_bytes(), self.settings.common().provider_id().clone(), pair_address, request_started_at, received_at);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/tests/public_api.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -134,7 +134,7 @@ fn public_pre_006_jupiter_and_dexscreener_settings_and_adapters_are_available_fr
|
||||
};
|
||||
assert_eq!(jupiter_provider.descriptor().id().as_str(), "jupiter");
|
||||
assert_eq!(jupiter_provider.descriptor().semantics(), ksp_offchain_transport_lib::MarketPriceSemantics::SolanaHeuristic);
|
||||
let dexscreener = match ksp_offchain_transport_lib::MarketPriceDexScreenerSettings::new(false, "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE") {
|
||||
let dexscreener = match ksp_offchain_transport_lib::MarketPriceDexScreenerSettings::new(false, std::option::Option::None) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_dexscreener.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
const TEST_PAIR: &str = "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE";
|
||||
|
||||
@@ -35,13 +35,26 @@ fn dexscreener_fixture_validates_chain_pair_and_sol_identity_and_maps_exact_pair
|
||||
|
||||
#[test]
|
||||
fn dexscreener_settings_and_descriptor_keep_pair_explicit_and_pair_endpoint_rate_limited() -> ksp_core_lib::Result<()> {
|
||||
assert!(crate::MarketPriceDexScreenerSettings::new(true, "not-a-pubkey").is_err());
|
||||
let settings = match crate::MarketPriceDexScreenerSettings::new(true, TEST_PAIR) {
|
||||
let disabled = crate::MarketPriceDexScreenerSettings::new(false, std::option::Option::None);
|
||||
assert!(disabled.is_ok(), "disabled DexScreener settings must not require a fake pair address: {disabled:?}");
|
||||
assert!(crate::MarketPriceDexScreenerSettings::new(true, std::option::Option::Some("not-a-pubkey".to_owned())).is_err());
|
||||
let settings = match crate::MarketPriceDexScreenerSettings::new(true, std::option::Option::Some(TEST_PAIR.to_owned())) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
assert_eq!(settings.pair_address().to_string(), TEST_PAIR);
|
||||
let request = match super::build_request(settings.pair_address()) {
|
||||
let pair_address = settings.pair_address();
|
||||
assert!(pair_address.is_some(), "enabled DexScreener settings should retain the configured pair");
|
||||
let pair_address = match pair_address {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(ksp_core_lib::Error::new(
|
||||
crate::ERROR_CODE_MARKET_PRICE_PROVIDER_SETTINGS_INVALID,
|
||||
"enabled DexScreener settings lost their pair address",
|
||||
));
|
||||
},
|
||||
};
|
||||
assert_eq!(pair_address.to_string(), TEST_PAIR);
|
||||
let request = match super::build_request(pair_address) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_registry.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn registry_orders_all_eight_v1_providers_and_exposes_only_generic_entries() -> ksp_core_lib::Result<()> {
|
||||
@@ -91,7 +91,7 @@ fn all_v1_entries() -> ksp_core_lib::Result<std::vec::Vec<crate::MarketPriceProv
|
||||
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") {
|
||||
let dexscreener_settings = match crate::MarketPriceDexScreenerSettings::new(false, std::option::Option::None) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_service.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn service_registry_orders_all_eight_v1_setups_and_keeps_runtime_projection_generic() -> ksp_core_lib::Result<()> {
|
||||
@@ -118,7 +118,7 @@ fn all_disabled_setups() -> ksp_core_lib::Result<std::vec::Vec<crate::MarketPric
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let dexscreener = match crate::MarketPriceDexScreenerSettings::new(false, "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE") {
|
||||
let dexscreener = match crate::MarketPriceDexScreenerSettings::new(false, std::option::Option::None) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user