v0.5.1-pre.008

This commit is contained in:
2026-08-10 11:21:07 +02:00
parent a2820062eb
commit 0e05c660c6
90 changed files with 2490 additions and 1613 deletions

View File

@@ -1,16 +1,13 @@
// file: ks-config/src/settings.rs
// version: 22
// version: 23
//! Resolved runtime configuration models retained during the `0.5.1` source-document split.
use ts_rs::TS; // rust-rules: derive-import
const CONFIG_JSON_SCHEMA: &str =
include_str!("../../config/schemas/resolved.app.config.schema.json");
/// Resolved runtime configuration containing every composed named profile.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/AppConfig.ts")]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct AppConfig {
/// Active profile name.
pub active_profile: std::string::String,
@@ -19,16 +16,10 @@ pub struct AppConfig {
}
/// Resolved runtime profile selected by the root active profile name.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/ks_config/settings/ProfileConfig.ts"
)]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct ProfileConfig {
/// Profile code.
pub name: std::string::String,
/// Application metadata.
pub app: AppSectionConfig,
/// Database configuration.
pub database: DatabaseConfig,
/// Solana endpoint and listener configuration.
@@ -37,29 +28,10 @@ pub struct ProfileConfig {
pub wallet: WalletConfig,
/// Execution safety configuration.
pub execution: ExecutionConfig,
/// Demo application configuration.
pub demo: DemoConfig,
}
/// Application metadata for a profile.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/ks_config/settings/AppSectionConfig.ts"
)]
pub struct AppSectionConfig {
/// Application name.
pub name: std::string::String,
/// Environment name.
pub environment: std::string::String,
}
/// Database backend configuration.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/ks_config/settings/DatabaseConfig.ts"
)]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct DatabaseConfig {
/// Enables database storage.
pub enabled: bool,
@@ -72,33 +44,26 @@ pub struct DatabaseConfig {
}
/// PostgreSQL backend configuration.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/ks_config/settings/PostgresConfig.ts"
)]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct PostgresConfig {
/// PostgreSQL URL or local placeholder.
pub url: std::string::String,
/// Maximum connection count.
pub max_connections: u32,
/// Connection timeout in milliseconds.
#[ts(type = "number")]
pub connect_timeout_ms: u64,
/// Enables schema initialization at startup.
pub auto_initialize_schema: bool,
}
/// SQLite backend configuration.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/SqliteConfig.ts")]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct SqliteConfig {
/// SQLite database path.
pub path: std::string::String,
/// Creates the file when missing.
pub create_if_missing: bool,
/// Busy timeout in milliseconds.
#[ts(type = "number")]
pub busy_timeout_ms: u64,
/// Maximum connection count.
pub max_connections: u32,
@@ -109,8 +74,7 @@ pub struct SqliteConfig {
}
/// Solana endpoints and listener configuration.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/SolanaConfig.ts")]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct SolanaConfig {
/// HTTP JSON-RPC endpoints.
pub http_endpoints: std::vec::Vec<HttpEndpointConfig>,
@@ -121,11 +85,7 @@ pub struct SolanaConfig {
}
/// HTTP JSON-RPC endpoint configuration.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/ks_config/settings/HttpEndpointConfig.ts"
)]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct HttpEndpointConfig {
/// Endpoint name.
pub name: std::string::String,
@@ -138,10 +98,8 @@ pub struct HttpEndpointConfig {
/// Full endpoint URL.
pub url: std::string::String,
/// Connection timeout in milliseconds.
#[ts(type = "number")]
pub connect_timeout_ms: u64,
/// Request timeout in milliseconds.
#[ts(type = "number")]
pub request_timeout_ms: u64,
/// Maximum idle connections per host.
pub max_idle_connections_per_host: u32,
@@ -150,11 +108,7 @@ pub struct HttpEndpointConfig {
}
/// Standard Solana WebSocket endpoint configuration.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/ks_config/settings/WsEndpointConfig.ts"
)]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct WsEndpointConfig {
/// Endpoint name.
pub name: std::string::String,
@@ -167,13 +121,10 @@ pub struct WsEndpointConfig {
/// Full endpoint URL.
pub url: std::string::String,
/// Connection timeout in milliseconds.
#[ts(type = "number")]
pub connect_timeout_ms: u64,
/// Request timeout in milliseconds.
#[ts(type = "number")]
pub request_timeout_ms: u64,
/// Unsubscribe timeout in milliseconds.
#[ts(type = "number")]
pub unsubscribe_timeout_ms: u64,
/// Writer channel capacity.
pub write_channel_capacity: u32,
@@ -186,11 +137,7 @@ pub struct WsEndpointConfig {
}
/// Role-specific endpoint limits.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/ks_config/settings/EndpointRoleConfig.ts"
)]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct EndpointRoleConfig {
/// Role code used by endpoint pools.
pub role: std::string::String,
@@ -209,16 +156,11 @@ pub struct EndpointRoleConfig {
/// Maximum subscriptions allowed for this role on this URL.
pub max_subscriptions: u32,
/// Pause after a rate limit response in milliseconds.
#[ts(type = "number")]
pub pause_after_rate_limit_ms: u64,
}
/// Listener configuration used by standard WebSocket subscriptions.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/ks_config/settings/ListenerConfig.ts"
)]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct ListenerConfig {
/// Enables listener creation.
pub enabled: bool,
@@ -233,11 +175,7 @@ pub struct ListenerConfig {
}
/// Log listener filtered by a program identifier mention.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/ks_config/settings/LogListenerConfig.ts"
)]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct LogListenerConfig {
/// Listener name.
pub name: std::string::String,
@@ -252,11 +190,7 @@ pub struct LogListenerConfig {
}
/// Program account listener.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/ks_config/settings/ProgramListenerConfig.ts"
)]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct ProgramListenerConfig {
/// Listener name.
pub name: std::string::String,
@@ -271,11 +205,7 @@ pub struct ProgramListenerConfig {
}
/// Account listener.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/ks_config/settings/AccountListenerConfig.ts"
)]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct AccountListenerConfig {
/// Listener name.
pub name: std::string::String,
@@ -290,8 +220,7 @@ pub struct AccountListenerConfig {
}
/// Wallet configuration.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/WalletConfig.ts")]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct WalletConfig {
/// Wallet directory path.
pub wallet_dir: std::string::String,
@@ -306,11 +235,7 @@ pub struct WalletConfig {
}
/// Execution safety configuration.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/ks_config/settings/ExecutionConfig.ts"
)]
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
pub struct ExecutionConfig {
/// Enables local validator transaction sending.
pub localnet_send_enabled: bool,
@@ -327,48 +252,29 @@ pub struct ExecutionConfig {
/// Requires explicit operator confirmation before send.
pub require_operator_confirmation: bool,
/// Maximum spend in lamports for local validator tests.
#[ts(type = "number")]
pub localnet_max_spend_lamports: u64,
/// Maximum spend in lamports for devnet tests.
#[ts(type = "number")]
pub devnet_max_spend_lamports: u64,
/// Maximum spend in lamports for testnet tests.
#[ts(type = "number")]
pub testnet_max_spend_lamports: u64,
/// Maximum spend in lamports for mainnet operations.
#[ts(type = "number")]
pub mainnet_max_spend_lamports: u64,
/// Maximum estimated transaction fee in lamports.
#[ts(type = "number")]
pub max_fee_lamports: u64,
/// Maximum compute-unit price in micro-lamports.
#[ts(type = "number")]
pub max_compute_unit_price_micro_lamports: u64,
/// Maximum accepted age for a recent blockhash in slots.
#[ts(type = "number")]
pub recent_blockhash_max_age_slots: u64,
/// Maximum node retransmission retries requested by `sendTransaction`.
pub send_max_retries: u32,
/// Delay between transaction confirmation polls.
#[ts(type = "number")]
pub confirmation_poll_interval_ms: u64,
/// Maximum number of transaction confirmation polls.
pub confirmation_max_attempts: u32,
/// Maximum devnet faucet airdrop allowed for a temporary wallet.
#[ts(type = "number")]
pub devnet_airdrop_max_lamports: u64,
}
/// Demo application configuration.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/DemoConfig.ts")]
pub struct DemoConfig {
/// Enables live demo pages.
pub live_demo_enabled: bool,
/// Enables trading demo pages.
pub trading_demo_enabled: bool,
}
/// Returns the embedded JSON Schema text used for resolved runtime contract validation.
pub fn config_json_schema_text() -> &'static str {
return CONFIG_JSON_SCHEMA;
@@ -413,9 +319,9 @@ pub fn validate_config_json_schema(raw_json: &str) -> ks_core::Result<()> {
let validation_result = validator.validate(&instance);
return match validation_result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
"config_json_schema_validation_failed",
error.to_string(),
"resolved configuration does not satisfy its schema",
)),
};
}
@@ -428,10 +334,10 @@ pub fn parse_config_json(raw_json: &str) -> ks_core::Result<AppConfig> {
}
let config = match serde_json::from_str::<AppConfig>(raw_json) {
std::result::Result::Ok(config) => config,
std::result::Result::Err(error) => {
std::result::Result::Err(_) => {
return std::result::Result::Err(ks_core::Error::new(
"config_json_decode_failed",
error.to_string(),
"resolved configuration could not be decoded",
));
},
};
@@ -477,36 +383,6 @@ pub fn read_config_json_file_with_environment(
return parse_config_json(&resolved);
}
/// Serializes a configuration value to compact JSON.
pub fn serialize_config_json(config: &AppConfig) -> ks_core::Result<std::string::String> {
match validate_config(config) {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
return match serde_json::to_string(config) {
std::result::Result::Ok(serialized) => std::result::Result::Ok(serialized),
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
"config_json_serialize_failed",
error.to_string(),
)),
};
}
/// Serializes a configuration value to pretty JSON.
pub fn serialize_config_json_pretty(config: &AppConfig) -> ks_core::Result<std::string::String> {
match validate_config(config) {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
return match serde_json::to_string_pretty(config) {
std::result::Result::Ok(serialized) => std::result::Result::Ok(serialized),
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
"config_json_pretty_serialize_failed",
error.to_string(),
)),
};
}
/// Returns the active profile declared by the root configuration.
pub fn active_profile(config: &AppConfig) -> ks_core::Result<&ProfileConfig> {
for profile in &config.profiles {
@@ -576,10 +452,6 @@ fn validate_root_config(config: &AppConfig) -> ks_core::Result<()> {
}
fn validate_profile(profile: &ProfileConfig) -> ks_core::Result<()> {
match validate_app_section(&profile.app) {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
match validate_database(&profile.database) {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -599,14 +471,6 @@ fn validate_profile(profile: &ProfileConfig) -> ks_core::Result<()> {
return validate_wallet_execution_pair(&profile.wallet, &profile.execution);
}
fn validate_app_section(config: &AppSectionConfig) -> ks_core::Result<()> {
match require_non_empty(&config.name, "app.name") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
return require_non_empty(&config.environment, "app.environment");
}
pub(crate) fn validate_database(config: &DatabaseConfig) -> ks_core::Result<()> {
if config.backend != "postgres" && config.backend != "sqlite" {
return std::result::Result::Err(ks_core::Error::new(
@@ -683,7 +547,7 @@ pub(crate) fn validate_http_endpoint(config: &HttpEndpointConfig) -> ks_core::Re
if !config.url.starts_with("http://") && !config.url.starts_with("https://") {
return std::result::Result::Err(ks_core::Error::new(
"http_endpoint_url_invalid",
config.url.clone(),
config.name.clone(),
));
}
if config.connect_timeout_ms == 0
@@ -714,7 +578,7 @@ pub(crate) fn validate_ws_endpoint(config: &WsEndpointConfig) -> ks_core::Result
if !config.url.starts_with("ws://") && !config.url.starts_with("wss://") {
return std::result::Result::Err(ks_core::Error::new(
"ws_endpoint_url_invalid",
config.url.clone(),
config.name.clone(),
));
}
if config.connect_timeout_ms == 0
@@ -1006,24 +870,6 @@ pub(crate) fn require_non_empty(value: &str, field_name: &str) -> ks_core::Resul
#[cfg(test)]
mod tests {
use ts_rs::TS; // rust-rules: derive-import
#[test]
fn exported_json_configuration_numbers_do_not_use_bigint() {
let config = ts_rs::Config::default();
let declarations = [
<crate::PostgresConfig as TS>::decl(&config),
<crate::SqliteConfig as TS>::decl(&config),
<crate::HttpEndpointConfig as TS>::decl(&config),
<crate::WsEndpointConfig as TS>::decl(&config),
<crate::EndpointRoleConfig as TS>::decl(&config),
<crate::ExecutionConfig as TS>::decl(&config),
];
for declaration in declarations {
assert!(!declaration.contains("bigint"));
}
}
const DEFAULT_CONFIG: &str =
include_str!("../../test-fixtures/config/resolved.app.config.json");
const EXAMPLE_CONFIG: &str =
@@ -1077,29 +923,6 @@ mod tests {
//assert_eq!(active.name, "local_devnet");
}
#[test]
fn default_app_config_serializes_and_roundtrips() {
let config_result = super::parse_config_json(DEFAULT_CONFIG);
assert!(config_result.is_ok());
let config = match config_result {
std::result::Result::Ok(config) => config,
std::result::Result::Err(error) => panic!("default app config must parse: {error}"),
};
let serialized_result = super::serialize_config_json_pretty(&config);
assert!(serialized_result.is_ok());
let serialized = match serialized_result {
std::result::Result::Ok(serialized) => serialized,
std::result::Result::Err(error) => panic!("default app config must serialize: {error}"),
};
let reparsed_result = super::parse_config_json(&serialized);
assert!(reparsed_result.is_ok());
let reparsed = match reparsed_result {
std::result::Result::Ok(reparsed) => reparsed,
std::result::Result::Err(error) => panic!("serialized config must parse: {error}"),
};
assert_eq!(config, reparsed);
}
#[test]
fn parser_rejects_missing_active_profile() {
let mut value = parse_default_value();