v0.2.7-pre.003
This commit is contained in:
14
.env.example
14
.env.example
@@ -1,5 +1,5 @@
|
||||
# file: .env.example
|
||||
# version: 4
|
||||
# version: 5
|
||||
|
||||
# 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.
|
||||
@@ -22,10 +22,22 @@ KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com
|
||||
# The committed Transport document falls back to https://api.mainnet-beta.solana.com when this variable is absent.
|
||||
KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet-beta.solana.com
|
||||
|
||||
# Optional public Solana Devnet WebSocket endpoint override used by config/std.transport.json.
|
||||
# The committed Transport document falls back to wss://api.devnet.solana.com when this variable is absent.
|
||||
KSP_PUBLIC_SOLANA_DEVNET_WS_URL=wss://api.devnet.solana.com
|
||||
|
||||
# Optional public Solana Mainnet WebSocket endpoint override used by config/std.transport.json and its example.
|
||||
# The committed Transport document falls back to wss://api.mainnet-beta.solana.com when this variable is absent.
|
||||
KSP_PUBLIC_SOLANA_MAINNET_WS_URL=wss://api.mainnet-beta.solana.com
|
||||
|
||||
# Optional complete private-provider HTTP endpoint URL used only by the Transport example when explicitly selected.
|
||||
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
|
||||
# KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me
|
||||
|
||||
# Optional complete private-provider WebSocket endpoint URL used only by the Transport example when explicitly selected.
|
||||
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
|
||||
# KSP_SECRET_SOLANA_WS_URL=wss://provider.example/?api-key=replace-me
|
||||
|
||||
# Fade-in duration in milliseconds used by the common KSP desk splash lifecycle.
|
||||
KSP_DESK_SPLASH_FADE_IN_MS=300
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 194
|
||||
# version: 195
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.7-pre.2.fix.1"
|
||||
version = "0.2.7-pre.3"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"format_version": 2,
|
||||
"retry": {
|
||||
"max_retries": 3,
|
||||
"initial_backoff_ms": 150,
|
||||
"max_backoff_ms": 3000
|
||||
},
|
||||
"ws_defaults": {
|
||||
"command_timeout_ms": 10000,
|
||||
"close_timeout_ms": 5000,
|
||||
"reconnect": {
|
||||
"max_retries": 5,
|
||||
"initial_backoff_ms": 250,
|
||||
"max_backoff_ms": 5000
|
||||
},
|
||||
"resubscribe": "active_subscriptions",
|
||||
"command_queue_capacity": 128,
|
||||
"notification_queue_capacity": 256,
|
||||
"max_active_subscriptions": 1024,
|
||||
"max_pending_requests": 128,
|
||||
"max_message_size_bytes": 67108864,
|
||||
"max_frame_size_bytes": 16777216,
|
||||
"max_write_buffer_size_bytes": 1048576
|
||||
},
|
||||
"default_profile": "mainnet_mixed",
|
||||
"profiles": [
|
||||
{
|
||||
@@ -23,7 +40,9 @@
|
||||
{
|
||||
"role": "default",
|
||||
"enabled": true,
|
||||
"request_kinds": ["*"],
|
||||
"request_kinds": [
|
||||
"*"
|
||||
],
|
||||
"priority": 200,
|
||||
"limits": {
|
||||
"requests_per_second": 5,
|
||||
@@ -47,7 +66,9 @@
|
||||
{
|
||||
"role": "default",
|
||||
"enabled": true,
|
||||
"request_kinds": ["*"],
|
||||
"request_kinds": [
|
||||
"*"
|
||||
],
|
||||
"priority": 100,
|
||||
"limits": {
|
||||
"requests_per_second": 20,
|
||||
@@ -58,6 +79,28 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ws_endpoints": [
|
||||
{
|
||||
"name": "mainnet_public_ws",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet-beta",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
|
||||
},
|
||||
{
|
||||
"name": "mainnet_private_ws",
|
||||
"enabled": true,
|
||||
"provider": "private-provider",
|
||||
"cluster": "mainnet-beta",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_SECRET_SOLANA_WS_URL:-wss://example.invalid}",
|
||||
"session": {
|
||||
"notification_queue_capacity": 512,
|
||||
"max_active_subscriptions": 2048
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "urn:ksp:schema:std.transport:v1",
|
||||
"title": "KSP standard HTTP Transport configuration",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["format_version", "retry", "default_profile", "profiles"],
|
||||
"properties": {
|
||||
"format_version": {"const": 1},
|
||||
"retry": {"$ref": "#/$defs/retry"},
|
||||
"default_profile": {"$ref": "#/$defs/profileId"},
|
||||
"profiles": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {"$ref": "#/$defs/profile"}
|
||||
"$id": "urn:ksp:schema:std.transport:v2",
|
||||
"title": "KSP standard HTTP + WebSocket Transport configuration",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/$defs/documentV1"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/documentV2"
|
||||
}
|
||||
},
|
||||
],
|
||||
"$defs": {
|
||||
"profileId": {
|
||||
"type": "string",
|
||||
@@ -35,50 +30,99 @@
|
||||
"minimum": 1,
|
||||
"maximum": 4294967295
|
||||
},
|
||||
"positiveUsize": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 4294967295
|
||||
},
|
||||
"retry": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["max_retries", "initial_backoff_ms", "max_backoff_ms"],
|
||||
"required": [
|
||||
"max_retries",
|
||||
"initial_backoff_ms",
|
||||
"max_backoff_ms"
|
||||
],
|
||||
"properties": {
|
||||
"max_retries": {"type": "integer", "minimum": 0, "maximum": 100},
|
||||
"initial_backoff_ms": {"$ref": "#/$defs/positiveMs"},
|
||||
"max_backoff_ms": {"$ref": "#/$defs/positiveMs"}
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
},
|
||||
"initial_backoff_ms": {
|
||||
"$ref": "#/$defs/positiveMs"
|
||||
},
|
||||
"max_backoff_ms": {
|
||||
"$ref": "#/$defs/positiveMs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"requests_per_second": {"$ref": "#/$defs/positiveU32"},
|
||||
"burst_capacity": {"$ref": "#/$defs/positiveU32"},
|
||||
"max_concurrent_requests": {"$ref": "#/$defs/positiveU32"},
|
||||
"pause_after_rate_limit_ms": {"$ref": "#/$defs/positiveMs"}
|
||||
"requests_per_second": {
|
||||
"$ref": "#/$defs/positiveU32"
|
||||
},
|
||||
"burst_capacity": {
|
||||
"$ref": "#/$defs/positiveU32"
|
||||
},
|
||||
"max_concurrent_requests": {
|
||||
"$ref": "#/$defs/positiveU32"
|
||||
},
|
||||
"pause_after_rate_limit_ms": {
|
||||
"$ref": "#/$defs/positiveMs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"role": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["role", "enabled", "request_kinds", "priority", "limits"],
|
||||
"required": [
|
||||
"role",
|
||||
"enabled",
|
||||
"request_kinds",
|
||||
"priority",
|
||||
"limits"
|
||||
],
|
||||
"properties": {
|
||||
"role": {"$ref": "#/$defs/descriptor"},
|
||||
"enabled": {"type": "boolean"},
|
||||
"role": {
|
||||
"$ref": "#/$defs/descriptor"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"request_kinds": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": {"$ref": "#/$defs/descriptor"},
|
||||
"items": {
|
||||
"$ref": "#/$defs/descriptor"
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {"contains": {"const": "*"}},
|
||||
"then": {"maxItems": 1}
|
||||
"if": {
|
||||
"contains": {
|
||||
"const": "*"
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"maxItems": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"priority": {"type": "integer", "minimum": 0, "maximum": 4294967295},
|
||||
"limits": {"$ref": "#/$defs/limits"}
|
||||
"priority": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295
|
||||
},
|
||||
"limits": {
|
||||
"$ref": "#/$defs/limits"
|
||||
}
|
||||
}
|
||||
},
|
||||
"endpoint": {
|
||||
"httpEndpoint": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
@@ -92,31 +136,322 @@
|
||||
"roles"
|
||||
],
|
||||
"properties": {
|
||||
"name": {"$ref": "#/$defs/descriptor"},
|
||||
"enabled": {"type": "boolean"},
|
||||
"provider": {"$ref": "#/$defs/descriptor"},
|
||||
"cluster": {"$ref": "#/$defs/descriptor"},
|
||||
"url": {"type": "string", "minLength": 1},
|
||||
"connect_timeout_ms": {"$ref": "#/$defs/positiveMs"},
|
||||
"request_timeout_ms": {"$ref": "#/$defs/positiveMs"},
|
||||
"max_idle_connections_per_host": {"type": "integer", "minimum": 1},
|
||||
"name": {
|
||||
"$ref": "#/$defs/descriptor"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"provider": {
|
||||
"$ref": "#/$defs/descriptor"
|
||||
},
|
||||
"cluster": {
|
||||
"$ref": "#/$defs/descriptor"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"connect_timeout_ms": {
|
||||
"$ref": "#/$defs/positiveMs"
|
||||
},
|
||||
"request_timeout_ms": {
|
||||
"$ref": "#/$defs/positiveMs"
|
||||
},
|
||||
"max_idle_connections_per_host": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {"$ref": "#/$defs/role"}
|
||||
"items": {
|
||||
"$ref": "#/$defs/role"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"wsReconnect": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["profile_id", "endpoints"],
|
||||
"required": [
|
||||
"max_retries",
|
||||
"initial_backoff_ms",
|
||||
"max_backoff_ms"
|
||||
],
|
||||
"properties": {
|
||||
"profile_id": {"$ref": "#/$defs/profileId"},
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
},
|
||||
"initial_backoff_ms": {
|
||||
"$ref": "#/$defs/positiveMs"
|
||||
},
|
||||
"max_backoff_ms": {
|
||||
"$ref": "#/$defs/positiveMs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wsReconnectOverride": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"max_retries": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
},
|
||||
"initial_backoff_ms": {
|
||||
"$ref": "#/$defs/positiveMs"
|
||||
},
|
||||
"max_backoff_ms": {
|
||||
"$ref": "#/$defs/positiveMs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wsSession": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"command_timeout_ms",
|
||||
"close_timeout_ms",
|
||||
"reconnect",
|
||||
"resubscribe",
|
||||
"command_queue_capacity",
|
||||
"notification_queue_capacity",
|
||||
"max_active_subscriptions",
|
||||
"max_pending_requests",
|
||||
"max_message_size_bytes",
|
||||
"max_frame_size_bytes",
|
||||
"max_write_buffer_size_bytes"
|
||||
],
|
||||
"properties": {
|
||||
"command_timeout_ms": {
|
||||
"$ref": "#/$defs/positiveMs"
|
||||
},
|
||||
"close_timeout_ms": {
|
||||
"$ref": "#/$defs/positiveMs"
|
||||
},
|
||||
"reconnect": {
|
||||
"$ref": "#/$defs/wsReconnect"
|
||||
},
|
||||
"resubscribe": {
|
||||
"enum": [
|
||||
"never",
|
||||
"active_subscriptions"
|
||||
]
|
||||
},
|
||||
"command_queue_capacity": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
},
|
||||
"notification_queue_capacity": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
},
|
||||
"max_active_subscriptions": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
},
|
||||
"max_pending_requests": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
},
|
||||
"max_message_size_bytes": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
},
|
||||
"max_frame_size_bytes": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
},
|
||||
"max_write_buffer_size_bytes": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wsSessionOverride": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"minProperties": 1,
|
||||
"properties": {
|
||||
"command_timeout_ms": {
|
||||
"$ref": "#/$defs/positiveMs"
|
||||
},
|
||||
"close_timeout_ms": {
|
||||
"$ref": "#/$defs/positiveMs"
|
||||
},
|
||||
"reconnect": {
|
||||
"$ref": "#/$defs/wsReconnectOverride"
|
||||
},
|
||||
"resubscribe": {
|
||||
"enum": [
|
||||
"never",
|
||||
"active_subscriptions"
|
||||
]
|
||||
},
|
||||
"command_queue_capacity": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
},
|
||||
"notification_queue_capacity": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
},
|
||||
"max_active_subscriptions": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
},
|
||||
"max_pending_requests": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
},
|
||||
"max_message_size_bytes": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
},
|
||||
"max_frame_size_bytes": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
},
|
||||
"max_write_buffer_size_bytes": {
|
||||
"$ref": "#/$defs/positiveUsize"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wsEndpoint": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"name",
|
||||
"enabled",
|
||||
"provider",
|
||||
"cluster",
|
||||
"kind",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"$ref": "#/$defs/descriptor"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"provider": {
|
||||
"$ref": "#/$defs/descriptor"
|
||||
},
|
||||
"cluster": {
|
||||
"$ref": "#/$defs/descriptor"
|
||||
},
|
||||
"kind": {
|
||||
"enum": [
|
||||
"solana_standard"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"session": {
|
||||
"$ref": "#/$defs/wsSessionOverride"
|
||||
}
|
||||
}
|
||||
},
|
||||
"profileV1": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"profile_id",
|
||||
"endpoints"
|
||||
],
|
||||
"properties": {
|
||||
"profile_id": {
|
||||
"$ref": "#/$defs/profileId"
|
||||
},
|
||||
"endpoints": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {"$ref": "#/$defs/endpoint"}
|
||||
"items": {
|
||||
"$ref": "#/$defs/httpEndpoint"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"profileV2": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"profile_id",
|
||||
"endpoints",
|
||||
"ws_endpoints"
|
||||
],
|
||||
"properties": {
|
||||
"profile_id": {
|
||||
"$ref": "#/$defs/profileId"
|
||||
},
|
||||
"endpoints": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/httpEndpoint"
|
||||
}
|
||||
},
|
||||
"ws_endpoints": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/wsEndpoint"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"documentV1": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"format_version",
|
||||
"retry",
|
||||
"default_profile",
|
||||
"profiles"
|
||||
],
|
||||
"properties": {
|
||||
"format_version": {
|
||||
"const": 1
|
||||
},
|
||||
"retry": {
|
||||
"$ref": "#/$defs/retry"
|
||||
},
|
||||
"default_profile": {
|
||||
"$ref": "#/$defs/profileId"
|
||||
},
|
||||
"profiles": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/profileV1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"documentV2": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"format_version",
|
||||
"retry",
|
||||
"ws_defaults",
|
||||
"default_profile",
|
||||
"profiles"
|
||||
],
|
||||
"properties": {
|
||||
"format_version": {
|
||||
"const": 2
|
||||
},
|
||||
"retry": {
|
||||
"$ref": "#/$defs/retry"
|
||||
},
|
||||
"ws_defaults": {
|
||||
"$ref": "#/$defs/wsSession"
|
||||
},
|
||||
"default_profile": {
|
||||
"$ref": "#/$defs/profileId"
|
||||
},
|
||||
"profiles": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "#/$defs/profileV2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"format_version": 2,
|
||||
"retry": {
|
||||
"max_retries": 2,
|
||||
"initial_backoff_ms": 100,
|
||||
"max_backoff_ms": 2000
|
||||
},
|
||||
"ws_defaults": {
|
||||
"command_timeout_ms": 10000,
|
||||
"close_timeout_ms": 5000,
|
||||
"reconnect": {
|
||||
"max_retries": 5,
|
||||
"initial_backoff_ms": 250,
|
||||
"max_backoff_ms": 5000
|
||||
},
|
||||
"resubscribe": "active_subscriptions",
|
||||
"command_queue_capacity": 128,
|
||||
"notification_queue_capacity": 256,
|
||||
"max_active_subscriptions": 1024,
|
||||
"max_pending_requests": 128,
|
||||
"max_message_size_bytes": 67108864,
|
||||
"max_frame_size_bytes": 16777216,
|
||||
"max_write_buffer_size_bytes": 1048576
|
||||
},
|
||||
"default_profile": "devnet_public",
|
||||
"profiles": [
|
||||
{
|
||||
@@ -23,7 +40,9 @@
|
||||
{
|
||||
"role": "default",
|
||||
"enabled": true,
|
||||
"request_kinds": ["*"],
|
||||
"request_kinds": [
|
||||
"*"
|
||||
],
|
||||
"priority": 100,
|
||||
"limits": {
|
||||
"requests_per_second": 5,
|
||||
@@ -34,6 +53,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ws_endpoints": [
|
||||
{
|
||||
"name": "solana_devnet_public_ws",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "devnet",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -52,7 +81,9 @@
|
||||
{
|
||||
"role": "default",
|
||||
"enabled": true,
|
||||
"request_kinds": ["*"],
|
||||
"request_kinds": [
|
||||
"*"
|
||||
],
|
||||
"priority": 100,
|
||||
"limits": {
|
||||
"requests_per_second": 5,
|
||||
@@ -63,6 +94,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ws_endpoints": [
|
||||
{
|
||||
"name": "solana_mainnet_public_ws",
|
||||
"enabled": true,
|
||||
"provider": "solana-public",
|
||||
"cluster": "mainnet-beta",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-config-lib/README.md -->
|
||||
<!-- version: 6 -->
|
||||
<!-- version: 7 -->
|
||||
|
||||
# ksp-config-lib
|
||||
|
||||
@@ -23,7 +23,7 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
|
||||
- la classification `Public`, `Internal`, `Secret` ;
|
||||
- les représentations réelle et sûre/redacted ainsi que la provenance des valeurs résolues ;
|
||||
- l'adapter du document Logging effectif vers `ksp_logging_lib::LoggingSettings` ;
|
||||
- l'adapter du document HTTP Transport effectif vers `ksp_onchain_transport_lib::HttpTransportSettings`, y compris redaction/provenance des URLs `KSP_SECRET_*` ;
|
||||
- l'adapter du document Transport V1/V2 vers `HttpTransportSettings` et, en V2, `WsTransportSettings`, y compris redaction/provenance des URLs `KSP_SECRET_*` ;
|
||||
- la surface de management pour inspecter et réparer les sources Config enregistrées, modifier `std.logging.json`, consulter les rapports d'environnement, révéler explicitement une valeur réelle et modifier `.env` ;
|
||||
- les écritures atomiques JSON/`.env` et la protection des permissions `.env` ;
|
||||
- les audits workspace empêchant les bypass d'ownership Config et les oublis dans `.env.example`.
|
||||
@@ -81,7 +81,7 @@ Un secret reste accessible au runtime ou au management lorsqu'un consumer autori
|
||||
|
||||
Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées.
|
||||
|
||||
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs endpoint : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
|
||||
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs HTTP et WebSocket : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -89,7 +89,7 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur
|
||||
- [`TODO.md`](TODO.md) — points explicitement différés après `0.1.3` ;
|
||||
- [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ;
|
||||
- [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ;
|
||||
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard HTTP Transport ;
|
||||
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard Transport V2 HTTP + WebSocket, avec lecture backward du V1 HTTP-only ;
|
||||
- [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ;
|
||||
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Transport/Wallet de Wallet Desk ;
|
||||
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
||||
<!-- version: 6 -->
|
||||
<!-- version: 7 -->
|
||||
|
||||
# Utilisation de ksp-config-lib
|
||||
|
||||
@@ -125,7 +125,7 @@ Un `logs_directory` relatif est ancré sur le current working directory du proce
|
||||
|
||||
Les `files[].path` restent relatifs sous le root Logging, y compris après interpolation.
|
||||
|
||||
### 4.1 Construire le Transport HTTP depuis Config
|
||||
### 4.1 Construire le Transport HTTP + WebSocket depuis Config
|
||||
|
||||
Config possède également l'adapter du document `std.transport` vers le contrat runtime de `ksp-onchain-transport-lib` :
|
||||
|
||||
@@ -135,12 +135,18 @@ let transport = match engine.load_resolved_transport_config(std::option::Option:
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
|
||||
let transport_settings = transport.into_settings();
|
||||
let http_settings = transport.http_settings();
|
||||
let ws_settings = transport.ws_settings();
|
||||
let _ = (http_settings, ws_settings);
|
||||
```
|
||||
|
||||
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. Les URLs peuvent provenir de `KSP_PUBLIC_*` ou de `KSP_SECRET_*`; dans ce dernier cas la valeur réelle reste disponible au runtime Transport, mais `ResolvedTransportConfig::effective().safe_value()` et les représentations `Debug` sont redacted.
|
||||
`std.transport` V2 conserve `retry` et `profiles[].endpoints[]` pour HTTP, ajoute `ws_defaults` et `profiles[].ws_endpoints[]`, puis exige `kind = "solana_standard"` en `0.2.7`. Un `ws_endpoints[].session` optionnel surcharge seulement les paramètres génériques de `WsSessionSettings`.
|
||||
|
||||
La dépendance reste unidirectionnelle : Config connaît le contrat Transport pour le construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP.
|
||||
Le même schema enregistré conserve la lecture stricte du V1 historique : dans ce cas `http_settings()` reste disponible et `ws_settings()` retourne `None`. Aucun `WsTransportSettings` vide n'est inventé pour simuler l'absence de WebSocket.
|
||||
|
||||
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. Les URLs HTTP et WebSocket peuvent provenir de `KSP_PUBLIC_*` ou de `KSP_SECRET_*`; dans ce dernier cas la valeur réelle reste disponible au runtime Transport, mais `ResolvedTransportConfig::effective().safe_value()` et les représentations `Debug` sont redacted.
|
||||
|
||||
La dépendance reste unidirectionnelle : Config connaît les contrats Transport pour les construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP.
|
||||
|
||||
### 4.2 Résoudre le répertoire Wallet depuis Config
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 16
|
||||
// version: 17
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -154,9 +154,9 @@ pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME;
|
||||
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
|
||||
/// Default physical filename for the standard Logging JSON Schema document.
|
||||
pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
|
||||
/// Default physical filename for the standard HTTP Transport configuration document.
|
||||
/// Default physical filename for the standard HTTP + WebSocket Transport configuration document.
|
||||
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
|
||||
/// Default physical filename for the standard HTTP Transport JSON Schema document.
|
||||
/// Default physical filename for the standard HTTP + WebSocket Transport JSON Schema document.
|
||||
pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME;
|
||||
/// Default physical filename for the standard Wallet configuration document.
|
||||
pub use self::registry::DEFAULT_STD_WALLET_FILENAME;
|
||||
@@ -168,13 +168,13 @@ pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
|
||||
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
|
||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
|
||||
/// Logical file identifier for the standard HTTP Transport JSON Schema document.
|
||||
/// Logical file identifier for the standard HTTP + WebSocket Transport JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
|
||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||
pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
|
||||
/// Logical file identifier for the standard Logging configuration document.
|
||||
pub use self::registry::FILE_ID_STD_LOGGING;
|
||||
/// Logical file identifier for the standard HTTP Transport configuration document.
|
||||
/// Logical file identifier for the standard HTTP + WebSocket Transport configuration document.
|
||||
pub use self::registry::FILE_ID_STD_TRANSPORT;
|
||||
/// Logical file identifier for the standard Wallet configuration document.
|
||||
pub use self::registry::FILE_ID_STD_WALLET;
|
||||
@@ -188,7 +188,7 @@ pub use self::sensitivity::REDACTED_CONFIG_VALUE;
|
||||
pub use self::sensitivity::ResolvedConfigJson;
|
||||
/// One resolved Config string preserving real/safe representations and provenance.
|
||||
pub use self::sensitivity::ResolvedConfigText;
|
||||
/// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`.
|
||||
/// Effective standard Transport configuration mapped to HTTP and optional WebSocket runtime settings.
|
||||
pub use self::transport::ResolvedTransportConfig;
|
||||
/// Effective standard Wallet configuration resolved to validated filesystem roots.
|
||||
pub use self::wallet::ResolvedWalletConfig;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/registry.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||
pub const ARG_FILE_MAP: &str = "--filemap";
|
||||
@@ -11,9 +11,9 @@ pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
|
||||
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
|
||||
/// Default physical filename for the standard Logging JSON Schema document.
|
||||
pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
|
||||
/// Default physical filename for the standard HTTP Transport configuration document.
|
||||
/// Default physical filename for the standard HTTP + WebSocket Transport configuration document.
|
||||
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
|
||||
/// Default physical filename for the standard HTTP Transport JSON Schema document.
|
||||
/// Default physical filename for the standard HTTP + WebSocket Transport JSON Schema document.
|
||||
pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json";
|
||||
/// Default physical filename for the standard Wallet configuration document.
|
||||
pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json";
|
||||
@@ -25,13 +25,13 @@ pub const FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK: &str = "cfg.composite.ksp-app-w
|
||||
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
|
||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
|
||||
/// Logical file identifier for the standard HTTP Transport JSON Schema document.
|
||||
/// Logical file identifier for the standard HTTP + WebSocket Transport JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
|
||||
/// Logical file identifier for the standard Wallet JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
|
||||
/// Logical file identifier for the standard Logging configuration document.
|
||||
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
|
||||
/// Logical file identifier for the standard HTTP Transport configuration document.
|
||||
/// Logical file identifier for the standard HTTP + WebSocket Transport configuration document.
|
||||
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
|
||||
/// Logical file identifier for the standard Wallet configuration document.
|
||||
pub const FILE_ID_STD_WALLET: &str = "cfg.std.wallet";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/ksp-config-lib/src/transport.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
/// Effective standard HTTP Transport configuration resolved from Config and mapped to the Transport runtime contract.
|
||||
/// Effective standard on-chain Transport configuration resolved from Config and mapped to HTTP and optional WebSocket runtime contracts.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct ResolvedTransportConfig {
|
||||
file_id: crate::ConfigFileId,
|
||||
@@ -10,6 +10,7 @@ pub struct ResolvedTransportConfig {
|
||||
selection_source: crate::ConfigProfileSelectionSource,
|
||||
effective: crate::ResolvedConfigJson,
|
||||
settings: ksp_onchain_transport_lib::HttpTransportSettings,
|
||||
ws_settings: std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>,
|
||||
}
|
||||
|
||||
impl ResolvedTransportConfig {
|
||||
@@ -47,16 +48,43 @@ impl ResolvedTransportConfig {
|
||||
}
|
||||
|
||||
/// Returns the validated runtime HTTP Transport settings.
|
||||
///
|
||||
/// This compatibility accessor keeps the HTTP contract introduced before Transport V2.
|
||||
#[must_use]
|
||||
pub const fn settings(&self) -> &ksp_onchain_transport_lib::HttpTransportSettings {
|
||||
return &self.settings;
|
||||
}
|
||||
|
||||
/// Returns the validated runtime HTTP Transport settings.
|
||||
#[must_use]
|
||||
pub const fn http_settings(&self) -> &ksp_onchain_transport_lib::HttpTransportSettings {
|
||||
return &self.settings;
|
||||
}
|
||||
|
||||
/// Returns validated WebSocket Transport settings when the selected document uses format V2.
|
||||
///
|
||||
/// Backward-compatible V1 HTTP-only documents return [`std::option::Option::None`].
|
||||
#[must_use]
|
||||
pub fn ws_settings(&self) -> std::option::Option<&ksp_onchain_transport_lib::WsTransportSettings> {
|
||||
return self.ws_settings.as_ref();
|
||||
}
|
||||
|
||||
/// Consumes this resolved Config and returns the mapped runtime HTTP Transport settings.
|
||||
#[must_use]
|
||||
pub fn into_settings(self) -> ksp_onchain_transport_lib::HttpTransportSettings {
|
||||
return self.settings;
|
||||
}
|
||||
|
||||
/// Consumes this resolved Config and returns both HTTP and optional WebSocket runtime settings.
|
||||
#[must_use]
|
||||
pub fn into_transport_settings(
|
||||
self,
|
||||
) -> (
|
||||
ksp_onchain_transport_lib::HttpTransportSettings,
|
||||
std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>,
|
||||
) {
|
||||
return (self.settings, self.ws_settings);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ResolvedTransportConfig {
|
||||
@@ -68,16 +96,16 @@ impl std::fmt::Debug for ResolvedTransportConfig {
|
||||
.field("profile_id", &self.profile_id)
|
||||
.field("selection_source", &self.selection_source)
|
||||
.field("effective", &self.effective)
|
||||
.field("has_ws_settings", &self.ws_settings.is_some())
|
||||
.finish_non_exhaustive();
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::ConfigDocumentEngine {
|
||||
/// Loads the standard HTTP Transport document, selects a profile, resolves environment placeholders and maps it to Transport runtime settings.
|
||||
/// Loads the standard Transport document, selects a profile, resolves environment placeholders and maps HTTP plus optional WebSocket runtime settings.
|
||||
///
|
||||
/// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. Secret endpoint URLs are allowed
|
||||
/// because the Transport URL wrapper owns runtime redaction. Invalid environment-resolved values are reported as
|
||||
/// [`crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID`] without copying endpoint URL values into ordinary error context.
|
||||
/// because Transport URL wrappers own runtime redaction. V1 documents remain HTTP-only; V2 documents require WebSocket defaults and endpoints.
|
||||
pub fn load_resolved_transport_config(
|
||||
&self,
|
||||
requested_profile: std::option::Option<&str>,
|
||||
@@ -96,7 +124,7 @@ impl crate::ConfigDocumentEngine {
|
||||
return resolve_transport_profile(&profile, environment);
|
||||
}
|
||||
|
||||
/// Maps an already resolved standard Transport profile to the runtime HTTP Transport adapter while preserving its selection provenance.
|
||||
/// Maps an already resolved standard Transport profile to HTTP plus optional WebSocket runtime adapters while preserving selection provenance.
|
||||
///
|
||||
/// This entry point is intended for profiles selected by a composite. The profile must reference `cfg.std.transport`.
|
||||
pub fn resolve_transport_config_profile(
|
||||
@@ -121,7 +149,11 @@ struct EffectiveTransportSource {
|
||||
format_version: u32,
|
||||
profile_id: String,
|
||||
retry: EffectiveRetrySource,
|
||||
#[serde(default)]
|
||||
ws_defaults: std::option::Option<EffectiveWsSessionSource>,
|
||||
endpoints: std::vec::Vec<EffectiveEndpointSource>,
|
||||
#[serde(default)]
|
||||
ws_endpoints: std::option::Option<std::vec::Vec<EffectiveWsEndpointSource>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@@ -165,7 +197,69 @@ struct EffectiveLimitsSource {
|
||||
pause_after_rate_limit_ms: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveWsSessionSource {
|
||||
command_timeout_ms: u64,
|
||||
close_timeout_ms: u64,
|
||||
reconnect: EffectiveWsReconnectSource,
|
||||
resubscribe: String,
|
||||
command_queue_capacity: usize,
|
||||
notification_queue_capacity: usize,
|
||||
max_active_subscriptions: usize,
|
||||
max_pending_requests: usize,
|
||||
max_message_size_bytes: usize,
|
||||
max_frame_size_bytes: usize,
|
||||
max_write_buffer_size_bytes: usize,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveWsReconnectSource {
|
||||
max_retries: u32,
|
||||
initial_backoff_ms: u64,
|
||||
max_backoff_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveWsEndpointSource {
|
||||
name: String,
|
||||
enabled: bool,
|
||||
provider: String,
|
||||
cluster: String,
|
||||
kind: String,
|
||||
url: String,
|
||||
#[serde(default)]
|
||||
session: std::option::Option<EffectiveWsSessionOverrideSource>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveWsSessionOverrideSource {
|
||||
command_timeout_ms: std::option::Option<u64>,
|
||||
close_timeout_ms: std::option::Option<u64>,
|
||||
reconnect: std::option::Option<EffectiveWsReconnectOverrideSource>,
|
||||
resubscribe: std::option::Option<String>,
|
||||
command_queue_capacity: std::option::Option<usize>,
|
||||
notification_queue_capacity: std::option::Option<usize>,
|
||||
max_active_subscriptions: std::option::Option<usize>,
|
||||
max_pending_requests: std::option::Option<usize>,
|
||||
max_message_size_bytes: std::option::Option<usize>,
|
||||
max_frame_size_bytes: std::option::Option<usize>,
|
||||
max_write_buffer_size_bytes: std::option::Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EffectiveWsReconnectOverrideSource {
|
||||
max_retries: std::option::Option<u32>,
|
||||
initial_backoff_ms: std::option::Option<u64>,
|
||||
max_backoff_ms: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedTransportConfig> {
|
||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapping standard Transport Config profile");
|
||||
let effective = profile.resolve_effective_environment_detailed(environment);
|
||||
let effective = match effective {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -180,12 +274,10 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
|
||||
);
|
||||
},
|
||||
};
|
||||
if source.format_version != 1 {
|
||||
return std::result::Result::Err(effective_error(profile, "effective Transport format_version is unsupported"));
|
||||
}
|
||||
if source.profile_id != profile.profile_id() {
|
||||
return std::result::Result::Err(effective_error(profile, "effective Transport profile_id does not match the selected profile"));
|
||||
}
|
||||
let format_version = source.format_version;
|
||||
let retry = ksp_onchain_transport_lib::HttpRetrySettings::new(
|
||||
source.retry.max_retries,
|
||||
std::time::Duration::from_millis(source.retry.initial_backoff_ms),
|
||||
@@ -197,14 +289,24 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(endpoints, retry);
|
||||
let validation = settings.validate();
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(transport_contract_error(profile, "effective Transport settings fail the Transport runtime contract", &error));
|
||||
if let std::result::Result::Err(error) = settings.validate() {
|
||||
return std::result::Result::Err(transport_contract_error(profile, "effective HTTP Transport settings fail the Transport runtime contract", &error));
|
||||
}
|
||||
let ws_settings = map_optional_ws_settings(format_version, source.ws_defaults, source.ws_endpoints, profile);
|
||||
let ws_settings = match ws_settings {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let ws_endpoint_count = match ws_settings.as_ref() {
|
||||
std::option::Option::Some(value) => value.endpoints().len(),
|
||||
std::option::Option::None => 0_usize,
|
||||
};
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
profile_id = profile.profile_id(),
|
||||
endpoint_count = settings.endpoints().len(),
|
||||
format_version,
|
||||
http_endpoint_count = settings.endpoints().len(),
|
||||
ws_endpoint_count,
|
||||
"mapped standard Transport Config to runtime settings"
|
||||
);
|
||||
return std::result::Result::Ok(ResolvedTransportConfig {
|
||||
@@ -214,9 +316,49 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
|
||||
selection_source: profile.selection_source(),
|
||||
effective,
|
||||
settings,
|
||||
ws_settings,
|
||||
});
|
||||
}
|
||||
|
||||
fn map_optional_ws_settings(
|
||||
format_version: u32,
|
||||
defaults: std::option::Option<EffectiveWsSessionSource>,
|
||||
sources: std::option::Option<std::vec::Vec<EffectiveWsEndpointSource>>,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
) -> ksp_core_lib::Result<std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>> {
|
||||
return match format_version {
|
||||
1 => {
|
||||
if defaults.is_some() || sources.is_some() {
|
||||
std::result::Result::Err(effective_error(profile, "Transport V1 must remain HTTP-only"))
|
||||
} else {
|
||||
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapped backward-compatible Transport V1 without WebSocket settings");
|
||||
std::result::Result::Ok(std::option::Option::None)
|
||||
}
|
||||
},
|
||||
2 => {
|
||||
let defaults = match defaults {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2 requires ws_defaults")),
|
||||
};
|
||||
let sources = match sources {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2 profile requires ws_endpoints")),
|
||||
};
|
||||
let endpoints = map_ws_endpoints(sources, &defaults, profile);
|
||||
let endpoints = match endpoints {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let settings = ksp_onchain_transport_lib::WsTransportSettings::new(endpoints);
|
||||
if let std::result::Result::Err(error) = settings.validate() {
|
||||
return std::result::Result::Err(transport_contract_error(profile, "effective WebSocket Transport settings fail the Transport runtime contract", &error));
|
||||
}
|
||||
std::result::Result::Ok(std::option::Option::Some(settings))
|
||||
},
|
||||
_ => std::result::Result::Err(effective_error(profile, "effective Transport format_version is unsupported")),
|
||||
};
|
||||
}
|
||||
|
||||
fn map_endpoints(
|
||||
sources: std::vec::Vec<EffectiveEndpointSource>,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
@@ -229,7 +371,7 @@ fn map_endpoints(
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
transport_contract_error(profile, "effective endpoint URL is invalid", &error).with_context("endpoint_name", endpoint_name),
|
||||
transport_contract_error(profile, "effective HTTP endpoint URL is invalid", &error).with_context("endpoint_name", endpoint_name),
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -253,6 +395,171 @@ fn map_endpoints(
|
||||
return std::result::Result::Ok(endpoints);
|
||||
}
|
||||
|
||||
fn map_ws_endpoints(
|
||||
sources: std::vec::Vec<EffectiveWsEndpointSource>,
|
||||
defaults: &EffectiveWsSessionSource,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::WsEndpointSettings>> {
|
||||
let mut endpoints = std::vec::Vec::<ksp_onchain_transport_lib::WsEndpointSettings>::with_capacity(sources.len());
|
||||
for source in sources {
|
||||
let endpoint_name = source.name.clone();
|
||||
let protocol = map_ws_protocol_kind(source.kind.as_str(), profile, endpoint_name.as_str());
|
||||
let protocol = match protocol {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let url = ksp_onchain_transport_lib::WsEndpointUrl::parse(source.url);
|
||||
let url = match url {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
transport_contract_error(profile, "effective WebSocket endpoint URL is invalid", &error).with_context("endpoint_name", endpoint_name),
|
||||
);
|
||||
},
|
||||
};
|
||||
let session = map_ws_session_settings(defaults, source.session.as_ref(), profile, endpoint_name.as_str());
|
||||
let session = match session {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
endpoints.push(ksp_onchain_transport_lib::WsEndpointSettings::new(
|
||||
source.name,
|
||||
source.enabled,
|
||||
ksp_onchain_transport_lib::WsProviderName::new(source.provider),
|
||||
ksp_onchain_transport_lib::WsClusterName::new(source.cluster),
|
||||
protocol,
|
||||
url,
|
||||
session,
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(endpoints);
|
||||
}
|
||||
|
||||
fn map_ws_protocol_kind(
|
||||
value: &str,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
endpoint_name: &str,
|
||||
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsProtocolKind> {
|
||||
return match value {
|
||||
"solana_standard" => std::result::Result::Ok(ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard),
|
||||
_ => std::result::Result::Err(
|
||||
effective_error(profile, "effective WebSocket protocol kind is unsupported")
|
||||
.with_context("endpoint_name", endpoint_name)
|
||||
.with_context("ws_kind", value),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn map_ws_session_settings(
|
||||
defaults: &EffectiveWsSessionSource,
|
||||
overrides: std::option::Option<&EffectiveWsSessionOverrideSource>,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
endpoint_name: &str,
|
||||
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsSessionSettings> {
|
||||
let mut command_timeout_ms = defaults.command_timeout_ms;
|
||||
let mut close_timeout_ms = defaults.close_timeout_ms;
|
||||
let mut reconnect_max_retries = defaults.reconnect.max_retries;
|
||||
let mut reconnect_initial_backoff_ms = defaults.reconnect.initial_backoff_ms;
|
||||
let mut reconnect_max_backoff_ms = defaults.reconnect.max_backoff_ms;
|
||||
let mut resubscribe_text = defaults.resubscribe.clone();
|
||||
let mut command_queue_capacity = defaults.command_queue_capacity;
|
||||
let mut notification_queue_capacity = defaults.notification_queue_capacity;
|
||||
let mut max_active_subscriptions = defaults.max_active_subscriptions;
|
||||
let mut max_pending_requests = defaults.max_pending_requests;
|
||||
let mut max_message_size_bytes = defaults.max_message_size_bytes;
|
||||
let mut max_frame_size_bytes = defaults.max_frame_size_bytes;
|
||||
let mut max_write_buffer_size_bytes = defaults.max_write_buffer_size_bytes;
|
||||
if let std::option::Option::Some(overrides) = overrides {
|
||||
if let std::option::Option::Some(value) = overrides.command_timeout_ms {
|
||||
command_timeout_ms = value;
|
||||
}
|
||||
if let std::option::Option::Some(value) = overrides.close_timeout_ms {
|
||||
close_timeout_ms = value;
|
||||
}
|
||||
if let std::option::Option::Some(reconnect) = overrides.reconnect.as_ref() {
|
||||
if let std::option::Option::Some(value) = reconnect.max_retries {
|
||||
reconnect_max_retries = value;
|
||||
}
|
||||
if let std::option::Option::Some(value) = reconnect.initial_backoff_ms {
|
||||
reconnect_initial_backoff_ms = value;
|
||||
}
|
||||
if let std::option::Option::Some(value) = reconnect.max_backoff_ms {
|
||||
reconnect_max_backoff_ms = value;
|
||||
}
|
||||
}
|
||||
if let std::option::Option::Some(value) = overrides.resubscribe.as_ref() {
|
||||
resubscribe_text = value.clone();
|
||||
}
|
||||
if let std::option::Option::Some(value) = overrides.command_queue_capacity {
|
||||
command_queue_capacity = value;
|
||||
}
|
||||
if let std::option::Option::Some(value) = overrides.notification_queue_capacity {
|
||||
notification_queue_capacity = value;
|
||||
}
|
||||
if let std::option::Option::Some(value) = overrides.max_active_subscriptions {
|
||||
max_active_subscriptions = value;
|
||||
}
|
||||
if let std::option::Option::Some(value) = overrides.max_pending_requests {
|
||||
max_pending_requests = value;
|
||||
}
|
||||
if let std::option::Option::Some(value) = overrides.max_message_size_bytes {
|
||||
max_message_size_bytes = value;
|
||||
}
|
||||
if let std::option::Option::Some(value) = overrides.max_frame_size_bytes {
|
||||
max_frame_size_bytes = value;
|
||||
}
|
||||
if let std::option::Option::Some(value) = overrides.max_write_buffer_size_bytes {
|
||||
max_write_buffer_size_bytes = value;
|
||||
}
|
||||
}
|
||||
let reconnect = ksp_onchain_transport_lib::WsReconnectSettings::new(
|
||||
reconnect_max_retries,
|
||||
std::time::Duration::from_millis(reconnect_initial_backoff_ms),
|
||||
std::time::Duration::from_millis(reconnect_max_backoff_ms),
|
||||
);
|
||||
let resubscribe = map_ws_resubscribe_policy(resubscribe_text.as_str(), profile, endpoint_name);
|
||||
let resubscribe = match resubscribe {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let settings = ksp_onchain_transport_lib::WsSessionSettings::new(
|
||||
std::time::Duration::from_millis(command_timeout_ms),
|
||||
std::time::Duration::from_millis(close_timeout_ms),
|
||||
reconnect,
|
||||
resubscribe,
|
||||
command_queue_capacity,
|
||||
notification_queue_capacity,
|
||||
max_active_subscriptions,
|
||||
max_pending_requests,
|
||||
max_message_size_bytes,
|
||||
max_frame_size_bytes,
|
||||
max_write_buffer_size_bytes,
|
||||
);
|
||||
if let std::result::Result::Err(error) = settings.validate() {
|
||||
return std::result::Result::Err(
|
||||
transport_contract_error(profile, "effective WebSocket session settings fail the Transport runtime contract", &error)
|
||||
.with_context("endpoint_name", endpoint_name),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(settings);
|
||||
}
|
||||
|
||||
fn map_ws_resubscribe_policy(
|
||||
value: &str,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
endpoint_name: &str,
|
||||
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsResubscribePolicy> {
|
||||
return match value {
|
||||
"never" => std::result::Result::Ok(ksp_onchain_transport_lib::WsResubscribePolicy::Never),
|
||||
"active_subscriptions" => std::result::Result::Ok(ksp_onchain_transport_lib::WsResubscribePolicy::ActiveSubscriptions),
|
||||
_ => std::result::Result::Err(
|
||||
effective_error(profile, "effective WebSocket resubscribe policy is unsupported")
|
||||
.with_context("endpoint_name", endpoint_name)
|
||||
.with_context("resubscribe", value),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn map_roles(
|
||||
sources: std::vec::Vec<EffectiveRoleSource>,
|
||||
profile: &crate::ResolvedConfigProfile,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
||||
// version: 21
|
||||
// version: 22
|
||||
|
||||
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
|
||||
//! Logging/Transport adapters and management contracts.
|
||||
@@ -252,6 +252,9 @@ fn transport_adapter_contract_is_available_from_crate_root() {
|
||||
let _loader = ksp_config_lib::ConfigDocumentEngine::load_resolved_transport_config;
|
||||
let _composite_loader = ksp_config_lib::ConfigDocumentEngine::resolve_transport_config_profile;
|
||||
assert!(std::mem::size_of::<ksp_config_lib::ResolvedTransportConfig>() > 0);
|
||||
let _http_settings = ksp_config_lib::ResolvedTransportConfig::http_settings;
|
||||
let _ws_settings = ksp_config_lib::ResolvedTransportConfig::ws_settings;
|
||||
let _into_transport_settings = ksp_config_lib::ResolvedTransportConfig::into_transport_settings;
|
||||
assert_eq!(ksp_config_lib::FILE_ID_STD_TRANSPORT, "cfg.std.transport");
|
||||
assert_eq!(ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT, "schema.std.transport");
|
||||
assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_FILENAME, "std.transport.json");
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"format_version": 2,
|
||||
"retry": {
|
||||
"max_retries": 4,
|
||||
"initial_backoff_ms": 125,
|
||||
"max_backoff_ms": 2500
|
||||
},
|
||||
"ws_defaults": {
|
||||
"command_timeout_ms": 8000,
|
||||
"close_timeout_ms": 4000,
|
||||
"reconnect": {
|
||||
"max_retries": 6,
|
||||
"initial_backoff_ms": 200,
|
||||
"max_backoff_ms": 4000
|
||||
},
|
||||
"resubscribe": "active_subscriptions",
|
||||
"command_queue_capacity": 64,
|
||||
"notification_queue_capacity": 96,
|
||||
"max_active_subscriptions": 256,
|
||||
"max_pending_requests": 48,
|
||||
"max_message_size_bytes": 33554432,
|
||||
"max_frame_size_bytes": 8388608,
|
||||
"max_write_buffer_size_bytes": 524288
|
||||
},
|
||||
"default_profile": "secret_test",
|
||||
"profiles": [
|
||||
{
|
||||
@@ -23,7 +40,9 @@
|
||||
{
|
||||
"role": "default",
|
||||
"enabled": true,
|
||||
"request_kinds": ["*"],
|
||||
"request_kinds": [
|
||||
"*"
|
||||
],
|
||||
"priority": 7,
|
||||
"limits": {
|
||||
"requests_per_second": 9,
|
||||
@@ -34,6 +53,25 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"ws_endpoints": [
|
||||
{
|
||||
"name": "fixture_private_ws",
|
||||
"enabled": true,
|
||||
"provider": "fixture-provider",
|
||||
"cluster": "fixture-cluster",
|
||||
"kind": "solana_standard",
|
||||
"url": "${KSP_SECRET_TRANSPORT_TEST_WS_URL:-wss://fallback.invalid}",
|
||||
"session": {
|
||||
"command_timeout_ms": 4500,
|
||||
"reconnect": {
|
||||
"max_retries": 7
|
||||
},
|
||||
"resubscribe": "never",
|
||||
"notification_queue_capacity": 48,
|
||||
"max_pending_requests": 24
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"format_version": 1,
|
||||
"retry": {
|
||||
"max_retries": 1,
|
||||
"initial_backoff_ms": 90,
|
||||
"max_backoff_ms": 900
|
||||
},
|
||||
"default_profile": "legacy_http",
|
||||
"profiles": [
|
||||
{
|
||||
"profile_id": "legacy_http",
|
||||
"endpoints": [
|
||||
{
|
||||
"name": "legacy_http",
|
||||
"enabled": true,
|
||||
"provider": "legacy-provider",
|
||||
"cluster": "devnet",
|
||||
"url": "https://legacy.invalid",
|
||||
"connect_timeout_ms": 5000,
|
||||
"request_timeout_ms": 2200,
|
||||
"max_idle_connections_per_host": 2,
|
||||
"roles": [
|
||||
{
|
||||
"role": "default",
|
||||
"enabled": true,
|
||||
"request_kinds": [
|
||||
"*"
|
||||
],
|
||||
"priority": 10,
|
||||
"limits": {
|
||||
"requests_per_second": 2,
|
||||
"burst_capacity": 3,
|
||||
"max_concurrent_requests": 2,
|
||||
"pause_after_rate_limit_ms": 250
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/transport.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn fixture_transport_profile_maps_complete_runtime_contract() {
|
||||
@@ -41,6 +41,49 @@ fn fixture_transport_profile_maps_complete_runtime_contract() {
|
||||
assert_eq!(role.limits().burst_capacity().map(std::num::NonZeroU32::get), std::option::Option::Some(12));
|
||||
assert_eq!(role.limits().max_concurrent_requests().map(std::num::NonZeroU32::get), std::option::Option::Some(4));
|
||||
assert_eq!(role.limits().pause_after_rate_limit(), std::option::Option::Some(std::time::Duration::from_millis(650)));
|
||||
let ws = resolved.ws_settings();
|
||||
assert!(ws.is_some(), "V2 fixture should expose WebSocket settings");
|
||||
if let std::option::Option::Some(ws) = ws {
|
||||
assert_eq!(ws.endpoints().len(), 1);
|
||||
let endpoint = &ws.endpoints()[0];
|
||||
assert_eq!(endpoint.name(), "fixture_private_ws");
|
||||
assert_eq!(endpoint.provider().as_str(), "fixture-provider");
|
||||
assert_eq!(endpoint.cluster().as_str(), "fixture-cluster");
|
||||
assert_eq!(endpoint.protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
|
||||
assert_eq!(endpoint.url().as_str(), "wss://fallback.invalid");
|
||||
assert_eq!(endpoint.session().command_timeout(), std::time::Duration::from_millis(4500));
|
||||
assert_eq!(endpoint.session().close_timeout(), std::time::Duration::from_millis(4000));
|
||||
assert_eq!(endpoint.session().reconnect().max_retries(), 7);
|
||||
assert_eq!(endpoint.session().reconnect().initial_backoff(), std::time::Duration::from_millis(200));
|
||||
assert_eq!(endpoint.session().reconnect().max_backoff(), std::time::Duration::from_millis(4000));
|
||||
assert_eq!(endpoint.session().resubscribe(), ksp_onchain_transport_lib::WsResubscribePolicy::Never);
|
||||
assert_eq!(endpoint.session().command_queue_capacity(), 64);
|
||||
assert_eq!(endpoint.session().notification_queue_capacity(), 48);
|
||||
assert_eq!(endpoint.session().max_active_subscriptions(), 256);
|
||||
assert_eq!(endpoint.session().max_pending_requests(), 24);
|
||||
assert_eq!(endpoint.session().max_message_size_bytes(), 33_554_432);
|
||||
assert_eq!(endpoint.session().max_frame_size_bytes(), 8_388_608);
|
||||
assert_eq!(endpoint.session().max_write_buffer_size_bytes(), 524_288);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v1_transport_fixture_remains_backward_readable_and_http_only() {
|
||||
let engine = v1_fixture_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_transport_config(std::option::Option::None, &environment);
|
||||
assert!(resolved.is_ok(), "strict Transport V1 fixture should remain readable: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.profile_id(), "legacy_http");
|
||||
assert_eq!(resolved.settings().retry().max_retries(), 1);
|
||||
assert_eq!(resolved.settings().endpoints().len(), 1);
|
||||
assert_eq!(resolved.settings().endpoints()[0].url().as_str(), "https://legacy.invalid");
|
||||
assert!(resolved.ws_settings().is_none(), "V1 must not invent WebSocket runtime settings");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -59,12 +102,23 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
|
||||
assert_eq!(default.profile_id(), "devnet_public");
|
||||
assert_eq!(default.settings().endpoints()[0].cluster().as_str(), "devnet");
|
||||
assert_eq!(default.settings().endpoints()[0].url().as_str(), "https://api.devnet.solana.com");
|
||||
let ws = default.ws_settings();
|
||||
assert!(ws.is_some(), "committed V2 Devnet profile should expose WebSocket settings");
|
||||
if let std::option::Option::Some(ws) = ws {
|
||||
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.devnet.solana.com");
|
||||
assert_eq!(ws.endpoints()[0].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
|
||||
}
|
||||
}
|
||||
if let std::result::Result::Ok(mainnet) = mainnet {
|
||||
assert_eq!(mainnet.profile_id(), "mainnet_public");
|
||||
assert_eq!(mainnet.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
|
||||
assert_eq!(mainnet.settings().endpoints()[0].cluster().as_str(), "mainnet-beta");
|
||||
assert_eq!(mainnet.settings().endpoints()[0].url().as_str(), "https://api.mainnet-beta.solana.com");
|
||||
let ws = mainnet.ws_settings();
|
||||
assert!(ws.is_some(), "committed V2 Mainnet profile should expose WebSocket settings");
|
||||
if let std::option::Option::Some(ws) = ws {
|
||||
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.mainnet-beta.solana.com");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +138,9 @@ fn transport_profile_preserves_global_and_profile_origin() {
|
||||
assert!(profile.is_ok(), "committed Transport profile should resolve: {profile:?}");
|
||||
if let std::result::Result::Ok(profile) = profile {
|
||||
assert_eq!(profile.origin("retry"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
||||
assert_eq!(profile.origin("ws_defaults"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
||||
assert_eq!(profile.origin("endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
|
||||
assert_eq!(profile.origin("ws_endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
|
||||
assert_eq!(profile.origin("format_version"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
||||
}
|
||||
}
|
||||
@@ -149,6 +205,36 @@ fn secret_transport_url_is_runtime_available_but_safe_projection_is_redacted() {
|
||||
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_websocket_url_is_runtime_available_but_safe_projection_is_redacted() {
|
||||
let engine = fixture_engine();
|
||||
let engine = match engine {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let canary = "wss://user:pass@secret-provider.invalid/path?api-key=transport-ws-secret-canary";
|
||||
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||
process.insert("KSP_SECRET_TRANSPORT_TEST_WS_URL".to_owned(), canary.to_owned());
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment);
|
||||
assert!(resolved.is_ok(), "secret WebSocket endpoint should map without being exposed: {resolved:?}");
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let ws = resolved.ws_settings();
|
||||
assert!(ws.is_some(), "V2 fixture should expose WebSocket settings");
|
||||
if let std::option::Option::Some(ws) = ws {
|
||||
assert_eq!(ws.endpoints()[0].url().as_str(), canary);
|
||||
}
|
||||
let safe_url = resolved.effective().safe_value().pointer("/ws_endpoints/0/url").and_then(serde_json::Value::as_str);
|
||||
assert_eq!(safe_url, std::option::Option::Some(crate::REDACTED_CONFIG_VALUE));
|
||||
let debug = format!("{resolved:?}");
|
||||
assert!(!debug.contains("transport-ws-secret-canary"));
|
||||
assert!(!debug.contains("user:pass"));
|
||||
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_secret_url_provenance_uses_process_and_process_beats_dotenv() {
|
||||
let engine = fixture_engine();
|
||||
@@ -216,6 +302,22 @@ fn fixture_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
|
||||
}
|
||||
|
||||
fn v1_fixture_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||
let workspace = workspace_root();
|
||||
let fixture_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures_v1");
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture_root, 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 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"));
|
||||
|
||||
225
deltas/0.2.7/pre.003.md
Normal file
225
deltas/0.2.7/pre.003.md
Normal file
@@ -0,0 +1,225 @@
|
||||
<!-- file: deltas/0.2.7/pre.003.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.2.7-pre.003` — `std.transport` V2 HTTP + WebSocket
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
```text
|
||||
0.2.7-pre.002-fix.001 appliquée
|
||||
workspace.package.version = 0.2.7-pre.2.fix.1
|
||||
```
|
||||
|
||||
Le checkpoint opérateur reçu avant cette tranche est entièrement vert : `cargo fmt`, audit Python, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, tests Transport ciblés et `cargo test --workspace`.
|
||||
|
||||
## 2. Signal technique
|
||||
|
||||
Cette prerelease non-fix modifie configuration exécutable, Rust et tests. Conformément à `VER-ID-009` :
|
||||
|
||||
```text
|
||||
livraison = 0.2.7-pre.003
|
||||
workspace.package.version = 0.2.7-pre.3
|
||||
commit = v0.2.7-pre.003
|
||||
```
|
||||
|
||||
Aucun tag prerelease.
|
||||
|
||||
## 3. `std.transport` V2
|
||||
|
||||
Le document standard livré devient :
|
||||
|
||||
```text
|
||||
format_version = 2
|
||||
retry
|
||||
ws_defaults
|
||||
default_profile
|
||||
profiles[]
|
||||
profile_id
|
||||
endpoints[]
|
||||
ws_endpoints[]
|
||||
kind
|
||||
session?
|
||||
```
|
||||
|
||||
Le HTTP existant reste inchangé dans `endpoints[]`. `ws_defaults` contient les defaults génériques de `WsSessionSettings`; `ws_endpoints[].session` peut surcharger seulement les paramètres génériques nécessaires à un endpoint.
|
||||
|
||||
Le discriminateur est obligatoire :
|
||||
|
||||
```text
|
||||
kind = solana_standard
|
||||
```
|
||||
|
||||
Toute autre famille est rejetée en `0.2.7`. Aucun paramètre Helius, LaserStream ou autre provider-specific n'est pré-implémenté.
|
||||
|
||||
## 4. Backward V1 strict
|
||||
|
||||
Le schema enregistré passe à :
|
||||
|
||||
```text
|
||||
urn:ksp:schema:std.transport:v2
|
||||
```
|
||||
|
||||
Il conserve deux branches strictes discriminées par `format_version` :
|
||||
|
||||
```text
|
||||
V1 -> HTTP-only historique
|
||||
V2 -> HTTP + ws_defaults + ws_endpoints
|
||||
```
|
||||
|
||||
Le V1 n'est pas rendu compatible par un relâchement de `additionalProperties`. Une fixture V1 dédiée prouve le chemin historique.
|
||||
|
||||
Dans l'adapter :
|
||||
|
||||
```text
|
||||
V1 -> HttpTransportSettings + ws_settings = None
|
||||
V2 -> HttpTransportSettings + Some(WsTransportSettings)
|
||||
```
|
||||
|
||||
`WsTransportSettings` conserve donc son invariant `pre.002` : il n'existe jamais comme faux conteneur vide.
|
||||
|
||||
## 5. Adapter Config -> Transport
|
||||
|
||||
`ResolvedTransportConfig` conserve `settings()` pour compatibilité HTTP et ajoute :
|
||||
|
||||
```text
|
||||
http_settings()
|
||||
ws_settings() -> Option<&WsTransportSettings>
|
||||
into_transport_settings() -> (HttpTransportSettings, Option<WsTransportSettings>)
|
||||
```
|
||||
|
||||
Le mapper V2 construit :
|
||||
|
||||
```text
|
||||
WsProviderName
|
||||
WsClusterName
|
||||
WsProtocolKind::SolanaStandard
|
||||
WsEndpointUrl
|
||||
WsReconnectSettings
|
||||
WsResubscribePolicy
|
||||
WsSessionSettings
|
||||
WsEndpointSettings
|
||||
WsTransportSettings
|
||||
```
|
||||
|
||||
La direction reste strictement `Config -> Transport`. Aucun import de Config n'est ajouté à `ksp-onchain-transport-lib`.
|
||||
|
||||
## 6. Overrides session
|
||||
|
||||
Les valeurs de `ws_defaults` correspondent aux defaults KSP matérialisés en `pre.002`. Un endpoint peut surcharger indépendamment :
|
||||
|
||||
```text
|
||||
command_timeout_ms
|
||||
close_timeout_ms
|
||||
reconnect.{max_retries, initial_backoff_ms, max_backoff_ms}
|
||||
resubscribe
|
||||
command_queue_capacity
|
||||
notification_queue_capacity
|
||||
max_active_subscriptions
|
||||
max_pending_requests
|
||||
max_message_size_bytes
|
||||
max_frame_size_bytes
|
||||
max_write_buffer_size_bytes
|
||||
```
|
||||
|
||||
La session résultante est toujours validée par `WsSessionSettings::validate()`.
|
||||
|
||||
## 7. Secrets et logging
|
||||
|
||||
Les URLs HTTP et WebSocket peuvent provenir de `KSP_SECRET_*`; les valeurs réelles restent disponibles au runtime mais la projection Config safe les redacted. Les erreurs d'adaptation ne recopient pas l'URL.
|
||||
|
||||
Le mapping Config utilise exclusivement `ksp-logging-lib` avec le `TRACING_TARGET` existant de `ksp-config-lib` :
|
||||
|
||||
```text
|
||||
trace -> début mapping et chemin backward V1
|
||||
debug -> version, compteurs HTTP/WS après validation
|
||||
```
|
||||
|
||||
Aucun `tracing` direct n'est ajouté.
|
||||
|
||||
## 8. Environment inventory
|
||||
|
||||
Ajouts `.env.example` :
|
||||
|
||||
```text
|
||||
KSP_PUBLIC_SOLANA_DEVNET_WS_URL
|
||||
KSP_PUBLIC_SOLANA_MAINNET_WS_URL
|
||||
# KSP_SECRET_SOLANA_WS_URL
|
||||
```
|
||||
|
||||
Les URLs provider privées restent des valeurs complètes gérées via `KSP_SECRET_*`.
|
||||
|
||||
## 9. Tests ajoutés/étendus
|
||||
|
||||
```text
|
||||
V2 fixture HTTP + WS complète
|
||||
ws_defaults + endpoint overrides
|
||||
V1 strict toujours chargeable
|
||||
V1 n'invente pas de WsTransportSettings vide
|
||||
committed Devnet/Mainnet V2 mappe HTTP + WS
|
||||
provenance ws_defaults globale et ws_endpoints profil
|
||||
secret WebSocket URL disponible au runtime mais redacted en safe/Debug
|
||||
public API canary pour les nouveaux accessors
|
||||
```
|
||||
|
||||
## 10. Fichiers principaux modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
.env.example
|
||||
config/std.transport.json
|
||||
config/examples/std.transport.example.json
|
||||
config/schemas/std.transport.schema.json
|
||||
crates/ksp-config-lib/src/transport.rs
|
||||
crates/ksp-config-lib/src/lib.rs
|
||||
crates/ksp-config-lib/src/registry.rs
|
||||
crates/ksp-config-lib/unit_tests/transport.rs
|
||||
crates/ksp-config-lib/unit_tests/fixtures/std.transport.json
|
||||
crates/ksp-config-lib/tests/public_api.rs
|
||||
crates/ksp-config-lib/README.md
|
||||
crates/ksp-config-lib/USAGE.md
|
||||
docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md
|
||||
docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md
|
||||
```
|
||||
|
||||
Nouveau :
|
||||
|
||||
```text
|
||||
crates/ksp-config-lib/unit_tests/fixtures_v1/std.transport.json
|
||||
deltas/0.2.7/pre.003.md
|
||||
```
|
||||
|
||||
Aucune dépendance externe n'est ajoutée.
|
||||
|
||||
## 11. Validation de préparation
|
||||
|
||||
Le sandbox de génération ne fournit pas Cargo. Sont exécutés ici :
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
validation JSON des documents/schema
|
||||
inspection absence de tracing direct
|
||||
inspection version workspace
|
||||
inspection archive delta
|
||||
```
|
||||
|
||||
Les gates compilés restent opérateur-only.
|
||||
|
||||
## 12. Gates opérateur avant commit
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-config-lib
|
||||
cargo test -p ksp-onchain-transport-lib
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Si le checkpoint est vert :
|
||||
|
||||
```text
|
||||
commit = v0.2.7-pre.003
|
||||
```
|
||||
|
||||
La tranche suivante est `0.2.7-pre.004` : dépendances WebSocket, actor physique, handshake/read/write, pending JSON-RPC et serveur local déterministe.
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- file: docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# Plan `0.2.7` — WebSocket Solana standard
|
||||
|
||||
> **Statut : actif, `0.2.7-pre.002`.** Le gate `pre.001` est clos. `pre.002` matérialise les settings WebSocket Transport, la redaction URL, le discriminateur de protocole, les identités locales, les états lifecycle et les snapshots sûrs. Le socket physique reste différé à `pre.004`.
|
||||
> **Statut : actif, `0.2.7-pre.003`.** Les gates `pre.001` et `pre.002` sont clos. `pre.003` matérialise `std.transport` V2 HTTP + WebSocket, la lecture backward V1 HTTP-only et l'adapter `Config -> WsTransportSettings`. Le socket physique reste différé à `pre.004`.
|
||||
|
||||
## 1. Objet et base vérifiée
|
||||
|
||||
@@ -110,9 +110,9 @@ Aucun gate Cargo n'est déclaré réussi. Une validation opérateur est obligato
|
||||
|
||||
La surface WebSocket doit être additive : aucune renormalisation invasive des types `Http*` n'est requise dans `0.2.7`.
|
||||
|
||||
### 4.2 Config actuel est explicitement HTTP V1
|
||||
### 4.2 Config hérité et évolution V2
|
||||
|
||||
`config/std.transport.json`, son schema et `ksp-config-lib/src/transport.rs` décrivent un document V1 HTTP :
|
||||
À l’ouverture de `0.2.7`, `config/std.transport.json`, son schema et `ksp-config-lib/src/transport.rs` décrivaient un document V1 HTTP :
|
||||
|
||||
```text
|
||||
format_version = 1
|
||||
@@ -124,7 +124,7 @@ format_version != 1 rejeté
|
||||
|
||||
Décision : **ne pas ajouter silencieusement des clés WebSocket au V1**.
|
||||
|
||||
`0.2.7` fera évoluer le même composant standard `std.transport` vers un **format V2 explicitement HTTP + WebSocket**, tout en conservant la lecture du V1 HTTP-only pour compatibilité. Config reste propriétaire du parsing/résolution et adapte ensuite vers les settings publics Transport.
|
||||
`pre.003` fait évoluer le même composant standard `std.transport` vers un **format V2 explicitement HTTP + WebSocket**, tout en conservant la lecture du V1 HTTP-only pour compatibilité. Config reste propriétaire du parsing/résolution et adapte ensuite vers les settings publics Transport.
|
||||
|
||||
Shape cible minimale :
|
||||
|
||||
@@ -146,7 +146,21 @@ Décision d'extensibilité : **le conteneur V2 ne doit pas supposer qu'il n'exis
|
||||
|
||||
Cette préparation ne promet aucune option Helius dans `0.2.7`. L'audit provider-specific de la release suivante décidera la relation exacte entre Helius Enhanced WebSocket, LaserStream et le moteur standard avant d'ajouter des variantes/configurations concrètes.
|
||||
|
||||
Le V1 chargé sous `0.2.7` produit une configuration HTTP valide et une collection WebSocket vide. Le V2 devient le format livré par les fixtures Config dès la tranche qui matérialise le WebSocket.
|
||||
Le V1 chargé sous `0.2.7` produit une configuration HTTP valide et aucune instance `WsTransportSettings`. Le V2 devient le format livré par les fixtures Config dès la tranche qui matérialise le WebSocket.
|
||||
|
||||
Checkpoint `pre.003` matérialisé :
|
||||
|
||||
```text
|
||||
std.transport schema id = urn:ksp:schema:std.transport:v2
|
||||
V1 strict HTTP-only -> toujours accepté
|
||||
V2 strict HTTP + WS -> format livré
|
||||
ws_defaults -> session defaults génériques
|
||||
ws_endpoints[].session -> overrides génériques optionnels
|
||||
ws_endpoints[].kind -> solana_standard uniquement en 0.2.7
|
||||
ResolvedTransportConfig -> HTTP toujours présent + WS Option pour backward V1
|
||||
```
|
||||
|
||||
Le schema V2 utilise deux branches strictes discriminées par `format_version`; la compatibilité V1 n'est donc pas obtenue en relâchant `additionalProperties`. Les overrides endpoint ne contiennent que des paramètres génériques de `WsSessionSettings`, jamais des options Helius/provider-specific.
|
||||
|
||||
## 5. Audit historique bot3
|
||||
|
||||
@@ -853,7 +867,7 @@ L'inventaire officiel n'impose que 9 familles de subscriptions, mais le lifecycl
|
||||
```text
|
||||
pre.001 audit interne/externe + matrice 18 méthodes + bot3 + dependencies + threat model + plan/sizing
|
||||
pre.002 DONE — settings WS Transport + URL redaction + IDs/states/snapshots + tests de settings
|
||||
pre.003 std.transport V2 HTTP+WS + backward V1 + discriminateur WS + schema/fixtures + Config -> WsTransportSettings
|
||||
pre.003 DONE — std.transport V2 HTTP+WS + backward V1 + discriminateur WS + schema/fixtures + Config -> WsTransportSettings
|
||||
pre.004 deps tokio-tungstenite/futures-util + actor physique + handshake/read/write + pending JSON-RPC + serveur local
|
||||
pre.005 limites frame/message/request + control frames + cancellation/close/shutdown + adversarial socket tests
|
||||
pre.006 registry subscriptions + IDs locaux + generic subscribe/unsubscribe engine + channels typed bounded
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- file: docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# Validation `0.2.7` — WebSocket Solana standard
|
||||
|
||||
> **Statut : matrice active, `0.2.7-pre.002`.** Le gate normatif `pre.001` est clos ; les settings, identités, états lifecycle et snapshots sûrs sont maintenant matérialisés. Les preuves runtime socket/subscription restent ouvertes.
|
||||
> **Statut : matrice active, `0.2.7-pre.003`.** Les settings/lifecycle `pre.002` et la composition Config V2 `pre.003` sont matérialisés. Les preuves runtime socket/subscription restent ouvertes.
|
||||
|
||||
## 1. Baseline normative
|
||||
|
||||
@@ -253,7 +253,7 @@ Aucune de ces dependencies n'est ajoutée par `pre.001`; le graphe Cargo stable
|
||||
|
||||
## 9. Config compliance initiale
|
||||
|
||||
V1 actuel : HTTP-only strict. Décision :
|
||||
V1 hérité : HTTP-only strict. Contrat de compatibilité :
|
||||
|
||||
```text
|
||||
V1 -> support de lecture conservé, WS vide
|
||||
@@ -266,7 +266,24 @@ Transport -X-> Config
|
||||
|
||||
Le type Transport correspondant au discriminateur est prévu `#[non_exhaustive]`. L'objectif est de pouvoir ajouter ultérieurement une famille telle que Helius Enhanced WebSocket sans créer un nouveau conteneur Config ni injecter des options provider-specific dans les settings Solana standard. Toute valeur inconnue reste explicitement rejetée en `0.2.7`; aucun paramètre Helius n'est implémenté par anticipation.
|
||||
|
||||
La schema V1 n'est pas assouplie. Une schema V2 explicite remplace la fixture standard au moment où l'adapter est matérialisé.
|
||||
`pre.003` remplace le schema enregistré par `urn:ksp:schema:std.transport:v2`, avec deux branches strictes : V1 HTTP-only et V2 HTTP + WebSocket. La compatibilité V1 ne relâche donc ni `additionalProperties`, ni la shape historique.
|
||||
|
||||
Preuves `pre.003` :
|
||||
|
||||
```text
|
||||
config/std.transport.json format_version = 2
|
||||
config/examples/std.transport.example.json format_version = 2
|
||||
V1 fixture dédiée load + HTTP mapping OK attendu
|
||||
V1 ws_settings = None
|
||||
V2 ws_settings = Some(validated)
|
||||
ws_defaults globals
|
||||
profiles[].ws_endpoints profile-local
|
||||
ws_endpoints[].kind enum schema solana_standard
|
||||
ws_endpoints[].session overrides génériques optionnels
|
||||
Config -> Transport seule direction de dépendance
|
||||
```
|
||||
|
||||
`ResolvedTransportConfig::settings()` reste l'accesseur HTTP historique. `http_settings()` l'explicite et `ws_settings()` expose `Option<&WsTransportSettings>` afin que le V1 backward ne force jamais un faux `WsTransportSettings` vide. `into_transport_settings()` permet de consommer les deux contrats ensemble.
|
||||
|
||||
## 9.1 Checkpoint settings/lifecycle `pre.002`
|
||||
|
||||
@@ -359,7 +376,7 @@ N subscriptions same session prouvé
|
||||
reconnect/resubscribe/backpressure/shutdown gates verts
|
||||
unstable warnings centralisés
|
||||
HTTP 52+14 non régressé
|
||||
Config V1 backward + V2 WS validés
|
||||
Config V1 backward + V2 WS validés (**Done `pre.003`**, compilation opérateur requise)
|
||||
smoke live opt-in documenté
|
||||
cargo tree inspecté
|
||||
cargo test --workspace vert
|
||||
|
||||
Reference in New Issue
Block a user