v0.2.11-pre.009
This commit is contained in:
22
.env.example
22
.env.example
@@ -1,5 +1,5 @@
|
|||||||
# file: .env.example
|
# file: .env.example
|
||||||
# version: 9
|
# version: 10
|
||||||
|
|
||||||
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
|
# 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.
|
# 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.
|
# Keep the real credential only in the process environment or local .env; never commit it.
|
||||||
# KSP_SECRET_HELIUS_API_KEY=replace-me
|
# 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.
|
# Fade-in duration in milliseconds used by the common KSP desk splash lifecycle.
|
||||||
KSP_DESK_SPLASH_FADE_IN_MS=300
|
KSP_DESK_SPLASH_FADE_IN_MS=300
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 280
|
# version: 281
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
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"]
|
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]
|
[workspace.package]
|
||||||
version = "0.2.11-pre.8.fix.1"
|
version = "0.2.11-pre.9"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
43
config/examples/std.offchain_transport.example.json
Normal file
43
config/examples/std.offchain_transport.example.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
366
config/schemas/std.offchain_transport.schema.json
Normal file
366
config/schemas/std.offchain_transport.schema.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
config/std.offchain_transport.json
Normal file
75
config/std.offchain_transport.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -55,10 +55,12 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||||
"../../config/std.logging.json": "config/std.logging.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.transport.json": "config/std.transport.json",
|
||||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.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.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.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-config-desk/tests/desktop_contract.rs
|
// file: crates/ksp-app-config-desk/tests/desktop_contract.rs
|
||||||
// version: 7
|
// version: 8
|
||||||
|
|
||||||
//! Desktop build/shell contract audits for Config Desk.
|
//! 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);
|
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||||
assert!(resources.is_some(), "packaged Config resources map must exist");
|
assert!(resources.is_some(), "packaged Config resources map must exist");
|
||||||
if let std::option::Option::Some(resources) = resources {
|
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.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!(
|
assert_eq!(
|
||||||
resources.get("../../config/schemas/std.wallet.schema.json").and_then(serde_json::Value::as_str),
|
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"),
|
std::option::Option::Some("config/schemas/std.wallet.schema.json"),
|
||||||
|
|||||||
@@ -55,10 +55,12 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||||
"../../config/std.logging.json": "config/std.logging.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.transport.json": "config/std.transport.json",
|
||||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.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.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.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
|
// 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.
|
//! 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);
|
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");
|
assert!(resources.is_some(), "packaged Wallet Desk Config resources map must exist");
|
||||||
if let std::option::Option::Some(resources) = resources {
|
if let std::option::Option::Some(resources) = resources {
|
||||||
assert_eq!(resources.len(), 8);
|
assert_eq!(resources.len(), 10);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resources.get("../../config/composite.ksp-app-wallet-desk.json").and_then(serde_json::Value::as_str),
|
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"),
|
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),
|
resources.get("../../config/schemas/composite.schema.json").and_then(serde_json::Value::as_str),
|
||||||
std::option::Option::Some("config/schemas/composite.schema.json"),
|
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());
|
let main = read_text(root.join("frontend/main.html").as_path());
|
||||||
assert!(main.contains(packaged_version));
|
assert!(main.contains(packaged_version));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
|
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
//! Release-wide deterministic compliance canaries for Wallet Desk.
|
//! 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::Some(value) => value,
|
||||||
std::option::Option::None => return,
|
std::option::Option::None => return,
|
||||||
};
|
};
|
||||||
assert_eq!(resources.len(), 8);
|
assert_eq!(resources.len(), 10);
|
||||||
for (source, destination) in resources {
|
for (source, destination) in resources {
|
||||||
let destination = destination.as_str();
|
let destination = destination.as_str();
|
||||||
assert!(destination.is_some(), "resource destination must be textual");
|
assert!(destination.is_some(), "resource destination must be textual");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-config-lib/Cargo.toml
|
# file: crates/ksp-config-lib/Cargo.toml
|
||||||
# version: 8
|
# version: 9
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-config-lib"
|
name = "ksp-config-lib"
|
||||||
@@ -12,6 +12,7 @@ directories.workspace = true
|
|||||||
jsonschema.workspace = true
|
jsonschema.workspace = true
|
||||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||||
ksp-logging-lib = { path = "../ksp-logging-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" }
|
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/src/lib.rs
|
// file: crates/ksp-config-lib/src/lib.rs
|
||||||
// version: 18
|
// version: 19
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -21,6 +21,7 @@ mod environment;
|
|||||||
mod error;
|
mod error;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod management;
|
mod management;
|
||||||
|
mod offchain_transport;
|
||||||
mod packaging;
|
mod packaging;
|
||||||
mod persistence;
|
mod persistence;
|
||||||
mod profile;
|
mod profile;
|
||||||
@@ -127,6 +128,8 @@ pub use self::management::LoggingOutputFilterConfig;
|
|||||||
pub use self::management::LoggingProfileConfig;
|
pub use self::management::LoggingProfileConfig;
|
||||||
/// Typed source contract for one global Logging target override.
|
/// Typed source contract for one global Logging target override.
|
||||||
pub use self::management::LoggingTargetFilterConfig;
|
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.
|
/// Writable KSP runtime roots prepared from packaged Config resources.
|
||||||
pub use self::packaging::PackagedRuntimeLayout;
|
pub use self::packaging::PackagedRuntimeLayout;
|
||||||
/// Prepares the shared writable KSP desktop runtime from immutable packaged resources.
|
/// 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;
|
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
|
||||||
/// Default physical filename for the standard Logging JSON Schema document.
|
/// Default physical filename for the standard Logging JSON Schema document.
|
||||||
pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
|
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.
|
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||||
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
|
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
|
||||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
/// 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;
|
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
|
||||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||||
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
|
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.
|
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
||||||
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
|
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
|
||||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||||
pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
|
pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
|
||||||
/// Logical file identifier for the standard Logging configuration document.
|
/// Logical file identifier for the standard Logging configuration document.
|
||||||
pub use self::registry::FILE_ID_STD_LOGGING;
|
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.
|
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||||
pub use self::registry::FILE_ID_STD_TRANSPORT;
|
pub use self::registry::FILE_ID_STD_TRANSPORT;
|
||||||
/// Logical file identifier for the standard Wallet configuration document.
|
/// Logical file identifier for the standard Wallet configuration document.
|
||||||
|
|||||||
499
crates/ksp-config-lib/src/offchain_transport.rs
Normal file
499
crates/ksp-config-lib/src/offchain_transport.rs
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
// file: crates/ksp-config-lib/src/offchain_transport.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Adapter from Config-owned Off-chain Transport documents to the provider-agnostic market-price runtime service.
|
||||||
|
|
||||||
|
/// Effective standard Off-chain Transport configuration resolved from Config.
|
||||||
|
pub struct ResolvedOffchainTransportConfig {
|
||||||
|
effective: crate::ResolvedConfigJson,
|
||||||
|
file_id: crate::ConfigFileId,
|
||||||
|
profile_id: String,
|
||||||
|
selection_source: crate::ConfigProfileSelectionSource,
|
||||||
|
service: ksp_offchain_transport_lib::MarketPriceService,
|
||||||
|
source_path: std::path::PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::ResolvedOffchainTransportConfig {
|
||||||
|
/// Returns the detailed environment-resolved effective Config view with secret-safe diagnostics.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn effective(&self) -> &crate::ResolvedConfigJson {
|
||||||
|
return &self.effective;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the logical Config document identifier used by this runtime configuration.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn file_id(&self) -> &crate::ConfigFileId {
|
||||||
|
return &self.file_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the selected standard Off-chain Transport profile identifier.
|
||||||
|
#[must_use]
|
||||||
|
pub fn profile_id(&self) -> &str {
|
||||||
|
return self.profile_id.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the source that selected the standard Off-chain Transport profile.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource {
|
||||||
|
return self.selection_source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the provider-agnostic market-price runtime service.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn service(&self) -> &ksp_offchain_transport_lib::MarketPriceService {
|
||||||
|
return &self.service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the physical source Config document path.
|
||||||
|
#[must_use]
|
||||||
|
pub fn source_path(&self) -> &std::path::Path {
|
||||||
|
return self.source_path.as_path();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for crate::ResolvedOffchainTransportConfig {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let registry = self.service.registry();
|
||||||
|
return formatter
|
||||||
|
.debug_struct("ResolvedOffchainTransportConfig")
|
||||||
|
.field("effective", &self.effective)
|
||||||
|
.field("file_id", &self.file_id)
|
||||||
|
.field("profile_id", &self.profile_id)
|
||||||
|
.field("provider_registry", ®istry)
|
||||||
|
.field("selection_source", &self.selection_source)
|
||||||
|
.field("source_path", &self.source_path)
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::ConfigDocumentEngine {
|
||||||
|
/// Loads the standard Off-chain Transport document and maps one profile to the generic market-price runtime service.
|
||||||
|
pub fn load_resolved_offchain_transport_config(
|
||||||
|
&self,
|
||||||
|
requested_profile: std::option::Option<&str>,
|
||||||
|
environment: &crate::ConfigEnvironment,
|
||||||
|
) -> ksp_core_lib::Result<crate::ResolvedOffchainTransportConfig> {
|
||||||
|
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||||
|
let file_id = match file_id {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let profile = self.load_resolved_profile(&file_id, requested_profile);
|
||||||
|
let profile = match profile {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return resolve_offchain_transport_profile(&profile, environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maps an already resolved standard Off-chain Transport profile to the runtime service while preserving composite selection provenance.
|
||||||
|
pub fn resolve_offchain_transport_config_profile(
|
||||||
|
&self,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
environment: &crate::ConfigEnvironment,
|
||||||
|
) -> ksp_core_lib::Result<crate::ResolvedOffchainTransportConfig> {
|
||||||
|
if profile.file_id().as_str() != crate::FILE_ID_STD_OFFCHAIN_TRANSPORT {
|
||||||
|
return std::result::Result::Err(effective_error(profile, "resolved Config profile does not reference the standard Off-chain Transport document"));
|
||||||
|
}
|
||||||
|
let descriptor = self.registry().descriptor(profile.file_id());
|
||||||
|
if let std::result::Result::Err(error) = descriptor {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
return resolve_offchain_transport_profile(profile, environment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveOffchainTransportSource {
|
||||||
|
format_version: u32,
|
||||||
|
market_price: EffectiveMarketPriceSource,
|
||||||
|
profile_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveMarketPriceSource {
|
||||||
|
birdeye: EffectiveBirdeyeSource,
|
||||||
|
coinbase_exchange: EffectiveEnabledSource,
|
||||||
|
coingecko: EffectiveCoinGeckoSource,
|
||||||
|
coinmarketcap: EffectiveCoinMarketCapSource,
|
||||||
|
coinpaprika: EffectiveEnabledSource,
|
||||||
|
dexscreener: EffectiveDexScreenerSource,
|
||||||
|
jupiter: EffectiveJupiterSource,
|
||||||
|
kraken: EffectiveEnabledSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveEnabledSource {
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveBirdeyeSource {
|
||||||
|
api_key: std::option::Option<String>,
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveCoinGeckoSource {
|
||||||
|
access_mode: ksp_offchain_transport_lib::MarketPriceCoinGeckoAccessMode,
|
||||||
|
api_key: std::option::Option<String>,
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveCoinMarketCapSource {
|
||||||
|
access_mode: ksp_offchain_transport_lib::MarketPriceCoinMarketCapAccessMode,
|
||||||
|
api_key: std::option::Option<String>,
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveDexScreenerSource {
|
||||||
|
enabled: bool,
|
||||||
|
sol_usd_pair_address: std::option::Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveJupiterSource {
|
||||||
|
access_mode: ksp_offchain_transport_lib::MarketPriceJupiterAccessMode,
|
||||||
|
api_key: std::option::Option<String>,
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_offchain_transport_profile(
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
environment: &crate::ConfigEnvironment,
|
||||||
|
) -> ksp_core_lib::Result<crate::ResolvedOffchainTransportConfig> {
|
||||||
|
ksp_logging_lib::trace!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
profile_id = profile.profile_id(),
|
||||||
|
"mapping standard Off-chain Transport Config profile"
|
||||||
|
);
|
||||||
|
let effective = profile.resolve_effective_environment_detailed(environment);
|
||||||
|
let effective = match effective {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let source = serde_json::from_value::<EffectiveOffchainTransportSource>(effective.value().clone());
|
||||||
|
let source = match source {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
effective_error(profile, "effective Off-chain Transport Config cannot be decoded into the runtime adapter contract").with_source(error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if source.format_version != 1 {
|
||||||
|
return std::result::Result::Err(effective_error(profile, "effective Off-chain Transport format_version is unsupported"));
|
||||||
|
}
|
||||||
|
if source.profile_id != profile.profile_id() {
|
||||||
|
return std::result::Result::Err(effective_error(profile, "effective Off-chain Transport profile_id does not match the selected profile"));
|
||||||
|
}
|
||||||
|
let setups = map_market_price_setups(source.market_price, &effective, profile);
|
||||||
|
let setups = match setups {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let service = ksp_offchain_transport_lib::MarketPriceService::new(setups);
|
||||||
|
let service = match service {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(offchain_contract_error(
|
||||||
|
profile,
|
||||||
|
"effective market-price providers fail the Off-chain Transport runtime contract",
|
||||||
|
&error,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let provider_count = service.registry().entries().len();
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
profile_id = profile.profile_id(),
|
||||||
|
format_version = source.format_version,
|
||||||
|
provider_count,
|
||||||
|
"mapped standard Off-chain Transport Config to market-price service"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(crate::ResolvedOffchainTransportConfig {
|
||||||
|
effective,
|
||||||
|
file_id: profile.file_id().clone(),
|
||||||
|
profile_id: profile.profile_id().to_owned(),
|
||||||
|
selection_source: profile.selection_source(),
|
||||||
|
service,
|
||||||
|
source_path: profile.path().to_path_buf(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_market_price_setups(
|
||||||
|
source: EffectiveMarketPriceSource,
|
||||||
|
effective: &crate::ResolvedConfigJson,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
) -> ksp_core_lib::Result<std::vec::Vec<ksp_offchain_transport_lib::MarketPriceProviderSetup>> {
|
||||||
|
let birdeye = map_birdeye(source.birdeye, effective, profile);
|
||||||
|
let birdeye = match birdeye {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let coinbase_exchange = ksp_offchain_transport_lib::MarketPriceCoinbaseExchangeSettings::new(source.coinbase_exchange.enabled);
|
||||||
|
let coinbase_exchange = match coinbase_exchange {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(provider_contract_error(profile, "coinbase_exchange", &error)),
|
||||||
|
};
|
||||||
|
let coingecko = map_coingecko(source.coingecko, effective, profile);
|
||||||
|
let coingecko = match coingecko {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let coinmarketcap = map_coinmarketcap(source.coinmarketcap, effective, profile);
|
||||||
|
let coinmarketcap = match coinmarketcap {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let coinpaprika = ksp_offchain_transport_lib::MarketPriceCoinPaprikaSettings::new(source.coinpaprika.enabled);
|
||||||
|
let coinpaprika = match coinpaprika {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(provider_contract_error(profile, "coinpaprika", &error)),
|
||||||
|
};
|
||||||
|
let dexscreener = map_dexscreener(source.dexscreener, effective, profile);
|
||||||
|
let dexscreener = match dexscreener {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let jupiter = map_jupiter(source.jupiter, effective, profile);
|
||||||
|
let jupiter = match jupiter {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let kraken = ksp_offchain_transport_lib::MarketPriceKrakenSettings::new(source.kraken.enabled);
|
||||||
|
let kraken = match kraken {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(provider_contract_error(profile, "kraken", &error)),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(std::vec![
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderSetup::Birdeye(birdeye),
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinbaseExchange(coinbase_exchange),
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinGecko(coingecko),
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinMarketCap(coinmarketcap),
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderSetup::CoinPaprika(coinpaprika),
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderSetup::DexScreener(dexscreener),
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderSetup::Jupiter(jupiter),
|
||||||
|
ksp_offchain_transport_lib::MarketPriceProviderSetup::Kraken(kraken),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_birdeye(
|
||||||
|
source: EffectiveBirdeyeSource,
|
||||||
|
effective: &crate::ResolvedConfigJson,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
) -> ksp_core_lib::Result<ksp_offchain_transport_lib::MarketPriceBirdeyeSettings> {
|
||||||
|
if source.api_key.is_some()
|
||||||
|
&& let std::result::Result::Err(error) = validate_secret_field_provenance(effective, "/market_price/birdeye/api_key", profile, "birdeye")
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let settings = ksp_offchain_transport_lib::MarketPriceBirdeyeSettings::new(source.enabled, source.api_key);
|
||||||
|
return match settings {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(provider_contract_error(profile, "birdeye", &error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_coingecko(
|
||||||
|
source: EffectiveCoinGeckoSource,
|
||||||
|
effective: &crate::ResolvedConfigJson,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
) -> ksp_core_lib::Result<ksp_offchain_transport_lib::MarketPriceCoinGeckoSettings> {
|
||||||
|
if source.api_key.is_some()
|
||||||
|
&& let std::result::Result::Err(error) = validate_secret_field_provenance(effective, "/market_price/coingecko/api_key", profile, "coingecko")
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let settings = match source.access_mode {
|
||||||
|
ksp_offchain_transport_lib::MarketPriceCoinGeckoAccessMode::Demo => {
|
||||||
|
ksp_offchain_transport_lib::MarketPriceCoinGeckoSettings::demo(source.enabled, source.api_key)
|
||||||
|
},
|
||||||
|
ksp_offchain_transport_lib::MarketPriceCoinGeckoAccessMode::Keyless => {
|
||||||
|
ksp_offchain_transport_lib::MarketPriceCoinGeckoSettings::keyless(source.enabled)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return match settings {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(provider_contract_error(profile, "coingecko", &error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_coinmarketcap(
|
||||||
|
source: EffectiveCoinMarketCapSource,
|
||||||
|
effective: &crate::ResolvedConfigJson,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
) -> ksp_core_lib::Result<ksp_offchain_transport_lib::MarketPriceCoinMarketCapSettings> {
|
||||||
|
if source.api_key.is_some()
|
||||||
|
&& let std::result::Result::Err(error) = validate_secret_field_provenance(effective, "/market_price/coinmarketcap/api_key", profile, "coinmarketcap")
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let settings = match source.access_mode {
|
||||||
|
ksp_offchain_transport_lib::MarketPriceCoinMarketCapAccessMode::Basic => {
|
||||||
|
ksp_offchain_transport_lib::MarketPriceCoinMarketCapSettings::basic(source.enabled, source.api_key)
|
||||||
|
},
|
||||||
|
ksp_offchain_transport_lib::MarketPriceCoinMarketCapAccessMode::Keyless => {
|
||||||
|
ksp_offchain_transport_lib::MarketPriceCoinMarketCapSettings::keyless(source.enabled)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return match settings {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(provider_contract_error(profile, "coinmarketcap", &error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_dexscreener(
|
||||||
|
source: EffectiveDexScreenerSource,
|
||||||
|
effective: &crate::ResolvedConfigJson,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
) -> ksp_core_lib::Result<ksp_offchain_transport_lib::MarketPriceDexScreenerSettings> {
|
||||||
|
if source.sol_usd_pair_address.is_some()
|
||||||
|
&& let std::result::Result::Err(error) =
|
||||||
|
validate_public_field_provenance(effective, "/market_price/dexscreener/sol_usd_pair_address", profile, "dexscreener")
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let settings = ksp_offchain_transport_lib::MarketPriceDexScreenerSettings::new(source.enabled, source.sol_usd_pair_address);
|
||||||
|
return match settings {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(provider_contract_error(profile, "dexscreener", &error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_jupiter(
|
||||||
|
source: EffectiveJupiterSource,
|
||||||
|
effective: &crate::ResolvedConfigJson,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
) -> ksp_core_lib::Result<ksp_offchain_transport_lib::MarketPriceJupiterSettings> {
|
||||||
|
if source.api_key.is_some()
|
||||||
|
&& let std::result::Result::Err(error) = validate_secret_field_provenance(effective, "/market_price/jupiter/api_key", profile, "jupiter")
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let settings = match source.access_mode {
|
||||||
|
ksp_offchain_transport_lib::MarketPriceJupiterAccessMode::Free => {
|
||||||
|
ksp_offchain_transport_lib::MarketPriceJupiterSettings::free(source.enabled, source.api_key)
|
||||||
|
},
|
||||||
|
ksp_offchain_transport_lib::MarketPriceJupiterAccessMode::Keyless => ksp_offchain_transport_lib::MarketPriceJupiterSettings::keyless(source.enabled),
|
||||||
|
};
|
||||||
|
return match settings {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(provider_contract_error(profile, "jupiter", &error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_secret_field_provenance(
|
||||||
|
effective: &crate::ResolvedConfigJson,
|
||||||
|
pointer: &str,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
provider: &'static str,
|
||||||
|
) -> ksp_core_lib::Result<()> {
|
||||||
|
let provenance = match effective.provenance_at(pointer) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
effective_error(profile, "Off-chain Transport secret provenance is unavailable")
|
||||||
|
.with_context("provider", provider)
|
||||||
|
.with_context("field", pointer),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mut has_secret_environment = false;
|
||||||
|
for item in provenance {
|
||||||
|
let variable_name = match item.variable_name() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => continue,
|
||||||
|
};
|
||||||
|
let sensitivity = crate::ConfigSensitivity::from_variable_name(variable_name);
|
||||||
|
let sensitivity = match sensitivity {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if !sensitivity.is_secret() {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
effective_error(profile, "Off-chain Transport credentials may reference only secret environment variables")
|
||||||
|
.with_context("provider", provider)
|
||||||
|
.with_context("field", pointer),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
has_secret_environment = true;
|
||||||
|
}
|
||||||
|
if !has_secret_environment {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
effective_error(profile, "Off-chain Transport credentials require secret environment provenance")
|
||||||
|
.with_context("provider", provider)
|
||||||
|
.with_context("field", pointer),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_public_field_provenance(
|
||||||
|
effective: &crate::ResolvedConfigJson,
|
||||||
|
pointer: &str,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
provider: &'static str,
|
||||||
|
) -> ksp_core_lib::Result<()> {
|
||||||
|
let provenance = match effective.provenance_at(pointer) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
effective_error(profile, "Off-chain Transport public provenance is unavailable")
|
||||||
|
.with_context("provider", provider)
|
||||||
|
.with_context("field", pointer),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
for item in provenance {
|
||||||
|
let variable_name = match item.variable_name() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => continue,
|
||||||
|
};
|
||||||
|
let sensitivity = crate::ConfigSensitivity::from_variable_name(variable_name);
|
||||||
|
let sensitivity = match sensitivity {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
if sensitivity != crate::ConfigSensitivity::Public {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
effective_error(profile, "public Off-chain Transport fields may reference only public environment variables")
|
||||||
|
.with_context("provider", provider)
|
||||||
|
.with_context("field", pointer),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provider_contract_error(profile: &crate::ResolvedConfigProfile, provider: &'static str, error: &ksp_core_lib::Error) -> ksp_core_lib::Error {
|
||||||
|
return offchain_contract_error(profile, "effective market-price provider settings fail the Off-chain Transport runtime contract", error)
|
||||||
|
.with_context("provider", provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offchain_contract_error(profile: &crate::ResolvedConfigProfile, reason: &'static str, error: &ksp_core_lib::Error) -> ksp_core_lib::Error {
|
||||||
|
return effective_error(profile, reason)
|
||||||
|
.with_context("offchain_transport_error_domain", error.code().domain())
|
||||||
|
.with_context("offchain_transport_error_code", error.code().code());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract")
|
||||||
|
.with_context("file_id", profile.file_id().as_str())
|
||||||
|
.with_context("profile_id", profile.profile_id())
|
||||||
|
.with_context("reason", reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/offchain_transport.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/src/registry.rs
|
// file: crates/ksp-config-lib/src/registry.rs
|
||||||
// version: 10
|
// version: 11
|
||||||
|
|
||||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||||
pub const ARG_FILE_MAP: &str = "--filemap";
|
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";
|
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
|
||||||
/// Default physical filename for the standard Logging JSON Schema document.
|
/// Default physical filename for the standard Logging JSON Schema document.
|
||||||
pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
|
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.
|
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||||
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
|
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
|
||||||
/// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
|
/// 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";
|
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
|
||||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||||
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
|
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.
|
/// 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";
|
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
|
||||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||||
pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
|
pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
|
||||||
/// Logical file identifier for the standard Logging configuration document.
|
/// Logical file identifier for the standard Logging configuration document.
|
||||||
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
|
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.
|
/// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
|
||||||
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
|
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
|
||||||
/// Logical file identifier for the standard Wallet configuration document.
|
/// Logical file identifier for the standard Wallet configuration document.
|
||||||
@@ -172,6 +180,26 @@ impl ConfigFileRegistry {
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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(
|
let transport = ConfigFileDescriptor::new(
|
||||||
FILE_ID_STD_TRANSPORT,
|
FILE_ID_STD_TRANSPORT,
|
||||||
ConfigFileKind::Config,
|
ConfigFileKind::Config,
|
||||||
@@ -204,7 +232,18 @@ impl ConfigFileRegistry {
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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.
|
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/tests/ownership.rs
|
// file: crates/ksp-config-lib/tests/ownership.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
//! Workspace ownership audits for KSP application configuration boundaries.
|
//! Workspace ownership audits for KSP application configuration boundaries.
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ fn has_preceding_explanatory_comment(source: &str, assignment_line_index: usize)
|
|||||||
#[test]
|
#[test]
|
||||||
fn foundational_dependency_direction_does_not_point_back_to_config() {
|
fn foundational_dependency_direction_does_not_point_back_to_config() {
|
||||||
let root = workspace_root();
|
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_path = root.join("crates").join(crate_name).join("Cargo.toml");
|
||||||
let manifest = std::fs::read_to_string(manifest_path.as_path());
|
let manifest = std::fs::read_to_string(manifest_path.as_path());
|
||||||
assert!(manifest.is_ok(), "unable to read {}", manifest_path.display());
|
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\"",
|
"\"composite.schema.json\"",
|
||||||
"\"std.logging.json\"",
|
"\"std.logging.json\"",
|
||||||
"\"std.logging.schema.json\"",
|
"\"std.logging.schema.json\"",
|
||||||
|
"\"std.offchain_transport.json\"",
|
||||||
|
"\"std.offchain_transport.schema.json\"",
|
||||||
"\"std.transport.json\"",
|
"\"std.transport.json\"",
|
||||||
"\"std.transport.schema.json\"",
|
"\"std.transport.schema.json\"",
|
||||||
"\"std.wallet.json\"",
|
"\"std.wallet.json\"",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
// 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,
|
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
|
||||||
//! Logging/Transport adapters and management contracts.
|
//! 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:?}");
|
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
|
||||||
if let std::result::Result::Ok(registry) = registry {
|
if let std::result::Result::Ok(registry) = registry {
|
||||||
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
|
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[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[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[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_WALLET);
|
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_SCHEMA_COMPOSITE);
|
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_STD_LOGGING);
|
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_TRANSPORT);
|
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_WALLET);
|
assert_eq!(descriptors[7].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||||
let schema_file_id = descriptors[3].schema_file_id();
|
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");
|
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 {
|
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);
|
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");
|
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]
|
#[test]
|
||||||
fn packaged_runtime_layout_contract_is_available_from_crate_root() {
|
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;
|
let prepare: fn(&std::path::Path) -> ksp_core_lib::Result<ksp_config_lib::PackagedRuntimeLayout> = ksp_config_lib::prepare_packaged_runtime;
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"default_profile": "public_keyless",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "public_keyless",
|
||||||
|
"market_price": {
|
||||||
|
"birdeye": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"coinbase_exchange": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"coingecko": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "keyless"
|
||||||
|
},
|
||||||
|
"coinmarketcap": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "keyless"
|
||||||
|
},
|
||||||
|
"coinpaprika": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"dexscreener": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"jupiter": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "keyless"
|
||||||
|
},
|
||||||
|
"kraken": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "all_free",
|
||||||
|
"market_price": {
|
||||||
|
"birdeye": {
|
||||||
|
"enabled": true,
|
||||||
|
"api_key": "${KSP_SECRET_BIRDEYE_API_KEY}"
|
||||||
|
},
|
||||||
|
"coinbase_exchange": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"coingecko": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "demo",
|
||||||
|
"api_key": "${KSP_SECRET_COINGECKO_DEMO_API_KEY}"
|
||||||
|
},
|
||||||
|
"coinmarketcap": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "basic",
|
||||||
|
"api_key": "${KSP_SECRET_COINMARKETCAP_API_KEY}"
|
||||||
|
},
|
||||||
|
"coinpaprika": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"dexscreener": {
|
||||||
|
"enabled": true,
|
||||||
|
"sol_usd_pair_address": "${KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS}"
|
||||||
|
},
|
||||||
|
"jupiter": {
|
||||||
|
"enabled": true,
|
||||||
|
"access_mode": "free",
|
||||||
|
"api_key": "${KSP_SECRET_JUPITER_API_KEY}"
|
||||||
|
},
|
||||||
|
"kraken": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
239
crates/ksp-config-lib/unit_tests/offchain_transport.rs
Normal file
239
crates/ksp-config-lib/unit_tests/offchain_transport.rs
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
// file: crates/ksp-config-lib/unit_tests/offchain_transport.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
const TEST_PAIR: &str = "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn committed_public_keyless_profile_maps_all_eight_providers_without_secret_environment() {
|
||||||
|
let engine = committed_engine();
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||||
|
let resolved = engine.load_resolved_offchain_transport_config(std::option::Option::None, &environment);
|
||||||
|
assert!(resolved.is_ok(), "committed public_keyless Off-chain Transport profile should map: {resolved:?}");
|
||||||
|
if let std::result::Result::Ok(resolved) = resolved {
|
||||||
|
assert_eq!(resolved.file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||||
|
assert_eq!(resolved.profile_id(), "public_keyless");
|
||||||
|
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
|
||||||
|
let registry = resolved.service().registry();
|
||||||
|
assert_eq!(registry.len(), 8);
|
||||||
|
let ids: std::vec::Vec<&str> = registry.entries().iter().map(|entry| return entry.descriptor().id().as_str()).collect();
|
||||||
|
assert_eq!(ids, ["birdeye", "coinbase_exchange", "coingecko", "coinmarketcap", "coinpaprika", "dexscreener", "jupiter", "kraken"]);
|
||||||
|
assert_provider_availability(®istry, "birdeye", ksp_offchain_transport_lib::MarketPriceProviderAvailability::Disabled);
|
||||||
|
assert_provider_availability(®istry, "dexscreener", ksp_offchain_transport_lib::MarketPriceProviderAvailability::Disabled);
|
||||||
|
for provider_id in ["coinbase_exchange", "coingecko", "coinmarketcap", "coinpaprika", "jupiter", "kraken"] {
|
||||||
|
assert_provider_availability(®istry, provider_id, ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready);
|
||||||
|
}
|
||||||
|
let debug = format!("{resolved:?}");
|
||||||
|
assert!(!debug.contains("api_key"), "safe Debug should not expose credential field contents from the selected keyless profile");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn committed_all_free_profile_requires_config_owned_secrets_and_public_pair_provenance() {
|
||||||
|
let engine = committed_engine();
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let canaries = [
|
||||||
|
("KSP_SECRET_BIRDEYE_API_KEY", "birdeye-secret-canary"),
|
||||||
|
("KSP_SECRET_COINGECKO_DEMO_API_KEY", "coingecko-secret-canary"),
|
||||||
|
("KSP_SECRET_COINMARKETCAP_API_KEY", "coinmarketcap-secret-canary"),
|
||||||
|
("KSP_SECRET_JUPITER_API_KEY", "jupiter-secret-canary"),
|
||||||
|
("KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS", TEST_PAIR),
|
||||||
|
];
|
||||||
|
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||||
|
for (name, value) in canaries {
|
||||||
|
process.insert(name.to_owned(), value.to_owned());
|
||||||
|
}
|
||||||
|
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||||
|
let resolved = engine.load_resolved_offchain_transport_config(std::option::Option::Some("all_free"), &environment);
|
||||||
|
assert!(resolved.is_ok(), "committed all_free Off-chain Transport profile should map from Config-owned environment: {resolved:?}");
|
||||||
|
if let std::result::Result::Ok(resolved) = resolved {
|
||||||
|
assert_eq!(resolved.profile_id(), "all_free");
|
||||||
|
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
|
||||||
|
let registry = resolved.service().registry();
|
||||||
|
assert_eq!(registry.len(), 8);
|
||||||
|
for entry in registry.entries() {
|
||||||
|
assert_eq!(entry.state().availability(), ksp_offchain_transport_lib::MarketPriceProviderAvailability::Ready);
|
||||||
|
}
|
||||||
|
assert!(resolved.effective().sensitivity().is_secret());
|
||||||
|
let safe = resolved.effective().safe_value().to_string();
|
||||||
|
for secret in ["birdeye-secret-canary", "coingecko-secret-canary", "coinmarketcap-secret-canary", "jupiter-secret-canary"] {
|
||||||
|
assert!(!safe.contains(secret), "safe effective Config must redact provider credential canary");
|
||||||
|
}
|
||||||
|
assert!(safe.contains(TEST_PAIR), "public DexScreener pair should remain visible in the safe effective Config");
|
||||||
|
let debug = format!("{resolved:?}");
|
||||||
|
for secret in ["birdeye-secret-canary", "coingecko-secret-canary", "coinmarketcap-secret-canary", "jupiter-secret-canary"] {
|
||||||
|
assert!(!debug.contains(secret), "ResolvedOffchainTransportConfig Debug must redact provider credential canary");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn literal_or_nonsecret_provider_credentials_are_rejected_by_effective_adapter() {
|
||||||
|
let fixture = tempfile::tempdir();
|
||||||
|
assert!(fixture.is_ok(), "temporary Config root should be creatable: {fixture:?}");
|
||||||
|
let fixture = match fixture {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let source = committed_document_value();
|
||||||
|
let mut source = match source {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let profiles = source.get_mut("profiles").and_then(serde_json::Value::as_array_mut);
|
||||||
|
assert!(profiles.is_some(), "fixture should expose profiles");
|
||||||
|
if let std::option::Option::Some(profiles) = profiles {
|
||||||
|
let all_free = profiles
|
||||||
|
.iter_mut()
|
||||||
|
.find(|profile| return profile.get("profile_id").and_then(serde_json::Value::as_str) == std::option::Option::Some("all_free"));
|
||||||
|
assert!(all_free.is_some(), "fixture should contain all_free profile");
|
||||||
|
if let std::option::Option::Some(all_free) = all_free {
|
||||||
|
all_free["market_price"]["birdeye"]["api_key"] = serde_json::Value::String("literal-secret".to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let engine = fixture_engine_with_document(fixture.path(), &source);
|
||||||
|
assert!(engine.is_ok(), "literal-secret fixture engine should be constructible: {engine:?}");
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let mut process = all_free_environment();
|
||||||
|
process.insert("KSP_SECRET_BIRDEYE_API_KEY".to_owned(), "unused-secret".to_owned());
|
||||||
|
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||||
|
let resolved = engine.load_resolved_offchain_transport_config(std::option::Option::Some("all_free"), &environment);
|
||||||
|
assert!(resolved.is_err(), "literal provider credential must be rejected even though the JSON Schema accepts a non-empty string");
|
||||||
|
if let std::result::Result::Err(error) = resolved {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
|
||||||
|
assert!(!format!("{error:?}").contains("literal-secret"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dexscreener_pair_environment_must_use_public_namespace_and_disabled_pair_may_be_absent() {
|
||||||
|
let fixture = tempfile::tempdir();
|
||||||
|
assert!(fixture.is_ok(), "temporary Config root should be creatable: {fixture:?}");
|
||||||
|
let fixture = match fixture {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let source = committed_document_value();
|
||||||
|
let mut source = match source {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let profiles = source.get_mut("profiles").and_then(serde_json::Value::as_array_mut);
|
||||||
|
if let std::option::Option::Some(profiles) = profiles {
|
||||||
|
let all_free = profiles
|
||||||
|
.iter_mut()
|
||||||
|
.find(|profile| return profile.get("profile_id").and_then(serde_json::Value::as_str) == std::option::Option::Some("all_free"));
|
||||||
|
if let std::option::Option::Some(all_free) = all_free {
|
||||||
|
all_free["market_price"]["dexscreener"]["sol_usd_pair_address"] = serde_json::Value::String("${KSP_SECRET_DEXSCREENER_PAIR}".to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let engine = fixture_engine_with_document(fixture.path(), &source);
|
||||||
|
assert!(engine.is_ok(), "secret-pair fixture engine should be constructible: {engine:?}");
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let mut process = all_free_environment();
|
||||||
|
process.remove("KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS");
|
||||||
|
process.insert("KSP_SECRET_DEXSCREENER_PAIR".to_owned(), TEST_PAIR.to_owned());
|
||||||
|
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||||
|
let resolved = engine.load_resolved_offchain_transport_config(std::option::Option::Some("all_free"), &environment);
|
||||||
|
assert!(resolved.is_err(), "DexScreener pair environment must not use secret provenance");
|
||||||
|
if let std::result::Result::Err(error) = resolved {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
|
||||||
|
}
|
||||||
|
let disabled = ksp_offchain_transport_lib::MarketPriceDexScreenerSettings::new(false, std::option::Option::None);
|
||||||
|
assert!(disabled.is_ok(), "disabled DexScreener runtime settings should accept an absent pair after pre.009 capability reconciliation: {disabled:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_provider_availability(
|
||||||
|
registry: &ksp_offchain_transport_lib::MarketPriceProviderRegistry,
|
||||||
|
provider_id: &str,
|
||||||
|
expected: ksp_offchain_transport_lib::MarketPriceProviderAvailability,
|
||||||
|
) {
|
||||||
|
let provider_id = ksp_offchain_transport_lib::MarketPriceProviderId::new(provider_id);
|
||||||
|
assert!(provider_id.is_ok(), "provider id fixture should be valid: {provider_id:?}");
|
||||||
|
if let std::result::Result::Ok(provider_id) = provider_id {
|
||||||
|
let state = registry.state(&provider_id);
|
||||||
|
assert!(state.is_some(), "provider should be present in Config-produced service registry");
|
||||||
|
if let std::option::Option::Some(state) = state {
|
||||||
|
assert_eq!(state.availability(), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||||
|
let workspace = workspace_root();
|
||||||
|
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||||
|
let bootstrap = match bootstrap {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let registry = crate::ConfigFileRegistry::defaults();
|
||||||
|
let registry = match registry {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture_engine_with_document(root: &std::path::Path, document: &serde_json::Value) -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||||
|
let config_root = root.join("config");
|
||||||
|
let create = std::fs::create_dir_all(config_root.as_path());
|
||||||
|
if let std::result::Result::Err(error) = create {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "test Config root cannot be created").with_source(error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let bytes = serde_json::to_vec_pretty(document);
|
||||||
|
let bytes = match bytes {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_SYNTAX_INVALID, "test Config cannot be encoded").with_source(error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let path = config_root.join(crate::DEFAULT_STD_OFFCHAIN_TRANSPORT_FILENAME);
|
||||||
|
if let std::result::Result::Err(error) = std::fs::write(path.as_path(), bytes) {
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_JSON_FILE_READ_FAILED, "test Config cannot be written").with_source(error));
|
||||||
|
}
|
||||||
|
let bootstrap = crate::ConfigBootstrapOptions::from_paths(config_root, workspace_root().join("config/schemas"));
|
||||||
|
let bootstrap = match bootstrap {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let registry = crate::ConfigFileRegistry::defaults();
|
||||||
|
let registry = match registry {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn committed_document_value() -> std::result::Result<serde_json::Value, serde_json::Error> {
|
||||||
|
return serde_json::from_str(include_str!("../../../config/std.offchain_transport.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_free_environment() -> std::collections::BTreeMap<String, String> {
|
||||||
|
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||||
|
process.insert("KSP_SECRET_BIRDEYE_API_KEY".to_owned(), "birdeye-test".to_owned());
|
||||||
|
process.insert("KSP_SECRET_COINGECKO_DEMO_API_KEY".to_owned(), "coingecko-test".to_owned());
|
||||||
|
process.insert("KSP_SECRET_COINMARKETCAP_API_KEY".to_owned(), "coinmarketcap-test".to_owned());
|
||||||
|
process.insert("KSP_SECRET_JUPITER_API_KEY".to_owned(), "jupiter-test".to_owned());
|
||||||
|
process.insert("KSP_PUBLIC_DEXSCREENER_SOL_USD_PAIR_ADDRESS".to_owned(), TEST_PAIR.to_owned());
|
||||||
|
return process;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_root() -> std::path::PathBuf {
|
||||||
|
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/unit_tests/registry.rs
|
// file: crates/ksp-config-lib/unit_tests/registry.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
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:?}");
|
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||||
if let std::result::Result::Ok(registry) = registry {
|
if let std::result::Result::Ok(registry) = registry {
|
||||||
let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect();
|
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].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].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[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[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[2].file_id().as_str(), crate::FILE_ID_STD_OFFCHAIN_TRANSPORT);
|
||||||
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_STD_WALLET);
|
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
|
||||||
assert_eq!(descriptors[3].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
|
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_STD_WALLET);
|
||||||
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].filename(), std::path::Path::new(crate::DEFAULT_STD_WALLET_FILENAME));
|
||||||
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
|
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_STD_LOGGING);
|
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_TRANSPORT);
|
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_WALLET);
|
assert_eq!(descriptors[7].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_OFFCHAIN_TRANSPORT);
|
||||||
assert!(descriptors[0..4].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Config));
|
assert_eq!(descriptors[8].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||||
assert!(descriptors[4..8].iter().all(|descriptor| return descriptor.kind() == crate::ConfigFileKind::Schema));
|
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]
|
#[test]
|
||||||
fn defaults_register_transport_document_and_schema_with_distinct_roots() {
|
fn defaults_register_transport_document_and_schema_with_distinct_roots() {
|
||||||
let registry = crate::ConfigFileRegistry::defaults();
|
let registry = crate::ConfigFileRegistry::defaults();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-offchain-transport-lib/src/market_price_dexscreener.rs
|
// 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.
|
//! 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)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct MarketPriceDexScreenerSettings {
|
pub struct MarketPriceDexScreenerSettings {
|
||||||
common: crate::MarketPriceProviderCommonSettings,
|
common: crate::MarketPriceProviderCommonSettings,
|
||||||
pair_address: ksp_core_lib::Pubkey,
|
pair_address: std::option::Option<ksp_core_lib::Pubkey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::MarketPriceDexScreenerSettings {
|
impl crate::MarketPriceDexScreenerSettings {
|
||||||
/// Creates DexScreener settings for one explicit Solana pair address.
|
/// Creates DexScreener settings for one optional explicit Solana pair address.
|
||||||
pub fn new(enabled: bool, pair_address: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
///
|
||||||
|
/// 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) {
|
let provider_id = match crate::MarketPriceProviderId::new(DEXSCREENER_PROVIDER_ID) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
let pair_address = match pair_address.into().parse::<ksp_core_lib::Pubkey>() {
|
let pair_address = match pair_address {
|
||||||
std::result::Result::Ok(value) => value,
|
std::option::Option::Some(value) => match value.parse::<ksp_core_lib::Pubkey>() {
|
||||||
std::result::Result::Err(_) => return std::result::Result::Err(provider_settings_error("pair_address")),
|
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);
|
let common = crate::MarketPriceProviderCommonSettings::new(provider_id, enabled);
|
||||||
return std::result::Result::Ok(Self { common, pair_address });
|
return std::result::Result::Ok(Self { common, pair_address });
|
||||||
@@ -35,10 +42,10 @@ impl crate::MarketPriceDexScreenerSettings {
|
|||||||
return &self.common;
|
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]
|
#[must_use]
|
||||||
pub const fn pair_address(&self) -> &ksp_core_lib::Pubkey {
|
pub const fn pair_address(&self) -> std::option::Option<&ksp_core_lib::Pubkey> {
|
||||||
return &self.pair_address;
|
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) {
|
if let std::result::Result::Err(error) = crate::admit_request(DEXSCREENER_PROVIDER_ID, &self.admission) {
|
||||||
return std::result::Result::Err(error);
|
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() {
|
let request_started_at = match crate::current_timestamp() {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
return parse_response(
|
return parse_response(document.as_bytes(), self.settings.common().provider_id().clone(), pair_address, request_started_at, received_at);
|
||||||
document.as_bytes(),
|
|
||||||
self.settings.common().provider_id().clone(),
|
|
||||||
self.settings.pair_address(),
|
|
||||||
request_started_at,
|
|
||||||
received_at,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-offchain-transport-lib/tests/public_api.rs
|
// file: crates/ksp-offchain-transport-lib/tests/public_api.rs
|
||||||
// version: 9
|
// version: 10
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![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().id().as_str(), "jupiter");
|
||||||
assert_eq!(jupiter_provider.descriptor().semantics(), ksp_offchain_transport_lib::MarketPriceSemantics::SolanaHeuristic);
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_dexscreener.rs
|
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_dexscreener.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
const TEST_PAIR: &str = "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE";
|
const TEST_PAIR: &str = "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE";
|
||||||
|
|
||||||
@@ -35,13 +35,26 @@ fn dexscreener_fixture_validates_chain_pair_and_sol_identity_and_maps_exact_pair
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dexscreener_settings_and_descriptor_keep_pair_explicit_and_pair_endpoint_rate_limited() -> ksp_core_lib::Result<()> {
|
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 disabled = crate::MarketPriceDexScreenerSettings::new(false, std::option::Option::None);
|
||||||
let settings = match crate::MarketPriceDexScreenerSettings::new(true, TEST_PAIR) {
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
assert_eq!(settings.pair_address().to_string(), TEST_PAIR);
|
let pair_address = settings.pair_address();
|
||||||
let request = match super::build_request(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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_registry.rs
|
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_registry.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn registry_orders_all_eight_v1_providers_and_exposes_only_generic_entries() -> ksp_core_lib::Result<()> {
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_service.rs
|
// file: crates/ksp-offchain-transport-lib/unit_tests/market_price_service.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn service_registry_orders_all_eight_v1_setups_and_keeps_runtime_projection_generic() -> ksp_core_lib::Result<()> {
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
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::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
|
|||||||
240
deltas/0.2.11/pre.009.md
Normal file
240
deltas/0.2.11/pre.009.md
Normal 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.
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<!-- file: docs/plans/018-V0_2_11_OFFCHAIN_PRICE_TRANSPORT_PLAN.md -->
|
<!-- 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
|
# 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é
|
## 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
|
## 15. Config provider-capability-aware
|
||||||
|
|
||||||
Config reste l'unique propriétaire des documents/env/secrets.
|
Config reste l'unique propriétaire des documents, de l'environnement et des secrets. `pre.009` matérialise les identifiants durables :
|
||||||
|
|
||||||
Off-chain Transport expose ses settings publics ; Config adapte un futur document standard dédié, pressenti :
|
|
||||||
|
|
||||||
```text
|
```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 |
|
Deux profils versionnés servent de canaries :
|
||||||
|-------------------|------------------------------------------------------------|
|
|
||||||
| 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é |
|
|
||||||
|
|
||||||
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.
|
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é.**
|
**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
|
### `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
|
### `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
|
### `pre.010` — Hardening et complétude technique
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/014-V0_2_11_OFFCHAIN_PRICE_TRANSPORT.md -->
|
<!-- 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
|
# 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`
|
## 3.11 Gate `0.2.11-pre.008`
|
||||||
|
|
||||||
| Critère | Statut | Preuve |
|
| Critère | Statut | Preuve |
|
||||||
|-------------------------------------------------------------------------|---------|--------------------------------------------|
|
|-------------------------------------------------------------------------|--------|--------------------------------------------|
|
||||||
| version workspace `0.2.11-pre.8` | PASS | `Cargo.toml` racine |
|
| version workspace `0.2.11-pre.8` | PASS | `Cargo.toml` racine |
|
||||||
| `MarketPriceService` provider-agnostic public | PASS | `market_price_service.rs` |
|
| `MarketPriceService` provider-agnostic public | PASS | `market_price_service.rs` |
|
||||||
| setup provider-specific limité à la composition | PASS | `MarketPriceProviderSetup` |
|
| setup provider-specific limité à la composition | PASS | `MarketPriceProviderSetup` |
|
||||||
| registry détachable sans dispatch provider | PASS | `MarketPriceService::registry` |
|
| registry détachable sans dispatch provider | PASS | `MarketPriceService::registry` |
|
||||||
| refresh individuel par `MarketPriceProviderId` | PASS | `MarketPriceService::refresh` |
|
| refresh individuel par `MarketPriceProviderId` | PASS | `MarketPriceService::refresh` |
|
||||||
| refresh multiple par liste d'identifiants opaques | PASS | `MarketPriceService::refresh_many` |
|
| refresh multiple par liste d'identifiants opaques | PASS | `MarketPriceService::refresh_many` |
|
||||||
| refresh all en ordre provider déterministe | PASS | `MarketPriceService::refresh_all` |
|
| refresh all en ordre provider déterministe | PASS | `MarketPriceService::refresh_all` |
|
||||||
| doublons / provider inconnu rejetés avant dispatch | PASS | tests `market_price_service` |
|
| 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 |
|
| 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 |
|
| `429` / defer local projetés en `CoolingDown` | PASS | classification générique |
|
||||||
| `401/403` keyed projetés en `AuthenticationUnavailable` | PASS | classification auth-aware |
|
| `401/403` keyed projetés en `AuthenticationUnavailable` | PASS | classification auth-aware |
|
||||||
| transport temporaire projeté sans rendre le service global inutilisable | PASS | outcome provider-neutral |
|
| 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 |
|
| aucune attente/sleep de rate limit dans le service | PASS | dependency boundary |
|
||||||
| aucun fallback, consensus ou agrégation multi-provider | PASS | scope + 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+` |
|
| 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 Rust sandbox | PASS | clean, 0 candidate export |
|
||||||
| audit Markdown sandbox | PASS | clean, 122 tables / 113 fichiers |
|
| audit Markdown sandbox | PASS | clean, 122 tables / 113 fichiers |
|
||||||
| gate `pre.008` brut | FAIL | 1 warning Clippy + 1 canari boundary FAIL |
|
| gate `pre.008` brut | FAIL | 1 warning Clippy + 1 canari boundary FAIL |
|
||||||
| fix `pre.008-fix.001` statique | PASS | audits Rust/Markdown sandbox |
|
| fix `pre.008-fix.001` statique | PASS | audits Rust/Markdown sandbox |
|
||||||
| gate Cargo du fix | PLANNED | à exécuter par l'opérateur |
|
| 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
|
## 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
|
## 9. DexScreener V1
|
||||||
|
|
||||||
| Invariant | Statut |
|
| Invariant | Statut |
|
||||||
|----------------------------------------------|--------|
|
|--------------------------------------------------|--------|
|
||||||
| pair address SOL/USD explicite dans settings | PASS |
|
| pair address SOL/USD explicite si provider actif | PASS |
|
||||||
| `chainId = solana` vérifié | PASS |
|
| `chainId = solana` vérifié | PASS |
|
||||||
| identité SOL de la paire vérifiée | PASS |
|
| identité SOL de la paire vérifiée | PASS |
|
||||||
| `priceUsd` utilisé comme prix commun | PASS |
|
| `priceUsd` utilisé comme prix commun | PASS |
|
||||||
| aucune recherche automatique de pool | PASS |
|
| aucune recherche automatique de pool | PASS |
|
||||||
| aucun tri automatique par liquidité | PASS |
|
| aucun tri automatique par liquidité | PASS |
|
||||||
| aucune moyenne/consensus entre pools | PASS |
|
| aucune moyenne/consensus entre pools | PASS |
|
||||||
|
|
||||||
## 10. Config -> Off-chain Transport
|
## 10. Config -> Off-chain Transport
|
||||||
|
|
||||||
| Invariant | Statut |
|
| Invariant | Statut |
|
||||||
|------------------------------------------------------------------|---------|
|
|------------------------------------------------------------------|--------|
|
||||||
| Off-chain Transport ne dépend pas de Config | PLANNED |
|
| Off-chain Transport ne dépend pas de Config | PASS |
|
||||||
| document standard offchain dédié | PLANNED |
|
| document standard offchain dédié | PASS |
|
||||||
| branches provider conditionnelles dans schema | PLANNED |
|
| branches provider conditionnelles dans schema | PASS |
|
||||||
| CoinGecko keyless/Demo représentés | PLANNED |
|
| CoinGecko keyless/Demo représentés | PASS |
|
||||||
| CoinMarketCap keyless/Basic représentés | PLANNED |
|
| CoinMarketCap keyless/Basic représentés | PASS |
|
||||||
| Jupiter keyless/Free représentés | PLANNED |
|
| Jupiter keyless/Free représentés | PASS |
|
||||||
| Birdeye exige credential lorsqu'activé | PLANNED |
|
| Birdeye exige credential lorsqu'activé | PASS |
|
||||||
| DexScreener exige pair address lorsqu'activé | PLANNED |
|
| DexScreener exige pair address lorsqu'activé | PASS |
|
||||||
| providers sans auth n'exposent pas de faux champ API key | PLANNED |
|
| providers sans auth n'exposent pas de faux champ API key | PASS |
|
||||||
| secrets via Config uniquement | PLANNED |
|
| secrets via Config uniquement | PASS |
|
||||||
| `.env.example` synchronisé pour les secrets réellement ajoutés | PLANNED |
|
| `.env.example` synchronisé pour les secrets réellement ajoutés | PASS |
|
||||||
| aucune cadence provider configurable au-dessus de la limite sûre | PLANNED |
|
| aucune cadence provider configurable au-dessus de la limite sûre | PASS |
|
||||||
|
|
||||||
## 11. Frontière future `ksp-app-solprices-desk`
|
## 11. Frontière future `ksp-app-solprices-desk`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user