v0.2.11-pre.009

This commit is contained in:
2026-08-26 11:23:01 +02:00
parent 2fdff17651
commit 69294a153f
27 changed files with 1864 additions and 133 deletions

View File

@@ -1,5 +1,5 @@
# file: .env.example
# version: 9
# version: 10
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
@@ -50,6 +50,26 @@ KSP_PUBLIC_SOLANA_MAINNET_WS_URL=wss://api.mainnet-beta.solana.com
# Keep the real credential only in the process environment or local .env; never commit it.
# KSP_SECRET_HELIUS_API_KEY=replace-me
# Birdeye Standard API key used by the optional `all_free` profile in config/std.offchain_transport.json.
# Keep the real credential only in the process environment or local .env; never commit it.
# KSP_SECRET_BIRDEYE_API_KEY=replace-me
# CoinGecko Demo API key used by the optional `all_free` Off-chain Transport profile.
# Keep the real credential only in the process environment or local .env; never commit it.
# KSP_SECRET_COINGECKO_DEMO_API_KEY=replace-me
# CoinMarketCap Basic API key used by the optional `all_free` Off-chain Transport profile.
# Keep the real credential only in the process environment or local .env; never commit it.
# KSP_SECRET_COINMARKETCAP_API_KEY=replace-me
# Public Solana pair address used by DexScreener for explicit SOL/USD pair lookup in the optional `all_free` profile.
# This value is intentionally public and must identify the exact pair chosen by the operator.
# KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS=replace-with-solana-pair-pubkey
# Jupiter Free API key used by the optional `all_free` Off-chain Transport profile.
# Keep the real credential only in the process environment or local .env; never commit it.
# KSP_SECRET_JUPITER_API_KEY=replace-me
# Fade-in duration in milliseconds used by the common KSP desk splash lifecycle.
KSP_DESK_SPLASH_FADE_IN_MS=300

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml
# version: 280
# version: 281
[workspace]
resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
[workspace.package]
version = "0.2.11-pre.8.fix.1"
version = "0.2.11-pre.9"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -0,0 +1,43 @@
{
"format_version": 1,
"default_profile": "custom_free",
"profiles": [
{
"profile_id": "custom_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
}
}
}
]
}

View File

@@ -0,0 +1,366 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:ksp:schema:std.offchain_transport:v1",
"title": "KSP standard Off-chain Transport configuration",
"type": "object",
"additionalProperties": false,
"required": [
"format_version",
"default_profile",
"profiles"
],
"properties": {
"format_version": {
"const": 1
},
"default_profile": {
"$ref": "#/$defs/profileId"
},
"profiles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/profile"
}
}
},
"$defs": {
"profileId": {
"type": "string",
"pattern": "^[a-z0-9][a-z0-9._-]*$"
},
"enabledOnly": {
"type": "object",
"additionalProperties": false,
"required": [
"enabled"
],
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"coingecko": {
"type": "object",
"additionalProperties": false,
"required": [
"enabled",
"access_mode"
],
"properties": {
"enabled": {
"type": "boolean"
},
"access_mode": {
"enum": [
"demo",
"keyless"
]
},
"api_key": {
"type": "string",
"minLength": 1
}
},
"allOf": [
{
"if": {
"properties": {
"access_mode": {
"const": "keyless"
}
},
"required": [
"access_mode"
]
},
"then": {
"not": {
"required": [
"api_key"
]
}
}
},
{
"if": {
"properties": {
"enabled": {
"const": true
},
"access_mode": {
"const": "demo"
}
},
"required": [
"enabled",
"access_mode"
]
},
"then": {
"required": [
"api_key"
]
}
}
]
},
"coinmarketcap": {
"type": "object",
"additionalProperties": false,
"required": [
"enabled",
"access_mode"
],
"properties": {
"enabled": {
"type": "boolean"
},
"access_mode": {
"enum": [
"basic",
"keyless"
]
},
"api_key": {
"type": "string",
"minLength": 1
}
},
"allOf": [
{
"if": {
"properties": {
"access_mode": {
"const": "keyless"
}
},
"required": [
"access_mode"
]
},
"then": {
"not": {
"required": [
"api_key"
]
}
}
},
{
"if": {
"properties": {
"enabled": {
"const": true
},
"access_mode": {
"const": "basic"
}
},
"required": [
"enabled",
"access_mode"
]
},
"then": {
"required": [
"api_key"
]
}
}
]
},
"jupiter": {
"type": "object",
"additionalProperties": false,
"required": [
"enabled",
"access_mode"
],
"properties": {
"enabled": {
"type": "boolean"
},
"access_mode": {
"enum": [
"free",
"keyless"
]
},
"api_key": {
"type": "string",
"minLength": 1
}
},
"allOf": [
{
"if": {
"properties": {
"access_mode": {
"const": "keyless"
}
},
"required": [
"access_mode"
]
},
"then": {
"not": {
"required": [
"api_key"
]
}
}
},
{
"if": {
"properties": {
"enabled": {
"const": true
},
"access_mode": {
"const": "free"
}
},
"required": [
"enabled",
"access_mode"
]
},
"then": {
"required": [
"api_key"
]
}
}
]
},
"birdeye": {
"type": "object",
"additionalProperties": false,
"required": [
"enabled"
],
"properties": {
"enabled": {
"type": "boolean"
},
"api_key": {
"type": "string",
"minLength": 1
}
},
"allOf": [
{
"if": {
"properties": {
"enabled": {
"const": true
}
},
"required": [
"enabled"
]
},
"then": {
"required": [
"api_key"
]
}
}
]
},
"dexscreener": {
"type": "object",
"additionalProperties": false,
"required": [
"enabled"
],
"properties": {
"enabled": {
"type": "boolean"
},
"sol_usd_pair_address": {
"type": "string",
"minLength": 1
}
},
"allOf": [
{
"if": {
"properties": {
"enabled": {
"const": true
}
},
"required": [
"enabled"
]
},
"then": {
"required": [
"sol_usd_pair_address"
]
}
}
]
},
"marketPrice": {
"type": "object",
"additionalProperties": false,
"required": [
"birdeye",
"coinbase_exchange",
"coingecko",
"coinmarketcap",
"coinpaprika",
"dexscreener",
"jupiter",
"kraken"
],
"properties": {
"birdeye": {
"$ref": "#/$defs/birdeye"
},
"coinbase_exchange": {
"$ref": "#/$defs/enabledOnly"
},
"coingecko": {
"$ref": "#/$defs/coingecko"
},
"coinmarketcap": {
"$ref": "#/$defs/coinmarketcap"
},
"coinpaprika": {
"$ref": "#/$defs/enabledOnly"
},
"dexscreener": {
"$ref": "#/$defs/dexscreener"
},
"jupiter": {
"$ref": "#/$defs/jupiter"
},
"kraken": {
"$ref": "#/$defs/enabledOnly"
}
}
},
"profile": {
"type": "object",
"additionalProperties": false,
"required": [
"profile_id",
"market_price"
],
"properties": {
"profile_id": {
"$ref": "#/$defs/profileId"
},
"market_price": {
"$ref": "#/$defs/marketPrice"
}
}
}
}
}

View File

@@ -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
}
}
}
]
}

View File

@@ -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"
}

View File

@@ -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"),

View File

@@ -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"
}

View File

@@ -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));

View File

@@ -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");

View File

@@ -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

View File

@@ -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.

View 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", &registry)
.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;

View File

@@ -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.

View File

@@ -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\"",

View File

@@ -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;

View File

@@ -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
}
}
}
]
}

View 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(&registry, "birdeye", ksp_offchain_transport_lib::MarketPriceProviderAvailability::Disabled);
assert_provider_availability(&registry, "dexscreener", ksp_offchain_transport_lib::MarketPriceProviderAvailability::Disabled);
for provider_id in ["coinbase_exchange", "coingecko", "coinmarketcap", "coinpaprika", "jupiter", "kraken"] {
assert_provider_availability(&registry, 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("../..");
}

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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),
};

View File

@@ -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),
};

View File

@@ -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),
};

View File

@@ -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),
};

240
deltas/0.2.11/pre.009.md Normal file
View File

@@ -0,0 +1,240 @@
<!-- file: deltas/0.2.11/pre.009.md -->
<!-- version: 1 -->
# Delta `0.2.11-pre.009` — Config Off-chain Transport
## 1. Base requise
Cette tranche s'applique exclusivement après `0.2.11-pre.008-fix.001` validé.
Version Cargo attendue à l'entrée :
```text
0.2.11-pre.8.fix.1
```
Version Cargo de sortie :
```text
0.2.11-pre.9
```
Le gate opérateur de la base est intégralement PASS : audits Rust/Markdown, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, tests ciblés Off-chain puis `cargo test --workspace`.
## 2. Scope réalisé
`pre.009` matérialise la frontière Config -> Off-chain Transport sans modifier les contrats réseau des huit providers V1.
La tranche ajoute :
```text
cfg.std.offchain_transport
schema.std.offchain_transport
std.offchain_transport.json format_version 1
std.offchain_transport.schema.json
exemple et fixture Config
ResolvedOffchainTransportConfig
adapter Config -> MarketPriceService
variables d'environnement documentées pour les credentials/pair DexScreener
ressources desktop nécessaires au packaged runtime
```
Elle n'ajoute pas :
```text
nouveau provider
nouvel endpoint provider
SDK provider
URL provider configurable
rate limit librement configurable
scheduler périodique
fallback / consensus / agrégation
smoke live
```
## 3. Structure durable du document
Le document Off-chain Transport reste générique et place la capacité actuelle sous :
```text
market_price
```
Deux profils versionnés sont fournis :
```text
public_keyless
all_free
```
`public_keyless` active uniquement les modes réellement utilisables sans secret. Birdeye et DexScreener y restent désactivés.
`all_free` active les modes gratuits nécessitant une clé et DexScreener avec une paire SOL/USD explicite.
Les huit branches `market_price` sont obligatoires dans le schema afin que l'inventaire V1 reste explicite et déterministe.
## 4. Capabilities provider et secrets
Le schema conditionne les credentials selon les capacités :
```text
CoinGecko keyless -> api_key interdite
CoinGecko demo activé -> api_key requise
CoinMarketCap keyless -> api_key interdite
CoinMarketCap basic activé -> api_key requise
Jupiter keyless -> api_key interdite
Jupiter free activé -> api_key requise
Birdeye activé -> api_key requise
DexScreener activé -> sol_usd_pair_address requise
```
L'adapter effectif ajoute une contrainte de provenance que le JSON Schema ne peut pas exprimer : toute API key provider doit provenir d'une variable classée Secret par Config. Un littéral credential ou une variable d'environnement non secret sont rejetés sans recopier la valeur dans l'erreur.
Variables versionnées dans `.env.example` :
```text
KSP_SECRET_BIRDEYE_API_KEY
KSP_SECRET_COINGECKO_DEMO_API_KEY
KSP_SECRET_COINMARKETCAP_API_KEY
KSP_SECRET_JUPITER_API_KEY
KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS
```
La paire DexScreener est publique. Lorsqu'elle provient de l'environnement, l'adapter exige une provenance `Public` ; un namespace Secret/Internal est rejeté.
## 5. Adapter Config -> runtime
`ResolvedOffchainTransportConfig` conserve :
```text
file_id logique
profile_id
selection_source
source_path
ResolvedConfigJson effectif et safe
MarketPriceService construit
```
`ConfigDocumentEngine` expose :
```text
load_resolved_offchain_transport_config
resolve_offchain_transport_config_profile
```
Le mapping utilise les settings publics des huit adapters et construit `MarketPriceProviderSetup`, puis `MarketPriceService`. Après cette composition, le consumer runtime reste provider-agnostic.
`ksp-offchain-transport-lib` ne dépend jamais de `ksp-config-lib` et ne lit pas directement l'environnement.
## 6. Réconciliation DexScreener capability-aware
Le contrat de plan exigeait `sol_usd_pair_address` uniquement si DexScreener est activé. La construction runtime demandait encore une paire même lorsque le provider était désactivé.
`MarketPriceDexScreenerSettings` est donc réconcilié :
```text
enabled = true + pair absente -> erreur
pair présente mais invalide -> erreur
enabled = false + pair absente -> valide
```
Cette évolution évite de fabriquer une fausse adresse de paire uniquement pour conserver DexScreener dans le registry générique. Elle ne change ni l'endpoint, ni les validations chain/pair/WSOL, ni le parsing `priceUsd` lorsque le provider est actif.
## 7. Registry Config et packaged runtime
Le registry Config enregistre désormais dix descriptors et ajoute le document/schema Off-chain Transport avec leurs file ids durables.
Les deux applications desktop existantes embarquent également :
```text
config/std.offchain_transport.json
config/schemas/std.offchain_transport.schema.json
```
Cette inclusion est nécessaire parce que `prepare_packaged_runtime` synchronise l'intégralité du registry Config, même si Wallet Desk et Config Desk ne consomment pas directement le service de prix dans cette release.
## 8. Tests et canaries ajoutés
Les nouveaux tests couvrent notamment :
```text
registry document/schema Off-chain Transport
profil public_keyless -> huit providers, Birdeye/Dex disabled
profil all_free -> huit providers Ready avec environnement complet
redaction des quatre credential canaries
rejet d'un credential provider littéral
rejet d'une paire DexScreener provenant d'un namespace Secret
DexScreener disabled sans paire
façade Config publique depuis crate root
ownership Off-chain -> Config interdit
inventaire .env.example
ressources packaged desktop mises à jour
```
## 9. Fichiers ajoutés
```text
config/examples/std.offchain_transport.example.json
config/schemas/std.offchain_transport.schema.json
config/std.offchain_transport.json
crates/ksp-config-lib/src/offchain_transport.rs
crates/ksp-config-lib/unit_tests/fixtures/std.offchain_transport.json
crates/ksp-config-lib/unit_tests/offchain_transport.rs
deltas/0.2.11/pre.009.md
```
## 10. Fichiers modifiés
```text
.env.example
Cargo.toml
crates/ksp-app-config-desk/tauri.conf.json
crates/ksp-app-config-desk/tests/desktop_contract.rs
crates/ksp-app-wallet-desk/tauri.conf.json
crates/ksp-app-wallet-desk/tests/desktop_contract.rs
crates/ksp-app-wallet-desk/tests/release_compliance.rs
crates/ksp-config-lib/Cargo.toml
crates/ksp-config-lib/src/lib.rs
crates/ksp-config-lib/src/registry.rs
crates/ksp-config-lib/tests/ownership.rs
crates/ksp-config-lib/tests/public_api.rs
crates/ksp-config-lib/unit_tests/registry.rs
crates/ksp-offchain-transport-lib/src/market_price_dexscreener.rs
crates/ksp-offchain-transport-lib/tests/public_api.rs
crates/ksp-offchain-transport-lib/unit_tests/market_price_dexscreener.rs
crates/ksp-offchain-transport-lib/unit_tests/market_price_registry.rs
crates/ksp-offchain-transport-lib/unit_tests/market_price_service.rs
docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md
docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md
```
## 11. Audits sandbox
Contrôles disponibles à la construction de la tranche :
```text
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean (123 tables / 115 files)
JSON syntax: document, exemple, schema, fixture et Tauri configs PASS
JSON Schema Draft 2020-12: document, exemple et fixture PASS
schema conditionals: keyless credential interdit, keyed credential requis si activé, Birdeye key requis si activé, Dex pair requis si activé PASS
```
Cargo n'est pas disponible dans cet environnement ; les validations compilation/Clippy/tests restent donc opérateur.
## 12. Gate opérateur requis
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.11
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-config-lib
cargo test -p ksp-offchain-transport-lib
cargo test --workspace
```
`pre.010` ne doit commencer qu'après ce gate propre ou un delta fix explicite.

View File

@@ -1,9 +1,9 @@
<!-- file: docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md -->
<!-- version: 15 -->
<!-- version: 16 -->
# Plan `0.2.11` — Off-chain price transport SOL/USD multi-provider
**Statut courant : `0.2.11-pre.008-fix.001` corrige le gate de `pre.008` sans modifier son contrat fonctionnel. `MarketPriceService` possède toujours le dispatch provider, le registry runtime et les transitions d'availability ; les consumers utilisent uniquement les provider ids opaques, `registry()`, `refresh`, `refresh_many` et `refresh_all`. Le fix supprime un warning Clippy `collapsible_if` et remplace un canari lexical trop large sur le mot `consensus` par des vérifications de surfaces exécutables réellement interdites. `MarketPriceProviderSetup` reste la frontière provider-specific destinée à la composition/Config de `pre.009`.**
**Statut courant : `0.2.11-pre.009` matérialise la frontière Config -> Off-chain Transport. `cfg.std.offchain_transport` et `schema.std.offchain_transport` possèdent désormais un domaine `market_price` avec les huit providers V1, des profils `public_keyless` et `all_free`, les credentials résolus exclusivement depuis une provenance Config classée Secret, et une adresse de paire DexScreener publique lorsqu'elle provient de l'environnement. `ResolvedOffchainTransportConfig` construit `MarketPriceService` sans créer de dépendance inverse depuis Off-chain Transport. Les limites provider restent possédées par la crate runtime et ne deviennent pas des cadences libres de Config.**
## 1. Base et autorité
@@ -520,28 +520,43 @@ Le limiter `pre.003` est non bloquant. Une cadence fixe est matérialisée par u
## 15. Config provider-capability-aware
Config reste l'unique propriétaire des documents/env/secrets.
Off-chain Transport expose ses settings publics ; Config adapte un futur document standard dédié, pressenti :
Config reste l'unique propriétaire des documents, de l'environnement et des secrets. `pre.009` matérialise les identifiants durables :
```text
file_id = std.offchain_transport
file_id document = cfg.std.offchain_transport
file_id schema = schema.std.offchain_transport
fichier = std.offchain_transport.json
schema = std.offchain_transport.schema.json
format_version = 1
```
Les branches de configuration correspondent aux capacités réelles de chaque provider.
Le document reste générique Off-chain Transport et place la capacité actuelle sous `market_price`. Cette structure évite de transformer le document durable en Config dédiée uniquement au prix lorsque d'autres familles telles que `swap_quote` apparaîtront.
| Provider | Champs spécifiques V1 attendus |
|-------------------|------------------------------------------------------------|
| CoinGecko | enabled, access_mode keyless ou demo, api_key optionnelle |
| CoinMarketCap | enabled, access_mode keyless ou basic, api_key optionnelle |
| CoinPaprika | enabled |
| Kraken | enabled |
| Coinbase Exchange | enabled |
| Jupiter | enabled, access_mode keyless ou free, api_key optionnelle |
| Birdeye | enabled, api_key requise si activé |
| DexScreener | enabled, sol_usd_pair_address requise si activé |
Deux profils versionnés servent de canaries :
Le schema final doit conditionner la présence des credentials au mode choisi. Aucun secret n'est versionné. Toute variable d'environnement ajoutée respecte `KSP_SECRET_*` et est inventoriée dans `.env.example`.
```text
public_keyless = providers réellement utilisables sans secret ; Birdeye et DexScreener désactivés
all_free = modes gratuits à clé activés + paire DexScreener explicite
```
Les branches correspondent aux capacités réelles de chaque provider.
| Provider | Champs spécifiques V1 |
|-------------------|---------------------------------------------------------------|
| CoinGecko | enabled, access_mode keyless ou demo, api_key conditionnelle |
| CoinMarketCap | enabled, access_mode keyless ou basic, api_key conditionnelle |
| CoinPaprika | enabled |
| Kraken | enabled |
| Coinbase Exchange | enabled |
| Jupiter | enabled, access_mode keyless ou free, api_key conditionnelle |
| Birdeye | enabled, api_key requise si activé |
| DexScreener | enabled, sol_usd_pair_address requise si activé |
Le schema conditionne les credentials au mode choisi et interdit un faux champ `api_key` sur les modes keyless. L'adapter effectif applique en plus la provenance : une API key provider doit provenir d'une variable classée Secret (`KSP_SECRET_*` ou namespace KSPB secret admis par Config), même si un littéral non vide est syntaxiquement valide JSON. Les erreurs et `Debug` ne recopient jamais le credential.
La paire DexScreener n'est pas un secret. Un littéral validé comme Pubkey Solana est admis ; si la valeur vient de l'environnement, sa provenance doit rester Public, avec le canari versionné `KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS`. Pour respecter « requise seulement si activé », `MarketPriceDexScreenerSettings` accepte désormais l'absence de paire lorsque le provider est désactivé et la refuse toujours lorsqu'il est activé.
`.env.example` inventorie les variables nécessaires sans valeur réelle. Les applications desktop empaquetent aussi le nouveau document et son schema parce que `prepare_packaged_runtime` synchronise l'intégralité du registry Config.
Les limites provider ne deviennent pas des valeurs libres permettant à Config/UI d'augmenter la cadence. Une future policy pourra permettre un throttle **plus strict** si un besoin réel apparaît.
@@ -736,7 +751,7 @@ Implémentation des deux adapters exchange keyless avec DTOs wire privés et end
**Statut : réalisé, gate opérateur ciblé PASS ; workspace complet non rejoué.**
Implémentation des adapters Jupiter Price V3 et DexScreener avec DTOs wire privés et endpoints HTTPS officiels fixes. Jupiter distingue keyless à 0,5 requête par seconde et Free key à 1 requête par seconde sur le même `api.jup.ag`, utilise uniquement le mint WSOL et conserve `blockId` comme provenance de récence sans le convertir en timestamp. DexScreener exige un pair address Solana explicite dans ses settings, appelle uniquement `/latest/dex/pairs/solana/{pairId}`, vérifie `chainId = solana`, le pair address retourné et le mint WSOL comme base avant de lire `priceUsd`. Aucune découverte de pool, aucun tri de liquidité, aucun consensus et aucun registry global ne sont avancés.
Implémentation des adapters Jupiter Price V3 et DexScreener avec DTOs wire privés et endpoints HTTPS officiels fixes. Jupiter distingue keyless à 0,5 requête par seconde et Free key à 1 requête par seconde sur le même `api.jup.ag`, utilise uniquement le mint WSOL et conserve `blockId` comme provenance de récence sans le convertir en timestamp. DexScreener exige un pair address Solana explicite lorsqu'il est activé, appelle uniquement `/latest/dex/pairs/solana/{pairId}`, vérifie `chainId = solana`, le pair address retourné et le mint WSOL comme base avant de lire `priceUsd`. Aucune découverte de pool, aucun tri de liquidité, aucun consensus et aucun registry global ne sont avancés.
### `pre.007` — Birdeye, registry et availability
@@ -754,9 +769,9 @@ Implémentation de Birdeye Standard avec `X-API-KEY`, endpoint Price Single Sola
### `pre.009` — Config Off-chain Transport
**Statut : planifié.**
**Statut : réalisé ; gate Cargo opérateur à rejouer.**
Document standard `std.offchain_transport`, schema, exemples, secrets d'environnement nécessaires et adapter Config -> Off-chain Transport conforme aux capacités de chaque provider.
Ajout de `cfg.std.offchain_transport` / `schema.std.offchain_transport`, du document standard V1, de son schema strict, d'un exemple et de la fixture Config. Le domaine `market_price` configure les huit providers sans rendre leurs cadences modifiables. `public_keyless` fonctionne sans secret ; `all_free` référence quatre credentials `KSP_SECRET_*` et une paire DexScreener `KSP_PUBLIC_*`. `ResolvedOffchainTransportConfig` résout l'environnement, contrôle la provenance des credentials/public fields et construit `MarketPriceService` via `MarketPriceProviderSetup`, sans dépendance inverse. Les deux applications desktop embarquent les nouvelles ressources Config. La tranche réconcilie aussi DexScreener pour qu'une paire soit requise uniquement quand le provider est activé.
### `pre.010` — Hardening et complétude technique

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md -->
<!-- version: 14 -->
<!-- version: 15 -->
# Validation `0.2.11` — Off-chain price transport SOL/USD
@@ -261,30 +261,57 @@ Les tests déterministes de `pre.007` couvrent l'auth Birdeye et sa redaction, l
## 3.11 Gate `0.2.11-pre.008`
| Critère | Statut | Preuve |
|-------------------------------------------------------------------------|---------|--------------------------------------------|
| version workspace `0.2.11-pre.8` | PASS | `Cargo.toml` racine |
| `MarketPriceService` provider-agnostic public | PASS | `market_price_service.rs` |
| setup provider-specific limité à la composition | PASS | `MarketPriceProviderSetup` |
| registry détachable sans dispatch provider | PASS | `MarketPriceService::registry` |
| refresh individuel par `MarketPriceProviderId` | PASS | `MarketPriceService::refresh` |
| refresh multiple par liste d'identifiants opaques | PASS | `MarketPriceService::refresh_many` |
| refresh all en ordre provider déterministe | PASS | `MarketPriceService::refresh_all` |
| doublons / provider inconnu rejetés avant dispatch | PASS | tests `market_price_service` |
| provider non éligible projeté sans requête réseau | PASS | eligibility + test huit providers disabled |
| `429` / defer local projetés en `CoolingDown` | PASS | classification générique |
| `401/403` keyed projetés en `AuthenticationUnavailable` | PASS | classification auth-aware |
| transport temporaire projeté sans rendre le service global inutilisable | PASS | outcome provider-neutral |
| aucune attente/sleep de rate limit dans le service | PASS | dependency boundary |
| aucun fallback, consensus ou agrégation multi-provider | PASS | scope + dependency boundary |
| Config, scheduling périodique et smoke live non avancés | PASS | scope réservé à `pre.009+` |
| audit Rust sandbox | PASS | clean, 0 candidate export |
| audit Markdown sandbox | PASS | clean, 122 tables / 113 fichiers |
| gate `pre.008` brut | FAIL | 1 warning Clippy + 1 canari boundary FAIL |
| fix `pre.008-fix.001` statique | PASS | audits Rust/Markdown sandbox |
| gate Cargo du fix | PLANNED | à exécuter par l'opérateur |
| Critère | Statut | Preuve |
|-------------------------------------------------------------------------|--------|--------------------------------------------|
| version workspace `0.2.11-pre.8` | PASS | `Cargo.toml` racine |
| `MarketPriceService` provider-agnostic public | PASS | `market_price_service.rs` |
| setup provider-specific limité à la composition | PASS | `MarketPriceProviderSetup` |
| registry détachable sans dispatch provider | PASS | `MarketPriceService::registry` |
| refresh individuel par `MarketPriceProviderId` | PASS | `MarketPriceService::refresh` |
| refresh multiple par liste d'identifiants opaques | PASS | `MarketPriceService::refresh_many` |
| refresh all en ordre provider déterministe | PASS | `MarketPriceService::refresh_all` |
| doublons / provider inconnu rejetés avant dispatch | PASS | tests `market_price_service` |
| provider non éligible projeté sans requête réseau | PASS | eligibility + test huit providers disabled |
| `429` / defer local projetés en `CoolingDown` | PASS | classification générique |
| `401/403` keyed projetés en `AuthenticationUnavailable` | PASS | classification auth-aware |
| transport temporaire projeté sans rendre le service global inutilisable | PASS | outcome provider-neutral |
| aucune attente/sleep de rate limit dans le service | PASS | dependency boundary |
| aucun fallback, consensus ou agrégation multi-provider | PASS | scope + dependency boundary |
| Config, scheduling périodique et smoke live non avancés | PASS | scope réservé à `pre.009+` |
| audit Rust sandbox | PASS | clean, 0 candidate export |
| audit Markdown sandbox | PASS | clean, 122 tables / 113 fichiers |
| gate `pre.008` brut | FAIL | 1 warning Clippy + 1 canari boundary FAIL |
| fix `pre.008-fix.001` statique | PASS | audits Rust/Markdown sandbox |
| gate Cargo du fix | PASS | gate opérateur complet du 2026-08-26 |
Les tests déterministes de `pre.008` vérifient le registry de service sur les huit setups V1, `refresh_all` sans réseau lorsque tous les providers sont désactivés, le rejet pré-dispatch des doublons/providers inconnus, les classes auth/rate-limit/temporaire/local misconfiguration et l'éligibilité temporelle des cooldowns. Les méthodes multiples restent séquentielles et déterministes dans V1 ; elles ne dorment jamais pour attendre un provider et ne font aucun fallback/consensus. Le gate opérateur initial a toutefois trouvé un warning `clippy::collapsible_if` et un canari boundary trop large (`!service.contains("consensus")`) qui échouait sur la rustdoc décrivant justement l'absence de consensus. `pre.008-fix.001` corrige ces deux points sans modifier le comportement runtime.
Les tests déterministes de `pre.008` vérifient le registry de service sur les huit setups V1, `refresh_all` sans réseau lorsque tous les providers sont désactivés, le rejet pré-dispatch des doublons/providers inconnus, les classes auth/rate-limit/temporaire/local misconfiguration et l'éligibilité temporelle des cooldowns. Les méthodes multiples restent séquentielles et déterministes dans V1 ; elles ne dorment jamais pour attendre un provider et ne font aucun fallback/consensus. Le gate opérateur initial a toutefois trouvé un warning `clippy::collapsible_if` et un canari boundary trop large (`!service.contains("consensus")`) qui échouait sur la rustdoc décrivant justement l'absence de consensus. `pre.008-fix.001` corrige ces deux points sans modifier le comportement runtime. Le gate rejoué sur `0.2.11-pre.8.fix.1` est intégralement PASS : audits Rust/Markdown, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, 54 tests unitaires + 6 boundary + 7 public API Off-chain, puis `cargo test --workspace` avec uniquement les smokes/timing explicitement ignorés.
## 3.12 Gate `0.2.11-pre.009`
| Critère | Statut | Preuve |
|----------------------------------------------------------------------|---------|----------------------------------------------------------------|
| version workspace `0.2.11-pre.9` | PASS | `Cargo.toml` racine |
| document `cfg.std.offchain_transport` enregistré | PASS | registry Config + `std.offchain_transport.json` |
| schema `schema.std.offchain_transport` enregistré | PASS | registry Config + schema Draft 2020-12 |
| domaine actuel imbriqué sous `market_price` | PASS | document/schema V1 |
| profils `public_keyless` et `all_free` versionnés | PASS | document standard + exemple |
| huit branches provider Config obligatoires | PASS | schema + fixture |
| credentials conditionnés par mode/provider | PASS | `if/then` du schema |
| credentials effectifs exigent une provenance Config Secret | PASS | adapter `offchain_transport.rs` + tests |
| littéral credential rejeté par l'adapter effectif | PASS | test `literal_or_nonsecret_provider_credentials...` |
| DexScreener pair requis seulement lorsque le provider est activé | PASS | schema + `MarketPriceDexScreenerSettings` |
| Dex pair via environnement exige une provenance Public | PASS | adapter Config + test |
| `.env.example` inventorie quatre secrets et la paire publique | PASS | `.env.example` |
| Config construit `MarketPriceService` via `MarketPriceProviderSetup` | PASS | `ResolvedOffchainTransportConfig` |
| Off-chain Transport ne dépend pas de Config | PASS | dependency ownership + manifest |
| aucune cadence provider libre ajoutée à Config | PASS | schema/document sans rate-limit override |
| Config Debug/safe view ne divulgue pas les API keys | PASS | tests adapter |
| applications desktop embarquent document + schema | PASS | deux `tauri.conf.json` + canaries desktop |
| audit Rust sandbox | PASS | clean, 0 candidate export |
| audit Markdown sandbox | PASS | clean, 123 tables / 115 fichiers |
| `cargo check` / Clippy / tests Config et workspace | PLANNED | gate opérateur requis après application de l'archive `pre.009` |
La tranche n'ajoute aucune URL provider configurable, aucun SDK provider et aucune lecture directe de l'environnement dans `ksp-offchain-transport-lib`. Les secrets restent résolus par `ksp-config-lib`; le service runtime reçoit uniquement les settings typés nécessaires. La modification DexScreener est capability-aware : une paire absente est valide uniquement lorsque l'adapter est désactivé, ce qui permet de conserver son entrée dans l'inventaire générique sans fabriquer de fausse configuration.
## 4. Matrice provider prévue
@@ -374,32 +401,32 @@ Avant la stable, les conditions d'usage et la persistance des offres gratuites d
## 9. DexScreener V1
| Invariant | Statut |
|----------------------------------------------|--------|
| pair address SOL/USD explicite dans settings | PASS |
| `chainId = solana` vérifié | PASS |
| identité SOL de la paire vérifiée | PASS |
| `priceUsd` utilisé comme prix commun | PASS |
| aucune recherche automatique de pool | PASS |
| aucun tri automatique par liquidité | PASS |
| aucune moyenne/consensus entre pools | PASS |
| Invariant | Statut |
|--------------------------------------------------|--------|
| pair address SOL/USD explicite si provider actif | PASS |
| `chainId = solana` vérifié | PASS |
| identité SOL de la paire vérifiée | PASS |
| `priceUsd` utilisé comme prix commun | PASS |
| aucune recherche automatique de pool | PASS |
| aucun tri automatique par liquidité | PASS |
| aucune moyenne/consensus entre pools | PASS |
## 10. Config -> Off-chain Transport
| Invariant | Statut |
|------------------------------------------------------------------|---------|
| Off-chain Transport ne dépend pas de Config | PLANNED |
| document standard offchain dédié | PLANNED |
| branches provider conditionnelles dans schema | PLANNED |
| CoinGecko keyless/Demo représentés | PLANNED |
| CoinMarketCap keyless/Basic représentés | PLANNED |
| Jupiter keyless/Free représentés | PLANNED |
| Birdeye exige credential lorsqu'activé | PLANNED |
| DexScreener exige pair address lorsqu'activé | PLANNED |
| providers sans auth n'exposent pas de faux champ API key | PLANNED |
| secrets via Config uniquement | PLANNED |
| `.env.example` synchronisé pour les secrets réellement ajoutés | PLANNED |
| aucune cadence provider configurable au-dessus de la limite sûre | PLANNED |
| Invariant | Statut |
|------------------------------------------------------------------|--------|
| Off-chain Transport ne dépend pas de Config | PASS |
| document standard offchain dédié | PASS |
| branches provider conditionnelles dans schema | PASS |
| CoinGecko keyless/Demo représentés | PASS |
| CoinMarketCap keyless/Basic représentés | PASS |
| Jupiter keyless/Free représentés | PASS |
| Birdeye exige credential lorsqu'activé | PASS |
| DexScreener exige pair address lorsqu'activé | PASS |
| providers sans auth n'exposent pas de faux champ API key | PASS |
| secrets via Config uniquement | PASS |
| `.env.example` synchronisé pour les secrets réellement ajoutés | PASS |
| aucune cadence provider configurable au-dessus de la limite sûre | PASS |
## 11. Frontière future `ksp-app-solprices-desk`