Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c65096f4c | |||
| cb142b1cb8 | |||
| 08c8046262 | |||
| c0b131bf6f | |||
| 5bf9651038 | |||
| e91bf36e9e | |||
| 1125ade4c1 | |||
| 518cc9257f | |||
| 46afe10423 | |||
| 25edcc231e | |||
| 6094d33120 | |||
| dcd23abb92 | |||
| 36b98e0abc | |||
| 3c069347b7 | |||
| a0a5595186 | |||
| 6c1172efc3 | |||
| d5536b0778 | |||
| c6794af05b | |||
| bcda6db11f | |||
| 51b5114ae2 | |||
| c97a8739ab | |||
| c2ccef3849 | |||
| 58134c81d1 |
5
.pydevproject
Normal file
5
.pydevproject
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<?eclipse-pydev version="1.0"?><pydev_project>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python interpreter</pydev_property>
|
||||
</pydev_project>
|
||||
@@ -1,8 +1,14 @@
|
||||
<!-- file: CHANGELOG.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# Changelog KSP
|
||||
|
||||
## 0.2.5 — Wallet foundation — 2026-08-20
|
||||
|
||||
`0.2.5` publie `ksp-wallet-lib` et le format natif interopérable `.kspwallet` V1. La release stabilise les capacités cryptographiquement indépendantes VIEW/OWNER, le masquage de la Pubkey/alias/notes à l’état verrouillé, Argon2id v19 avec paramètres sérialisés et profil de création `65 536 KiB / 3 / 1`, XChaCha20-Poly1305, une autorité Ed25519 OWNER distincte de la keypair Solana, les compartiments `owner_control`/`metadata`/`secret`, la création et l’ouverture mémoire/fichier, la persistence no-clobber, la signature Solana OWNER sans getter secret, l’administration alias/notes, les rotations OWNER/VIEW, la révocation forte VIEW avec rekey metadata, la détection de handles stale et les adapters Solana CLI JSON/Base58 pour inspection/import/export. Le wire V1 est documenté indépendamment de Rust dans `docs/formats/KSPWALLET_V1.md` et couvert par des vecteurs publics ainsi que des canaris adversariaux et une reproduction externe. La release conserve Wallet indépendant de Config/Transport/ExecutionPolicy/Store/Tauri et expose la Pubkey via `ksp-core-lib` tandis que `solana-keypair` reste encapsulée dans Wallet.
|
||||
|
||||
La clôture technique `pre.010-fix.001` à `fix.003` met la dépendance directe KSP à `ed25519-dalek 3.0.0`, accepte la génération `2.2.0` transitive encore imposée par `solana-keypair 3.1.2`, normalise les sources Rust du workspace et introduit `python3 scripts/audit_rust_workspace_rules.py` comme gate structurel complémentaire à rustfmt/Clippy. Le checkpoint final `pre.010-fix.003` est validé avec audit Python clean, `cargo check`, Clippy, tests Wallet et workspace complet verts. Le prompt `prompts/011-V0_2_6_START_PROMPT.md` ouvre ensuite `0.2.6 — Wallet Desk` par audit/sizing avant implémentation.
|
||||
|
||||
## 0.2.4 — HTTP Blocks + Economics + compliance HTTP finale — 2026-08-18
|
||||
|
||||
`0.2.4` achève la surface HTTP Solana standard de `ksp-onchain-transport-lib` : les 10 wrappers Blocks et 5 wrappers Economics portent la couverture typée à **52/52 méthodes HTTP courantes**, tandis que les **14/14 méthodes historiques Deprecated/runtime Removed** restent conservées pour compliance. La release stabilise notamment `getBlock` moderne avec sa forme bare encoding legacy dépréciée, les quatre variantes `transactionDetails`, les versions transaction numériques génériques, `numRewardPartitions` (SIMD-0118), `commissionBps` (SIMD-0291), les overloads de ranges/performance, `getBlockProduction`, les valeurs Economics fournies par le runtime et `getInflationReward` positionnel sans plafond d'adresses inventé. Le réaudit final confirme l'égalité exacte entre l'inventaire HTTP officiel courant et le registre KSP, applique `KSP-TRANSPORT-007` aux **52/52 wrappers**, et ne matérialise aucune extension spéculative issue de la watchlist SIMD. Les canaries de frontières, le workspace complet et les deux smokes Devnet — Transport pur couvrant Accounts/Tokens/Cluster/Transactions/Blocks/Economics et composition Config -> Transport transitoire — sont validés avant publication. Le prompt `prompts/010-V0_2_5_START_PROMPT.md`, enrichi par `pre.009-fix.001`, ouvre `0.2.5 — Wallet foundation` avec threat model, interopérabilité `.kspwallet` et capacités indépendantes VIEW/OWNER.
|
||||
|
||||
14
Cargo.toml
14
Cargo.toml
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 142
|
||||
# version: 163
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
|
||||
members = ["crates/ksp-app-config-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.4"
|
||||
version = "0.2.5"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
@@ -14,21 +14,29 @@ authors = ["SinuS von SifriduS <sinus@sasedev.net>"]
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
argon2 = { version = "^0.5", default-features = false }
|
||||
chacha20poly1305 = { version = "^0.11", default-features = false }
|
||||
ed25519-dalek = { version = "^3.0", default-features = false }
|
||||
getrandom = { version = "^0.4", default-features = false }
|
||||
base64 = { version = "^0.23" }
|
||||
fs2 = { version = "^0.4" }
|
||||
serde = { version = "^1.0" }
|
||||
serde_json = { version = "^1.0" }
|
||||
jsonschema = { version = "^0.49", default-features = false }
|
||||
reqwest = { version = "^0.13", default-features = false }
|
||||
solana-keypair = { version = "^3.1", default-features = false }
|
||||
solana-pubkey = { version = "^4.3", default-features = false }
|
||||
tracing = { version = "^0.1", default-features = false }
|
||||
tracing-subscriber = { version = "^0.3", default-features = false }
|
||||
tracing-appender = { version = "^0.2", default-features = false }
|
||||
tokio = { version = "^1.53", default-features = false }
|
||||
tempfile = { version = "^3.27" }
|
||||
chrono = { version = "^0.4", default-features = false }
|
||||
tauri = { version = "^2.11" }
|
||||
tauri-build = { version = "^2.6" }
|
||||
tauri-plugin-tracing = { version = "^0.3" }
|
||||
ts-rs = { version = "^12.0" }
|
||||
zeroize = { version = "^1.9" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
missing_docs = "warn"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
<!-- version: 43 -->
|
||||
<!-- version: 55 -->
|
||||
|
||||
# Roadmap KSP
|
||||
|
||||
@@ -49,13 +49,13 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
|
||||
- [X] `0.2.2` — HTTP Accounts + Tokens + Cluster : 22 wrappers typés (5 Accounts + 5 Tokens + 12 Cluster), canaries de complétude 52+14, smoke Devnet Transport pur et smoke historique Config -> Transport validés, documentation durable et prompt `0.2.3` publiés stables.
|
||||
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
|
||||
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
|
||||
- [ ] `0.2.5` — Introduire `ksp-wallet-lib` et le format interopérable `.kspwallet` avec threat model offline, capacités indépendantes VIEW/OWNER, metadata protégées, signature, rotations de key slots et import/export extensible ; exclure `WalletPolicy`.
|
||||
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet.
|
||||
- [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001`–`fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe l’audit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
|
||||
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet ; le prompt de démarrage est préparé par `0.2.5-pre.010`.
|
||||
- [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe.
|
||||
- [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
|
||||
- [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.
|
||||
- [ ] `0.2.10` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR.
|
||||
- [ ] `0.2.11` — Introduire une petite application desk de visualisation des prix.
|
||||
- [ ] `0.2.11` — Introduire une petite application desk de visualisation/validation des prix offchain, puis intégrer cette capacité dans `ksp-app-wallet-desk` sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé.
|
||||
- [ ] `0.2.12` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes.
|
||||
- [ ] `0.2.13` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet.
|
||||
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
// file: crates/ksp-app-config-desk/src/app_state.rs
|
||||
// version: 6
|
||||
// version: 8
|
||||
|
||||
//! Shared backend state owned by the Tauri application.
|
||||
|
||||
struct LoggingRuntimeState {
|
||||
guard: ksp_logging_lib::LoggingGuard,
|
||||
active_profile_id: std::option::Option<String>,
|
||||
selection_source: String,
|
||||
generation: u32,
|
||||
fallback_active: bool,
|
||||
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
}
|
||||
|
||||
/// Shared Config Desk application state managed by Tauri.
|
||||
pub(crate) struct AppState {
|
||||
config_management: ksp_config_lib::ConfigManagement,
|
||||
@@ -28,7 +19,7 @@ impl AppState {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let runtime_identity = crate::logging_runtime::launch_identity();
|
||||
let runtime_identity = crate::launch_identity();
|
||||
let runtime_identity = match runtime_identity {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -175,7 +166,7 @@ impl AppState {
|
||||
let dropped = runtime.guard.dropped_lines();
|
||||
let mut files = std::vec::Vec::<crate::LoggingRuntimeFileDto>::new();
|
||||
for metadata in runtime.guard.active_file_outputs() {
|
||||
files.push(crate::logging_runtime::project_file(&metadata));
|
||||
files.push(crate::project_logging_file(&metadata));
|
||||
}
|
||||
return std::result::Result::Ok(crate::LoggingRuntimeStatusDto {
|
||||
active_profile: runtime.active_profile_id.clone(),
|
||||
@@ -185,9 +176,9 @@ impl AppState {
|
||||
application_id: identity.application_id().to_owned(),
|
||||
launch_timestamp: identity.launch_timestamp().to_owned(),
|
||||
console_enabled,
|
||||
dropped_console_lines: crate::logging_runtime::count_to_u64(dropped.console()),
|
||||
dropped_file_lines: crate::logging_runtime::count_to_u64(dropped.file()),
|
||||
dropped_total_lines: crate::logging_runtime::count_to_u64(dropped.total()),
|
||||
dropped_console_lines: crate::count_to_u64(dropped.console()),
|
||||
dropped_file_lines: crate::count_to_u64(dropped.file()),
|
||||
dropped_total_lines: crate::count_to_u64(dropped.total()),
|
||||
files,
|
||||
});
|
||||
}
|
||||
@@ -206,3 +197,12 @@ impl AppState {
|
||||
.is_ok();
|
||||
}
|
||||
}
|
||||
|
||||
struct LoggingRuntimeState {
|
||||
guard: ksp_logging_lib::LoggingGuard,
|
||||
active_profile_id: std::option::Option<String>,
|
||||
selection_source: String,
|
||||
generation: u32,
|
||||
fallback_active: bool,
|
||||
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
// file: crates/ksp-app-config-desk/src/bootstrap.rs
|
||||
// version: 3
|
||||
// version: 6
|
||||
|
||||
//! Config and Logging bootstrap for the desktop application.
|
||||
|
||||
/// Crate-internal `LoggingStartup` state shared across the owning crate.
|
||||
pub(crate) struct LoggingStartup {
|
||||
/// Stores the guard value for this state.
|
||||
pub(crate) guard: ksp_logging_lib::LoggingGuard,
|
||||
/// Stores the active profile id value for this state.
|
||||
pub(crate) active_profile_id: std::option::Option<String>,
|
||||
/// Stores the selection source value for this state.
|
||||
pub(crate) selection_source: String,
|
||||
/// Stores the fallback active value for this state.
|
||||
pub(crate) fallback_active: bool,
|
||||
/// Stores the startup diagnostic value for this state.
|
||||
pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
}
|
||||
|
||||
@@ -23,6 +29,7 @@ enum LoggingStartupPlan {
|
||||
},
|
||||
}
|
||||
|
||||
/// Executes the crate-internal config management operation for the owning module.
|
||||
pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
|
||||
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments);
|
||||
let bootstrap = match bootstrap {
|
||||
@@ -38,10 +45,11 @@ pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_li
|
||||
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine));
|
||||
}
|
||||
|
||||
/// Executes the crate-internal initialize logging operation for the owning module.
|
||||
pub(crate) fn initialize_logging(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<LoggingStartup> {
|
||||
) -> ksp_core_lib::Result<crate::LoggingStartup> {
|
||||
let plan = resolve_logging_startup(management);
|
||||
return match plan {
|
||||
LoggingStartupPlan::Managed { active_profile_id, settings } => {
|
||||
@@ -71,6 +79,15 @@ pub(crate) fn initialize_logging(
|
||||
};
|
||||
}
|
||||
|
||||
fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
|
||||
return ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::vec::Vec::new(),
|
||||
);
|
||||
}
|
||||
|
||||
fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan {
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
@@ -93,7 +110,7 @@ fn fallback_startup_plan(initial_error: ksp_core_lib::Error) -> LoggingStartupPl
|
||||
fn initialize_fallback_logging(
|
||||
initial_error: ksp_core_lib::Error,
|
||||
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<LoggingStartup> {
|
||||
) -> ksp_core_lib::Result<crate::LoggingStartup> {
|
||||
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
|
||||
return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity);
|
||||
}
|
||||
@@ -103,7 +120,7 @@ fn initialize_planned_fallback_logging(
|
||||
diagnostic: crate::CommandErrorDto,
|
||||
settings: ksp_logging_lib::LoggingSettings,
|
||||
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<LoggingStartup> {
|
||||
) -> ksp_core_lib::Result<crate::LoggingStartup> {
|
||||
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
|
||||
let guard = match guard {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -132,15 +149,6 @@ fn initialize_planned_fallback_logging(
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
|
||||
return ksp_logging_lib::LoggingSettings::new(
|
||||
ksp_logging_lib::LogFilterLevel::Info,
|
||||
ksp_logging_lib::SpanEvents::Off,
|
||||
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||
std::vec::Vec::new(),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/bootstrap.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
// file: crates/ksp-app-config-desk/src/constants.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! Application-owned tracing targets and domains.
|
||||
|
||||
/// Structured domain used while bootstrapping Config and Logging.
|
||||
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "config.bootstrap";
|
||||
/// Structured domain for Config document inventory, diagnostics and repair.
|
||||
pub(crate) const TRACING_DOMAIN_DOCUMENTS: &str = "config.documents";
|
||||
/// Structured domain for safe Config environment reports.
|
||||
pub(crate) const TRACING_DOMAIN_ENVIRONMENT: &str = "config.environment";
|
||||
/// Structured domain used by technical frontend events.
|
||||
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||
/// Structured domain for typed Logging editor inspection.
|
||||
pub(crate) const TRACING_DOMAIN_LOGGING_EDITOR: &str = "config.logging_editor";
|
||||
/// Structured domain for active Logging runtime metadata and explicit profile application.
|
||||
pub(crate) const TRACING_DOMAIN_LOGGING_RUNTIME: &str = "config.logging_runtime";
|
||||
/// Structured domain used by controlled Logging test events.
|
||||
pub(crate) const TRACING_DOMAIN_LOGGING_TEST: &str = "config.logging_test";
|
||||
/// Structured domain for Config profile selection and provenance inspection.
|
||||
pub(crate) const TRACING_DOMAIN_PROFILES: &str = "config.profiles";
|
||||
/// Structured domain for explicit privileged Secret reveal operations.
|
||||
pub(crate) const TRACING_DOMAIN_SECRETS: &str = "config.secrets";
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
|
||||
/// Owning target for backend events emitted by Config Desk.
|
||||
pub(crate) const TRACING_TARGET: &str = "ksp-app-config-desk";
|
||||
/// Owning target for generic frontend events emitted through the KSP bridge.
|
||||
@@ -13,23 +33,3 @@ pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-config-desk.front
|
||||
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-config-desk.frontend.splash";
|
||||
/// Dedicated target used by the controlled Logging test panel.
|
||||
pub(crate) const TRACING_TARGET_LOGGING_TEST: &str = "ksp-app-config-desk.logging-test";
|
||||
/// Structured domain used while bootstrapping Config and Logging.
|
||||
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "config.bootstrap";
|
||||
/// Structured domain used by technical frontend events.
|
||||
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
|
||||
/// Structured domain for Config document inventory, diagnostics and repair.
|
||||
pub(crate) const TRACING_DOMAIN_DOCUMENTS: &str = "config.documents";
|
||||
/// Structured domain for Config profile selection and provenance inspection.
|
||||
pub(crate) const TRACING_DOMAIN_PROFILES: &str = "config.profiles";
|
||||
/// Structured domain for safe Config environment reports.
|
||||
pub(crate) const TRACING_DOMAIN_ENVIRONMENT: &str = "config.environment";
|
||||
/// Structured domain for explicit privileged Secret reveal operations.
|
||||
pub(crate) const TRACING_DOMAIN_SECRETS: &str = "config.secrets";
|
||||
/// Structured domain for typed Logging editor inspection.
|
||||
pub(crate) const TRACING_DOMAIN_LOGGING_EDITOR: &str = "config.logging_editor";
|
||||
/// Structured domain for active Logging runtime metadata and explicit profile application.
|
||||
pub(crate) const TRACING_DOMAIN_LOGGING_RUNTIME: &str = "config.logging_runtime";
|
||||
/// Structured domain used by controlled Logging test events.
|
||||
pub(crate) const TRACING_DOMAIN_LOGGING_TEST: &str = "config.logging_test";
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// file: crates/ksp-app-config-desk/src/documents.rs
|
||||
// version: 1
|
||||
// version: 3
|
||||
|
||||
//! Generic Config document inventory, diagnostics and validated repair services.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
const STATUS_VALID: &str = "valid";
|
||||
const STATUS_INVALID: &str = "invalid";
|
||||
const STAGE_VALID: &str = "valid";
|
||||
const STAGE_READ: &str = "read";
|
||||
const STAGE_EFFECTIVE: &str = "effective";
|
||||
const STAGE_JSON: &str = "json";
|
||||
const STAGE_OTHER: &str = "other";
|
||||
const STAGE_READ: &str = "read";
|
||||
const STAGE_SCHEMA: &str = "schema";
|
||||
const STAGE_SEMANTIC: &str = "semantic";
|
||||
const STAGE_EFFECTIVE: &str = "effective";
|
||||
const STAGE_OTHER: &str = "other";
|
||||
const STAGE_VALID: &str = "valid";
|
||||
const STATUS_INVALID: &str = "invalid";
|
||||
const STATUS_VALID: &str = "valid";
|
||||
|
||||
/// Safe inventory row for one Config document registered by `ksp-config-lib`.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
@@ -78,7 +78,7 @@ impl ConfigDocumentErrorDto {
|
||||
}
|
||||
|
||||
/// Lists all Config-kind documents from the Config registry and evaluates their current backend validation state.
|
||||
pub(crate) fn inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocumentSummaryDto> {
|
||||
pub(crate) fn document_inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocumentSummaryDto> {
|
||||
let management = state.config_management();
|
||||
let engine = management.engine();
|
||||
let mut documents = std::vec::Vec::new();
|
||||
@@ -98,7 +98,7 @@ pub(crate) fn inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocument
|
||||
}
|
||||
|
||||
/// Loads one Config document detail, including raw source when Config can read it.
|
||||
pub(crate) fn detail(state: &crate::AppState, file_id_text: &str) -> std::result::Result<ConfigDocumentDetailDto, ConfigDocumentErrorDto> {
|
||||
pub(crate) fn document_detail(state: &crate::AppState, file_id_text: &str) -> std::result::Result<ConfigDocumentDetailDto, ConfigDocumentErrorDto> {
|
||||
let file_id = parse_config_file_id(file_id_text);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -133,7 +133,7 @@ pub(crate) fn detail(state: &crate::AppState, file_id_text: &str) -> std::result
|
||||
}
|
||||
|
||||
/// Validates and atomically persists one raw Config source candidate, then reloads the document detail.
|
||||
pub(crate) fn save_source(
|
||||
pub(crate) fn save_document_source(
|
||||
state: &crate::AppState,
|
||||
file_id_text: &str,
|
||||
source: &str,
|
||||
@@ -148,7 +148,7 @@ pub(crate) fn save_source(
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)),
|
||||
};
|
||||
let document = detail(state, file_id.as_str());
|
||||
let document = crate::document_detail(state, file_id.as_str());
|
||||
let document = match document {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-app-config-desk/src/dto_common.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Common Tauri DTOs shared by Config Desk commands.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Safe command error projection that never serializes arbitrary KSP error context or source values.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-app-config-desk/src/environment.rs
|
||||
// version: 2
|
||||
// version: 4
|
||||
|
||||
//! Safe Config environment reports and `.env` management projections for Config Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Safe desired/effective environment view exposed to the ordinary Config Desk frontend.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
@@ -63,17 +63,17 @@ impl EnvironmentMutationOperation {
|
||||
}
|
||||
|
||||
/// Returns the safe environment report owned by Config.
|
||||
pub(crate) fn report(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigEnvironmentReportDto>> {
|
||||
pub(crate) fn environment_report(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigEnvironmentReportDto>> {
|
||||
return report_from_management(state.config_management());
|
||||
}
|
||||
|
||||
/// Creates or replaces one `.env` entry exclusively through ConfigManagement.
|
||||
pub(crate) fn set_value(state: &crate::AppState, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> {
|
||||
pub(crate) fn set_environment_value(state: &crate::AppState, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> {
|
||||
return set_value_from_management(state.config_management(), variable_name, value);
|
||||
}
|
||||
|
||||
/// Removes one `.env` entry exclusively through ConfigManagement.
|
||||
pub(crate) fn remove_value(state: &crate::AppState, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> {
|
||||
pub(crate) fn remove_environment_value(state: &crate::AppState, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeDto> {
|
||||
return remove_value_from_management(state.config_management(), variable_name);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,42 @@
|
||||
// file: crates/ksp-app-config-desk/src/errors.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! Application-local error codes for the configuration desktop shell.
|
||||
|
||||
/// Tauri runtime assembly or execution failed.
|
||||
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_runtime_failed");
|
||||
/// Shared Config Desk application state is internally inconsistent.
|
||||
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_invalid");
|
||||
/// Shared Config Desk runtime state cannot be locked safely.
|
||||
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_lock_failed");
|
||||
/// A Documents command requested a registered file that is not a Config-kind document.
|
||||
pub(crate) const ERROR_CODE_DOCUMENT_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "document_kind_invalid");
|
||||
/// Frontend logging requested an unsupported level.
|
||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_level_invalid");
|
||||
/// Frontend logging requested a target outside the application whitelist.
|
||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_target_invalid");
|
||||
/// Config Desk could not install the managed Logging runtime or its safe fallback.
|
||||
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_bootstrap_failed");
|
||||
/// Config Desk persisted a Logging candidate but could not restore the previous source after runtime application failed.
|
||||
pub(crate) const ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("config_desk", "logging_source_rollback_failed");
|
||||
/// Shared Config Desk application state is internally inconsistent.
|
||||
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_invalid");
|
||||
/// Shared Config Desk runtime state cannot be locked safely.
|
||||
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "app_state_lock_failed");
|
||||
/// Frontend logging requested an unsupported level.
|
||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_level_invalid");
|
||||
/// Frontend logging requested a target outside the application whitelist.
|
||||
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_target_invalid");
|
||||
/// Controlled Logging test request contains an unsupported or invalid selector.
|
||||
pub(crate) const ERROR_CODE_LOGGING_TEST_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_test_request_invalid");
|
||||
|
||||
/// A validated Config document does not expose the standard profile contract expected by the Profiles panel.
|
||||
pub(crate) const ERROR_CODE_PROFILE_CONTRACT_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_contract_missing");
|
||||
/// Config profile data could not be projected safely for the frontend.
|
||||
pub(crate) const ERROR_CODE_PROFILE_PROJECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_projection_failed");
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_setting_invalid");
|
||||
/// Splash readiness was invoked from a window other than the splash window.
|
||||
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_origin_invalid");
|
||||
/// A required Tauri window is missing from the configured application runtime.
|
||||
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_missing");
|
||||
/// A Tauri window show/focus/destroy/event operation failed.
|
||||
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_operation_failed");
|
||||
|
||||
/// A Documents command requested a registered file that is not a Config-kind document.
|
||||
pub(crate) const ERROR_CODE_DOCUMENT_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "document_kind_invalid");
|
||||
/// Privileged reveal was requested for a non-Secret KSP/KSPB namespace.
|
||||
pub(crate) const ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_requires_secret");
|
||||
/// Privileged reveal requested an unsupported source selector.
|
||||
pub(crate) const ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "secret_reveal_source_invalid");
|
||||
/// Splash readiness was invoked from a window other than the splash window.
|
||||
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_origin_invalid");
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "splash_setting_invalid");
|
||||
/// Tauri runtime assembly or execution failed.
|
||||
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_runtime_failed");
|
||||
/// A required Tauri window is missing from the configured application runtime.
|
||||
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_missing");
|
||||
/// A Tauri window show/focus/destroy/event operation failed.
|
||||
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_operation_failed");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-app-config-desk/src/frontend_logging.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! KSP-owned bridge for technical log events emitted by Config Desk frontend scripts.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Log payload sent by Config Desk frontend scripts.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/lib.rs
|
||||
// version: 14
|
||||
// version: 16
|
||||
|
||||
//! Tauri desktop application for managing and validating KSP configuration.
|
||||
|
||||
@@ -28,61 +28,173 @@ mod tw_splash;
|
||||
/// Runs the KSP configuration desktop application.
|
||||
pub use self::tauri::run;
|
||||
|
||||
/// Shared Config Desk application state managed by Tauri.
|
||||
pub(crate) use self::app_state::AppState;
|
||||
/// Crate-internal `LoggingStartup` state shared across the owning crate.
|
||||
pub(crate) use self::bootstrap::LoggingStartup;
|
||||
/// Executes the crate-internal config management operation for the owning module.
|
||||
pub(crate) use self::bootstrap::config_management;
|
||||
/// Executes the crate-internal initialize logging operation for the owning module.
|
||||
pub(crate) use self::bootstrap::initialize_logging;
|
||||
/// Structured domain used while bootstrapping Config and Logging.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
|
||||
/// Structured domain for Config document inventory, diagnostics and repair.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_DOCUMENTS;
|
||||
/// Structured domain for safe Config environment reports.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_ENVIRONMENT;
|
||||
/// Structured domain used by technical frontend events.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
|
||||
/// Structured domain for typed Logging editor inspection.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_EDITOR;
|
||||
/// Structured domain for active Logging runtime metadata and explicit profile application.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_RUNTIME;
|
||||
/// Structured domain used by controlled Logging test events.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_TEST;
|
||||
/// Structured domain for Config profile selection and provenance inspection.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_PROFILES;
|
||||
/// Structured domain for explicit privileged Secret reveal operations.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_SECRETS;
|
||||
/// Structured domain used by Tauri window lifecycle operations.
|
||||
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
|
||||
/// Owning target for backend events emitted by Config Desk.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
/// Owning target for generic frontend events emitted through the KSP bridge.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
|
||||
/// Owning target for main-window frontend events.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
|
||||
/// Owning target for splash-window frontend events.
|
||||
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
|
||||
/// Dedicated target used by the controlled Logging test panel.
|
||||
pub(crate) use self::constants::TRACING_TARGET_LOGGING_TEST;
|
||||
/// Detailed management view for one registered Config document.
|
||||
pub(crate) use self::documents::ConfigDocumentDetailDto;
|
||||
/// Safe document-specific command error with backend-owned diagnostic classification.
|
||||
pub(crate) use self::documents::ConfigDocumentErrorDto;
|
||||
/// Result of one validated raw-source repair attempt that reached persistence successfully.
|
||||
pub(crate) use self::documents::ConfigDocumentSaveResultDto;
|
||||
/// Safe inventory row for one Config document registered by `ksp-config-lib`.
|
||||
pub(crate) use self::documents::ConfigDocumentSummaryDto;
|
||||
/// Loads one Config document detail, including raw source when Config can read it.
|
||||
pub(crate) use self::documents::document_detail;
|
||||
/// Lists all Config-kind documents from the Config registry and evaluates their current backend validation state.
|
||||
pub(crate) use self::documents::document_inventory;
|
||||
/// Validates and atomically persists one raw Config source candidate, then reloads the document detail.
|
||||
pub(crate) use self::documents::save_document_source;
|
||||
/// Initial application/runtime snapshot exposed to the frontend.
|
||||
pub(crate) use self::dto_common::AppSnapshotDto;
|
||||
/// Safe command error projection that never serializes arbitrary KSP error context or source values.
|
||||
pub(crate) use self::dto_common::CommandErrorDto;
|
||||
/// Safe result metadata for one `.env` mutation.
|
||||
pub(crate) use self::environment::ConfigEnvironmentChangeDto;
|
||||
/// Safe desired/effective environment view exposed to the ordinary Config Desk frontend.
|
||||
pub(crate) use self::environment::ConfigEnvironmentReportDto;
|
||||
/// Returns the safe environment report owned by Config.
|
||||
pub(crate) use self::environment::environment_report;
|
||||
/// Removes one `.env` entry exclusively through ConfigManagement.
|
||||
pub(crate) use self::environment::remove_environment_value;
|
||||
/// Creates or replaces one `.env` entry exclusively through ConfigManagement.
|
||||
pub(crate) use self::environment::set_environment_value;
|
||||
/// Shared Config Desk application state is internally inconsistent.
|
||||
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
|
||||
/// Shared Config Desk runtime state cannot be locked safely.
|
||||
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
|
||||
/// A Documents command requested a registered file that is not a Config-kind document.
|
||||
pub(crate) use self::errors::ERROR_CODE_DOCUMENT_KIND_INVALID;
|
||||
/// Frontend logging requested an unsupported level.
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
|
||||
/// Frontend logging requested a target outside the application whitelist.
|
||||
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
|
||||
/// Config Desk could not install the managed Logging runtime or its safe fallback.
|
||||
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
|
||||
/// Config Desk persisted a Logging candidate but could not restore the previous source after runtime application failed.
|
||||
pub(crate) use self::errors::ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED;
|
||||
/// Controlled Logging test request contains an unsupported or invalid selector.
|
||||
pub(crate) use self::errors::ERROR_CODE_LOGGING_TEST_REQUEST_INVALID;
|
||||
/// A validated Config document does not expose the standard profile contract expected by the Profiles panel.
|
||||
pub(crate) use self::errors::ERROR_CODE_PROFILE_CONTRACT_MISSING;
|
||||
/// Config profile data could not be projected safely for the frontend.
|
||||
pub(crate) use self::errors::ERROR_CODE_PROFILE_PROJECTION_FAILED;
|
||||
/// Privileged reveal was requested for a non-Secret KSP/KSPB namespace.
|
||||
pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET;
|
||||
/// Privileged reveal requested an unsupported source selector.
|
||||
pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_SOURCE_INVALID;
|
||||
/// Splash readiness was invoked from a window other than the splash window.
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
|
||||
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
|
||||
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
|
||||
/// Tauri runtime assembly or execution failed.
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
|
||||
/// A required Tauri window is missing from the configured application runtime.
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
|
||||
/// A Tauri window show/focus/destroy/event operation failed.
|
||||
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
|
||||
/// Log payload sent by Config Desk frontend scripts.
|
||||
pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
|
||||
/// Emits one validated frontend event through the KSP Logging facade.
|
||||
pub(crate) use self::frontend_logging::emit_frontend_log_event;
|
||||
/// Console sink configuration exposed to the editor.
|
||||
pub(crate) use self::logging_editor::LoggingConsoleDto;
|
||||
/// Complete editable Logging candidate accepted from the frontend.
|
||||
pub(crate) use self::logging_editor::LoggingDocumentCandidateDto;
|
||||
/// Complete typed Logging document exposed to the editor.
|
||||
pub(crate) use self::logging_editor::LoggingDocumentDto;
|
||||
/// Result of one validated typed Logging persistence operation.
|
||||
pub(crate) use self::logging_editor::LoggingDocumentSaveResultDto;
|
||||
pub(crate) use self::logging_runtime::{LoggingRuntimeFileDto, LoggingRuntimeStatusDto};
|
||||
pub(crate) use self::logging_test::{LoggingTestRequestDto, LoggingTestResultDto};
|
||||
/// One persistent file sink exposed to the editor.
|
||||
pub(crate) use self::logging_editor::LoggingFileDto;
|
||||
/// One Logging sink selector/filter exposed to the editor.
|
||||
pub(crate) use self::logging_editor::LoggingOutputFilterDto;
|
||||
/// One typed Logging profile exposed to the editor.
|
||||
pub(crate) use self::logging_editor::LoggingProfileDto;
|
||||
/// One global Logging target override exposed to the editor.
|
||||
pub(crate) use self::logging_editor::LoggingTargetFilterDto;
|
||||
/// Loads the typed Logging source through ConfigManagement and projects it for the frontend.
|
||||
pub(crate) use self::logging_editor::logging_document;
|
||||
/// Validates, atomically persists and hot-reloads one typed Logging candidate through ConfigManagement and `ksp-logging-lib`.
|
||||
pub(crate) use self::logging_editor::save_logging_document;
|
||||
/// Metadata for one currently active persistent Logging file output.
|
||||
pub(crate) use self::logging_runtime::LoggingRuntimeFileDto;
|
||||
/// Safe observable state of the currently active KSP Logging runtime.
|
||||
pub(crate) use self::logging_runtime::LoggingRuntimeStatusDto;
|
||||
/// Applies one persisted Logging profile explicitly without changing `default_profile` or the source document.
|
||||
pub(crate) use self::logging_runtime::apply_logging_profile;
|
||||
/// Executes the crate-internal count to u64 operation for the owning module.
|
||||
pub(crate) use self::logging_runtime::count_to_u64;
|
||||
/// Creates the stable runtime identity for this Config Desk process launch.
|
||||
pub(crate) use self::logging_runtime::launch_identity;
|
||||
/// Returns safe metadata for the currently active Logging runtime.
|
||||
pub(crate) use self::logging_runtime::logging_runtime_status;
|
||||
/// Executes the crate-internal project logging file operation for the owning module.
|
||||
pub(crate) use self::logging_runtime::project_logging_file;
|
||||
/// Controlled request emitted through the backend KSP Logging facade.
|
||||
pub(crate) use self::logging_test::LoggingTestRequestDto;
|
||||
/// Safe result metadata for one controlled backend Logging test request.
|
||||
pub(crate) use self::logging_test::LoggingTestResultDto;
|
||||
/// Emits one controlled backend test request through `ksp-logging-lib` only.
|
||||
pub(crate) use self::logging_test::emit_logging_test;
|
||||
/// Safe detailed view of one resolved Config profile.
|
||||
pub(crate) use self::profiles::ConfigProfileDetailDto;
|
||||
/// One Config document that exposes the standard `default_profile` / `profiles` contract.
|
||||
pub(crate) use self::profiles::ConfigProfileDocumentDto;
|
||||
/// Resolves one default or explicitly selected profile into a safe inspection DTO.
|
||||
pub(crate) use self::profiles::profile_detail;
|
||||
/// Lists validated Config documents that expose standard profiles.
|
||||
pub(crate) use self::profiles::profile_inventory;
|
||||
/// Privileged reveal request kept separate from ordinary environment-report DTOs.
|
||||
pub(crate) use self::secrets::SecretRevealRequestDto;
|
||||
/// Privileged reveal response. This type deliberately does not derive `Clone` or `Debug`.
|
||||
pub(crate) use self::secrets::SecretRevealResponseDto;
|
||||
/// Reveals one real Secret value only after the dedicated Tauri command has been explicitly invoked.
|
||||
pub(crate) use self::secrets::reveal_secret;
|
||||
/// Command emitted by Rust to the splash frontend.
|
||||
pub(crate) use self::splash::SplashOrderDto;
|
||||
/// Runtime timings used by the common desk splash lifecycle.
|
||||
pub(crate) use self::splash::SplashSettings;
|
||||
pub(crate) use self::tw_main::show_and_focus as show_main_window;
|
||||
pub(crate) use self::tw_splash::frontend_ready as splash_frontend_ready_service;
|
||||
/// Resolves the required main window or returns a typed error.
|
||||
pub(crate) use self::tw_main::require_main_window;
|
||||
/// Shows main window.
|
||||
pub(crate) use self::tw_main::show_main_window;
|
||||
/// Resolves the required splash window or returns a typed error.
|
||||
pub(crate) use self::tw_splash::require_splash_window;
|
||||
/// Executes the crate-internal splash frontend ready service operation for the owning module.
|
||||
pub(crate) use self::tw_splash::splash_frontend_ready_service;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-app-config-desk/src/logging_editor.rs
|
||||
// version: 4
|
||||
// version: 6
|
||||
|
||||
//! Typed Logging document projection and validated persistence for the Config Desk Logging editor.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// One Logging sink selector/filter exposed to the editor.
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
|
||||
@@ -32,7 +32,7 @@ pub(crate) struct LoggingConsoleDto {
|
||||
/// Console format source text.
|
||||
pub(crate) format: String,
|
||||
/// Sink-local selectors.
|
||||
pub(crate) filter: LoggingOutputFilterDto,
|
||||
pub(crate) filter: crate::LoggingOutputFilterDto,
|
||||
}
|
||||
|
||||
/// One persistent file sink exposed to the editor.
|
||||
@@ -53,7 +53,7 @@ pub(crate) struct LoggingFileDto {
|
||||
/// Whether ANSI output is requested.
|
||||
pub(crate) ansi: bool,
|
||||
/// Sink-local selectors.
|
||||
pub(crate) filter: LoggingOutputFilterDto,
|
||||
pub(crate) filter: crate::LoggingOutputFilterDto,
|
||||
}
|
||||
|
||||
/// One global Logging target override exposed to the editor.
|
||||
@@ -79,11 +79,11 @@ pub(crate) struct LoggingProfileDto {
|
||||
/// Span lifecycle source text.
|
||||
pub(crate) span_events: String,
|
||||
/// Console sink configuration.
|
||||
pub(crate) console: LoggingConsoleDto,
|
||||
pub(crate) console: crate::LoggingConsoleDto,
|
||||
/// Persistent file sinks in source order.
|
||||
pub(crate) files: std::vec::Vec<LoggingFileDto>,
|
||||
pub(crate) files: std::vec::Vec<crate::LoggingFileDto>,
|
||||
/// Global target overrides in source order.
|
||||
pub(crate) target_filters: std::vec::Vec<LoggingTargetFilterDto>,
|
||||
pub(crate) target_filters: std::vec::Vec<crate::LoggingTargetFilterDto>,
|
||||
}
|
||||
|
||||
/// Complete typed Logging document exposed to the editor.
|
||||
@@ -102,7 +102,7 @@ pub(crate) struct LoggingDocumentDto {
|
||||
/// Autonomous default profile identifier.
|
||||
pub(crate) default_profile: String,
|
||||
/// Typed profiles in source order.
|
||||
pub(crate) profiles: std::vec::Vec<LoggingProfileDto>,
|
||||
pub(crate) profiles: std::vec::Vec<crate::LoggingProfileDto>,
|
||||
}
|
||||
|
||||
/// Complete editable Logging candidate accepted from the frontend.
|
||||
@@ -115,7 +115,7 @@ pub(crate) struct LoggingDocumentCandidateDto {
|
||||
/// Autonomous default profile identifier.
|
||||
pub(crate) default_profile: String,
|
||||
/// Typed profiles in source order.
|
||||
pub(crate) profiles: std::vec::Vec<LoggingProfileDto>,
|
||||
pub(crate) profiles: std::vec::Vec<crate::LoggingProfileDto>,
|
||||
}
|
||||
|
||||
/// Result of one validated typed Logging persistence operation.
|
||||
@@ -138,12 +138,12 @@ pub(crate) struct LoggingDocumentSaveResultDto {
|
||||
}
|
||||
|
||||
/// Loads the typed Logging source through ConfigManagement and projects it for the frontend.
|
||||
pub(crate) fn document(state: &crate::AppState) -> ksp_core_lib::Result<LoggingDocumentDto> {
|
||||
pub(crate) fn logging_document(state: &crate::AppState) -> ksp_core_lib::Result<LoggingDocumentDto> {
|
||||
return document_from_management(state.config_management());
|
||||
}
|
||||
|
||||
/// Validates, atomically persists and hot-reloads one typed Logging candidate through ConfigManagement and `ksp-logging-lib`.
|
||||
pub(crate) fn save(state: &crate::AppState, candidate: LoggingDocumentCandidateDto) -> ksp_core_lib::Result<LoggingDocumentSaveResultDto> {
|
||||
pub(crate) fn save_logging_document(state: &crate::AppState, candidate: LoggingDocumentCandidateDto) -> ksp_core_lib::Result<LoggingDocumentSaveResultDto> {
|
||||
let file_id = ksp_config_lib::ConfigFileId::new(ksp_config_lib::FILE_ID_STD_LOGGING);
|
||||
let file_id = match file_id {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -255,7 +255,7 @@ fn document_from_management(management: &ksp_config_lib::ConfigManagement) -> ks
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut profiles = std::vec::Vec::<LoggingProfileDto>::with_capacity(source.profiles().len());
|
||||
let mut profiles = std::vec::Vec::<crate::LoggingProfileDto>::with_capacity(source.profiles().len());
|
||||
let mut file_count = 0usize;
|
||||
let mut target_filter_count = 0usize;
|
||||
for profile in source.profiles() {
|
||||
@@ -291,7 +291,7 @@ fn config_candidate(candidate: LoggingDocumentCandidateDto) -> ksp_config_lib::L
|
||||
return ksp_config_lib::LoggingConfigDocument::new(candidate.logs_directory, candidate.default_profile, profiles);
|
||||
}
|
||||
|
||||
fn config_profile(profile: LoggingProfileDto) -> ksp_config_lib::LoggingProfileConfig {
|
||||
fn config_profile(profile: crate::LoggingProfileDto) -> ksp_config_lib::LoggingProfileConfig {
|
||||
let mut files = std::vec::Vec::<ksp_config_lib::LoggingFileConfig>::with_capacity(profile.files.len());
|
||||
for file in profile.files {
|
||||
let mut config =
|
||||
@@ -319,14 +319,14 @@ fn config_profile(profile: LoggingProfileDto) -> ksp_config_lib::LoggingProfileC
|
||||
);
|
||||
}
|
||||
|
||||
fn config_output_filter(filter: LoggingOutputFilterDto) -> ksp_config_lib::LoggingOutputFilterConfig {
|
||||
fn config_output_filter(filter: crate::LoggingOutputFilterDto) -> ksp_config_lib::LoggingOutputFilterConfig {
|
||||
return ksp_config_lib::LoggingOutputFilterConfig::new(filter.level, filter.targets, filter.domains);
|
||||
}
|
||||
|
||||
fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingProfileDto {
|
||||
let mut files = std::vec::Vec::<LoggingFileDto>::with_capacity(profile.files().len());
|
||||
fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> crate::LoggingProfileDto {
|
||||
let mut files = std::vec::Vec::<crate::LoggingFileDto>::with_capacity(profile.files().len());
|
||||
for file in profile.files() {
|
||||
files.push(LoggingFileDto {
|
||||
files.push(crate::LoggingFileDto {
|
||||
output_id: file.output_id().to_owned(),
|
||||
enabled: file.enabled(),
|
||||
path: file.path().to_owned(),
|
||||
@@ -336,15 +336,15 @@ fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingPro
|
||||
filter: project_output_filter(file.filter()),
|
||||
});
|
||||
}
|
||||
let mut target_filters = std::vec::Vec::<LoggingTargetFilterDto>::with_capacity(profile.target_filters().len());
|
||||
let mut target_filters = std::vec::Vec::<crate::LoggingTargetFilterDto>::with_capacity(profile.target_filters().len());
|
||||
for target_filter in profile.target_filters() {
|
||||
target_filters.push(LoggingTargetFilterDto { target_prefix: target_filter.target_prefix().to_owned(), level: target_filter.level().to_owned() });
|
||||
target_filters.push(crate::LoggingTargetFilterDto { target_prefix: target_filter.target_prefix().to_owned(), level: target_filter.level().to_owned() });
|
||||
}
|
||||
return LoggingProfileDto {
|
||||
return crate::LoggingProfileDto {
|
||||
profile_id: profile.profile_id().to_owned(),
|
||||
default_filter: profile.default_filter().to_owned(),
|
||||
span_events: profile.span_events().to_owned(),
|
||||
console: LoggingConsoleDto {
|
||||
console: crate::LoggingConsoleDto {
|
||||
enabled: profile.console().enabled(),
|
||||
output: profile.console().output().to_owned(),
|
||||
ansi: profile.console().ansi(),
|
||||
@@ -356,8 +356,8 @@ fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingPro
|
||||
};
|
||||
}
|
||||
|
||||
fn project_output_filter(filter: &ksp_config_lib::LoggingOutputFilterConfig) -> LoggingOutputFilterDto {
|
||||
return LoggingOutputFilterDto {
|
||||
fn project_output_filter(filter: &ksp_config_lib::LoggingOutputFilterConfig) -> crate::LoggingOutputFilterDto {
|
||||
return crate::LoggingOutputFilterDto {
|
||||
level: filter.level().to_owned(),
|
||||
targets: filter.targets().to_vec(),
|
||||
domains: filter.domains().to_vec(),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-app-config-desk/src/logging_runtime.rs
|
||||
// version: 2
|
||||
// version: 4
|
||||
|
||||
//! Runtime Logging metadata, launch identity and explicit profile application for Config Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Metadata for one currently active persistent Logging file output.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
@@ -56,12 +56,12 @@ pub(crate) fn launch_identity() -> ksp_core_lib::Result<ksp_logging_lib::Logging
|
||||
}
|
||||
|
||||
/// Returns safe metadata for the currently active Logging runtime.
|
||||
pub(crate) fn status(state: &crate::AppState) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> {
|
||||
pub(crate) fn logging_runtime_status(state: &crate::AppState) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> {
|
||||
return state.logging_runtime_status();
|
||||
}
|
||||
|
||||
/// Applies one persisted Logging profile explicitly without changing `default_profile` or the source document.
|
||||
pub(crate) fn apply_profile(state: &crate::AppState, profile_id: &str) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> {
|
||||
pub(crate) fn apply_logging_profile(state: &crate::AppState, profile_id: &str) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> {
|
||||
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||
let environment = match environment {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -88,7 +88,8 @@ pub(crate) fn apply_profile(state: &crate::AppState, profile_id: &str) -> ksp_co
|
||||
return state.logging_runtime_status();
|
||||
}
|
||||
|
||||
pub(crate) fn project_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> LoggingRuntimeFileDto {
|
||||
/// Executes the crate-internal project logging file operation for the owning module.
|
||||
pub(crate) fn project_logging_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> LoggingRuntimeFileDto {
|
||||
return LoggingRuntimeFileDto {
|
||||
output_id: metadata.output_id().to_owned(),
|
||||
directory: metadata.directory().display().to_string(),
|
||||
@@ -97,7 +98,7 @@ pub(crate) fn project_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> L
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &'static str {
|
||||
const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &'static str {
|
||||
return match rotation {
|
||||
ksp_logging_lib::FileRotation::Never => "never",
|
||||
ksp_logging_lib::FileRotation::Hourly => "hourly",
|
||||
@@ -105,6 +106,7 @@ pub(crate) const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes the crate-internal count to u64 operation for the owning module.
|
||||
pub(crate) fn count_to_u64(value: usize) -> u64 {
|
||||
let converted = u64::try_from(value);
|
||||
return match converted {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-app-config-desk/src/logging_test.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Controlled Logging test events for validating KSP runtime routing from Config Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Controlled request emitted through the backend KSP Logging facade.
|
||||
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||
@@ -56,7 +56,7 @@ enum LoggingTestTarget {
|
||||
}
|
||||
|
||||
/// Emits one controlled backend test request through `ksp-logging-lib` only.
|
||||
pub(crate) fn emit(state: &crate::AppState, request: LoggingTestRequestDto) -> ksp_core_lib::Result<LoggingTestResultDto> {
|
||||
pub(crate) fn emit_logging_test(state: &crate::AppState, request: LoggingTestRequestDto) -> ksp_core_lib::Result<LoggingTestResultDto> {
|
||||
let level = parse_level(request.level.as_str());
|
||||
let level = match level {
|
||||
std::result::Result::Ok(value) => value,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-app-config-desk/src/profiles.rs
|
||||
// version: 1
|
||||
// version: 4
|
||||
|
||||
//! Safe Config profile inspection and provenance projections for Config Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// One Config document that exposes the standard `default_profile` / `profiles` contract.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
@@ -77,13 +77,22 @@ pub(crate) struct ConfigProfileDetailDto {
|
||||
pub(crate) environment_provenance: std::vec::Vec<ConfigProfileEnvironmentProvenanceDto>,
|
||||
}
|
||||
|
||||
struct ProfileContract {
|
||||
default_profile: String,
|
||||
profile_ids: std::vec::Vec<String>,
|
||||
}
|
||||
|
||||
/// Lists validated Config documents that expose standard profiles.
|
||||
pub(crate) fn inventory(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigProfileDocumentDto>> {
|
||||
pub(crate) fn profile_inventory(state: &crate::AppState) -> ksp_core_lib::Result<std::vec::Vec<ConfigProfileDocumentDto>> {
|
||||
return inventory_from_management(state.config_management());
|
||||
}
|
||||
|
||||
/// Resolves one default or explicitly selected profile into a safe inspection DTO.
|
||||
pub(crate) fn detail(state: &crate::AppState, file_id: &str, requested_profile: std::option::Option<&str>) -> ksp_core_lib::Result<ConfigProfileDetailDto> {
|
||||
pub(crate) fn profile_detail(
|
||||
state: &crate::AppState,
|
||||
file_id: &str,
|
||||
requested_profile: std::option::Option<&str>,
|
||||
) -> ksp_core_lib::Result<ConfigProfileDetailDto> {
|
||||
return detail_from_management(state.config_management(), file_id, requested_profile);
|
||||
}
|
||||
|
||||
@@ -213,11 +222,6 @@ fn detail_from_management(
|
||||
});
|
||||
}
|
||||
|
||||
struct ProfileContract {
|
||||
default_profile: String,
|
||||
profile_ids: std::vec::Vec<String>,
|
||||
}
|
||||
|
||||
fn profile_contract(document: &ksp_config_lib::ConfigJsonDocument) -> ksp_core_lib::Result<std::option::Option<ProfileContract>> {
|
||||
let root = match document.value().as_object() {
|
||||
std::option::Option::Some(value) => value,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-app-config-desk/src/secrets.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Privileged, explicitly requested Config Secret reveal boundary for Config Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Privileged reveal request kept separate from ordinary environment-report DTOs.
|
||||
#[derive(serde::Deserialize, TS)]
|
||||
@@ -56,7 +56,7 @@ impl SecretRevealSource {
|
||||
}
|
||||
|
||||
/// Reveals one real Secret value only after the dedicated Tauri command has been explicitly invoked.
|
||||
pub(crate) fn reveal(state: &crate::AppState, request: SecretRevealRequestDto) -> ksp_core_lib::Result<SecretRevealResponseDto> {
|
||||
pub(crate) fn reveal_secret(state: &crate::AppState, request: SecretRevealRequestDto) -> ksp_core_lib::Result<SecretRevealResponseDto> {
|
||||
return reveal_from_management(state.config_management(), request);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// file: crates/ksp-app-config-desk/src/splash.rs
|
||||
// version: 3
|
||||
// version: 5
|
||||
|
||||
//! Common splash settings and frontend event contracts for Config Desk.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
|
||||
const ENV_SPLASH_FADE_MS: &str = "KSP_DESK_SPLASH_FADE_MS";
|
||||
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
|
||||
const DEFAULT_SPLASH_FADE_MS: u32 = 300;
|
||||
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
|
||||
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
|
||||
const ENV_SPLASH_FADE_MS: &str = "KSP_DESK_SPLASH_FADE_MS";
|
||||
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
|
||||
const MAX_SPLASH_FADE_MS: u32 = 10_000;
|
||||
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
|
||||
|
||||
/// Runtime timings used by the common desk splash lifecycle.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -94,14 +94,6 @@ impl SplashSettings {
|
||||
}
|
||||
}
|
||||
|
||||
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
|
||||
return match source {
|
||||
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
|
||||
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
|
||||
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
|
||||
};
|
||||
}
|
||||
|
||||
/// Command emitted by Rust to the splash frontend.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -116,12 +108,21 @@ pub(crate) struct SplashOrderDto {
|
||||
}
|
||||
|
||||
impl SplashOrderDto {
|
||||
/// Creates a new `SplashOrderDto` value.
|
||||
#[must_use]
|
||||
pub(crate) fn new(action: &str, message: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self {
|
||||
return Self { action: action.to_owned(), message: message.map(std::string::ToString::to_string), duration_ms };
|
||||
}
|
||||
}
|
||||
|
||||
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
|
||||
return match source {
|
||||
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
|
||||
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
|
||||
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
|
||||
};
|
||||
}
|
||||
|
||||
fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> {
|
||||
let parsed = value.parse::<u64>();
|
||||
let parsed = match parsed {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/src/tauri.rs
|
||||
// version: 15
|
||||
// version: 16
|
||||
|
||||
//! Tauri runtime assembly for the KSP configuration desktop application.
|
||||
|
||||
@@ -60,11 +60,11 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
|
||||
|
||||
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.setup(|app| {
|
||||
let splash = crate::tw_splash::require_window(app);
|
||||
let splash = crate::require_splash_window(app);
|
||||
if let std::result::Result::Err(error) = splash {
|
||||
return std::result::Result::Err(std::boxed::Box::new(error));
|
||||
}
|
||||
let main = crate::tw_main::require_window(app);
|
||||
let main = crate::require_main_window(app);
|
||||
if let std::result::Result::Err(error) = main {
|
||||
return std::result::Result::Err(std::boxed::Box::new(error));
|
||||
}
|
||||
@@ -83,7 +83,7 @@ fn get_app_snapshot(state: tauri::State<'_, crate::AppState>) -> std::result::Re
|
||||
|
||||
#[tauri::command]
|
||||
fn get_config_documents(state: tauri::State<'_, crate::AppState>) -> std::vec::Vec<crate::ConfigDocumentSummaryDto> {
|
||||
return crate::documents::inventory(&state);
|
||||
return crate::document_inventory(&state);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -91,7 +91,7 @@ fn get_config_document_detail(
|
||||
file_id: String,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::ConfigDocumentDetailDto, crate::ConfigDocumentErrorDto> {
|
||||
return crate::documents::detail(&state, file_id.as_str());
|
||||
return crate::document_detail(&state, file_id.as_str());
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -100,14 +100,14 @@ fn save_config_document_source(
|
||||
source: String,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::ConfigDocumentSaveResultDto, crate::ConfigDocumentErrorDto> {
|
||||
return crate::documents::save_source(&state, file_id.as_str(), source.as_str());
|
||||
return crate::save_document_source(&state, file_id.as_str(), source.as_str());
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_config_profile_documents(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<std::vec::Vec<crate::ConfigProfileDocumentDto>, crate::CommandErrorDto> {
|
||||
let result = crate::profiles::inventory(&state);
|
||||
let result = crate::profile_inventory(&state);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
@@ -120,7 +120,7 @@ fn get_config_profile_detail(
|
||||
profile_id: std::option::Option<String>,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::ConfigProfileDetailDto, crate::CommandErrorDto> {
|
||||
let result = crate::profiles::detail(&state, file_id.as_str(), profile_id.as_deref());
|
||||
let result = crate::profile_detail(&state, file_id.as_str(), profile_id.as_deref());
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
@@ -131,7 +131,7 @@ fn get_config_profile_detail(
|
||||
fn get_environment_report(
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<std::vec::Vec<crate::ConfigEnvironmentReportDto>, crate::CommandErrorDto> {
|
||||
let result = crate::environment::report(&state);
|
||||
let result = crate::environment_report(&state);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
@@ -144,7 +144,7 @@ fn set_environment_value(
|
||||
value: String,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> {
|
||||
let result = crate::environment::set_value(&state, variable_name.as_str(), value.as_str());
|
||||
let result = crate::set_environment_value(&state, variable_name.as_str(), value.as_str());
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
@@ -156,7 +156,7 @@ fn remove_environment_value(
|
||||
variable_name: String,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::ConfigEnvironmentChangeDto, crate::CommandErrorDto> {
|
||||
let result = crate::environment::remove_value(&state, variable_name.as_str());
|
||||
let result = crate::remove_environment_value(&state, variable_name.as_str());
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
@@ -168,7 +168,7 @@ fn reveal_environment_value(
|
||||
request: crate::SecretRevealRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::SecretRevealResponseDto, crate::CommandErrorDto> {
|
||||
let result = crate::secrets::reveal(&state, request);
|
||||
let result = crate::reveal_secret(&state, request);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
@@ -177,7 +177,7 @@ fn reveal_environment_value(
|
||||
|
||||
#[tauri::command]
|
||||
fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingDocumentDto, crate::CommandErrorDto> {
|
||||
let result = crate::logging_editor::document(&state);
|
||||
let result = crate::logging_document(&state);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
@@ -186,7 +186,7 @@ fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result
|
||||
|
||||
#[tauri::command]
|
||||
fn get_logging_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> {
|
||||
let result = crate::logging_runtime::status(&state);
|
||||
let result = crate::logging_runtime_status(&state);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
@@ -198,7 +198,7 @@ fn apply_logging_profile(
|
||||
profile_id: String,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> {
|
||||
let result = crate::logging_runtime::apply_profile(&state, profile_id.as_str());
|
||||
let result = crate::apply_logging_profile(&state, profile_id.as_str());
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
@@ -210,7 +210,7 @@ fn save_logging_document(
|
||||
candidate: crate::LoggingDocumentCandidateDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::LoggingDocumentSaveResultDto, crate::CommandErrorDto> {
|
||||
let result = crate::logging_editor::save(&state, candidate);
|
||||
let result = crate::save_logging_document(&state, candidate);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
@@ -222,7 +222,7 @@ fn emit_logging_test(
|
||||
request: crate::LoggingTestRequestDto,
|
||||
state: tauri::State<'_, crate::AppState>,
|
||||
) -> std::result::Result<crate::LoggingTestResultDto, crate::CommandErrorDto> {
|
||||
let result = crate::logging_test::emit(&state, request);
|
||||
let result = crate::emit_logging_test(&state, request);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
// file: crates/ksp-app-config-desk/src/tw_main.rs
|
||||
// version: 1
|
||||
// version: 4
|
||||
|
||||
//! Tauri-window helpers for the Config Desk main window.
|
||||
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
/// Crate-internal `WINDOW_LABEL_MAIN` constant.
|
||||
pub(crate) const WINDOW_LABEL_MAIN: &str = "main";
|
||||
|
||||
pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||
/// Resolves the required main window or returns a typed error.
|
||||
pub(crate) fn require_main_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||
let window = manager.get_webview_window(WINDOW_LABEL_MAIN);
|
||||
return match window {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
@@ -18,8 +20,9 @@ pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn show_and_focus(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
|
||||
let window = require_window(app);
|
||||
/// Shows main window.
|
||||
pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
|
||||
let window = crate::require_main_window(app);
|
||||
let window = match window {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
// file: crates/ksp-app-config-desk/src/tw_splash.rs
|
||||
// version: 2
|
||||
// version: 5
|
||||
|
||||
//! Tauri-window lifecycle for the Config Desk splash window.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
/// Crate-internal `WINDOW_LABEL_SPLASH` constant.
|
||||
pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash";
|
||||
|
||||
const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
|
||||
|
||||
pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||
/// Resolves the required splash window or returns a typed error.
|
||||
pub(crate) fn require_splash_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
|
||||
let window = manager.get_webview_window(WINDOW_LABEL_SPLASH);
|
||||
return match window {
|
||||
std::option::Option::Some(value) => std::result::Result::Ok(value),
|
||||
@@ -20,7 +23,12 @@ pub(crate) fn require_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) async fn frontend_ready(app: tauri::AppHandle, invoking_window: tauri::WebviewWindow, state: &crate::AppState) -> ksp_core_lib::Result<()> {
|
||||
/// Executes the crate-internal splash frontend ready service operation for the owning module.
|
||||
pub(crate) async fn splash_frontend_ready_service(
|
||||
app: tauri::AppHandle,
|
||||
invoking_window: tauri::WebviewWindow,
|
||||
state: &crate::AppState,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
if invoking_window.label() != WINDOW_LABEL_SPLASH {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
// file: crates/ksp-app-config-desk/unit_tests/documents.rs
|
||||
// version: 1
|
||||
|
||||
use super::*;
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn diagnostic_classifier_distinguishes_json_schema_semantic_and_effective_errors() {
|
||||
let json = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "json");
|
||||
assert_eq!(classify_error(&json), STAGE_JSON);
|
||||
assert_eq!(super::classify_error(&json), super::STAGE_JSON);
|
||||
let schema = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_SCHEMA_VALIDATION_FAILED, "schema");
|
||||
assert_eq!(classify_error(&schema), STAGE_SCHEMA);
|
||||
assert_eq!(super::classify_error(&schema), super::STAGE_SCHEMA);
|
||||
let semantic = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "semantic");
|
||||
assert_eq!(classify_error(&semantic), STAGE_SEMANTIC);
|
||||
assert_eq!(super::classify_error(&semantic), super::STAGE_SEMANTIC);
|
||||
let effective = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective");
|
||||
assert_eq!(classify_error(&effective), STAGE_EFFECTIVE);
|
||||
assert_eq!(super::classify_error(&effective), super::STAGE_EFFECTIVE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_source_read_errors_are_classified_as_schema_failures() {
|
||||
let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_FILE_READ_FAILED, "missing schema").with_context("file_id", "schema.std.logging");
|
||||
assert_eq!(classify_error(&error), STAGE_SCHEMA);
|
||||
assert_eq!(super::classify_error(&error), super::STAGE_SCHEMA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_error_projection_does_not_expose_arbitrary_error_context() {
|
||||
let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "invalid source").with_context("detail", "sensitive-canary");
|
||||
let projection = ConfigDocumentErrorDto::from_error(&error);
|
||||
assert_eq!(projection.diagnostic_stage, STAGE_JSON);
|
||||
let projection = crate::ConfigDocumentErrorDto::from_error(&error);
|
||||
assert_eq!(projection.diagnostic_stage, super::STAGE_JSON);
|
||||
assert_eq!(projection.error.domain, "config");
|
||||
assert_eq!(projection.error.code, "json_syntax_invalid");
|
||||
assert_eq!(projection.error.message, "invalid source");
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// file: crates/ksp-app-config-desk/unit_tests/dto_common.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn command_error_projection_excludes_context_values() {
|
||||
let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failure"), "safe message").with_context("secret_canary", "must-not-be-exported");
|
||||
let dto = super::CommandErrorDto::from_error(&error);
|
||||
let dto = crate::CommandErrorDto::from_error(&error);
|
||||
assert_eq!(dto.domain, "test");
|
||||
assert_eq!(dto.code, "failure");
|
||||
assert_eq!(dto.message, "safe message");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/unit_tests/environment.rs
|
||||
// version: 2
|
||||
// version: 4
|
||||
|
||||
#[test]
|
||||
fn namespace_projection_distinguishes_ksp_and_kspb_sensitivity_families() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/unit_tests/logging_editor.rs
|
||||
// version: 4
|
||||
// version: 7
|
||||
|
||||
#[test]
|
||||
fn committed_logging_document_maps_complete_read_only_editor_contract() {
|
||||
@@ -66,7 +66,7 @@ fn editor_candidate_round_trips_through_typed_config_contract() {
|
||||
let document = super::document_from_management(&management);
|
||||
assert!(document.is_ok(), "Logging editor document should load: {document:?}");
|
||||
if let std::result::Result::Ok(document) = document {
|
||||
let candidate = super::LoggingDocumentCandidateDto {
|
||||
let candidate = crate::LoggingDocumentCandidateDto {
|
||||
logs_directory: document.logs_directory.clone(),
|
||||
default_profile: document.default_profile.clone(),
|
||||
profiles: document.profiles.clone(),
|
||||
@@ -89,42 +89,42 @@ fn editor_candidate_round_trips_through_typed_config_contract() {
|
||||
|
||||
#[test]
|
||||
fn profile_candidate_preserves_multi_file_and_target_filter_shape() {
|
||||
let profile = super::LoggingProfileDto {
|
||||
let profile = crate::LoggingProfileDto {
|
||||
profile_id: "clone_test".to_owned(),
|
||||
default_filter: "info".to_owned(),
|
||||
span_events: "full".to_owned(),
|
||||
console: super::LoggingConsoleDto {
|
||||
console: crate::LoggingConsoleDto {
|
||||
enabled: true,
|
||||
output: "stdout".to_owned(),
|
||||
ansi: false,
|
||||
format: "human".to_owned(),
|
||||
filter: super::LoggingOutputFilterDto { level: "info".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] },
|
||||
filter: crate::LoggingOutputFilterDto { level: "info".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] },
|
||||
},
|
||||
files: std::vec![
|
||||
super::LoggingFileDto {
|
||||
crate::LoggingFileDto {
|
||||
output_id: "file.one".to_owned(),
|
||||
enabled: true,
|
||||
path: "one.log".to_owned(),
|
||||
rotation: "daily".to_owned(),
|
||||
format: "human".to_owned(),
|
||||
ansi: false,
|
||||
filter: super::LoggingOutputFilterDto { level: "debug".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] },
|
||||
filter: crate::LoggingOutputFilterDto { level: "debug".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] },
|
||||
},
|
||||
super::LoggingFileDto {
|
||||
crate::LoggingFileDto {
|
||||
output_id: "file.two".to_owned(),
|
||||
enabled: false,
|
||||
path: "two.jsonl".to_owned(),
|
||||
rotation: "hourly".to_owned(),
|
||||
format: "json".to_owned(),
|
||||
ansi: false,
|
||||
filter: super::LoggingOutputFilterDto {
|
||||
filter: crate::LoggingOutputFilterDto {
|
||||
level: "error".to_owned(),
|
||||
targets: std::vec!["ksp-config-lib".to_owned()],
|
||||
domains: std::vec!["config".to_owned()],
|
||||
},
|
||||
},
|
||||
],
|
||||
target_filters: std::vec![super::LoggingTargetFilterDto { target_prefix: "ksp-app-config-desk".to_owned(), level: "trace".to_owned() }],
|
||||
target_filters: std::vec![crate::LoggingTargetFilterDto { target_prefix: "ksp-app-config-desk".to_owned(), level: "trace".to_owned() }],
|
||||
};
|
||||
let config = super::config_profile(profile);
|
||||
assert_eq!(config.profile_id(), "clone_test");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-app-config-desk/unit_tests/logging_runtime.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn launch_identity_is_safe_and_contains_application_and_process_identity() {
|
||||
let identity = super::launch_identity();
|
||||
let identity = crate::launch_identity();
|
||||
assert!(identity.is_ok());
|
||||
if let std::result::Result::Ok(identity) = identity {
|
||||
assert_eq!(identity.application_id(), crate::TRACING_TARGET);
|
||||
@@ -21,6 +21,6 @@ fn runtime_rotation_labels_are_stable() {
|
||||
|
||||
#[test]
|
||||
fn runtime_count_projection_is_non_lossy_for_normal_values() {
|
||||
assert_eq!(super::count_to_u64(0), 0);
|
||||
assert_eq!(super::count_to_u64(42), 42);
|
||||
assert_eq!(crate::count_to_u64(0), 0);
|
||||
assert_eq!(crate::count_to_u64(42), 42);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
// file: crates/ksp-app-config-desk/unit_tests/logging_test.rs
|
||||
// version: 1
|
||||
|
||||
use super::{LoggingTestLevel, LoggingTestTarget};
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn controlled_logging_test_levels_include_all_five_levels_and_batch_mode() {
|
||||
assert!(matches!(super::parse_level("trace"), std::result::Result::Ok(LoggingTestLevel::Trace)));
|
||||
assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(LoggingTestLevel::Debug)));
|
||||
assert!(matches!(super::parse_level("info"), std::result::Result::Ok(LoggingTestLevel::Info)));
|
||||
assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(LoggingTestLevel::Warn)));
|
||||
assert!(matches!(super::parse_level("error"), std::result::Result::Ok(LoggingTestLevel::Error)));
|
||||
assert!(matches!(super::parse_level("all"), std::result::Result::Ok(LoggingTestLevel::All)));
|
||||
assert!(matches!(super::parse_level("trace"), std::result::Result::Ok(super::LoggingTestLevel::Trace)));
|
||||
assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(super::LoggingTestLevel::Debug)));
|
||||
assert!(matches!(super::parse_level("info"), std::result::Result::Ok(super::LoggingTestLevel::Info)));
|
||||
assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(super::LoggingTestLevel::Warn)));
|
||||
assert!(matches!(super::parse_level("error"), std::result::Result::Ok(super::LoggingTestLevel::Error)));
|
||||
assert!(matches!(super::parse_level("all"), std::result::Result::Ok(super::LoggingTestLevel::All)));
|
||||
assert!(super::parse_level("fatal").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controlled_logging_test_targets_are_static_ksp_targets() {
|
||||
assert!(matches!(super::parse_target("app"), std::result::Result::Ok(LoggingTestTarget::App)));
|
||||
assert!(matches!(super::parse_target("logging_test"), std::result::Result::Ok(LoggingTestTarget::Dedicated)));
|
||||
assert_eq!(super::target_value(LoggingTestTarget::App), crate::TRACING_TARGET);
|
||||
assert_eq!(super::target_value(LoggingTestTarget::Dedicated), crate::TRACING_TARGET_LOGGING_TEST);
|
||||
assert!(matches!(super::parse_target("app"), std::result::Result::Ok(super::LoggingTestTarget::App)));
|
||||
assert!(matches!(super::parse_target("logging_test"), std::result::Result::Ok(super::LoggingTestTarget::Dedicated)));
|
||||
assert_eq!(super::target_value(super::LoggingTestTarget::App), crate::TRACING_TARGET);
|
||||
assert_eq!(super::target_value(super::LoggingTestTarget::Dedicated), crate::TRACING_TARGET_LOGGING_TEST);
|
||||
assert!(super::parse_target("external").is_err());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs
|
||||
// version: 3
|
||||
// version: 5
|
||||
|
||||
#[test]
|
||||
fn profile_inventory_exposes_registered_standard_profile_documents() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/unit_tests/secrets.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn reveal_policy_accepts_only_secret_names_and_supported_sources() {
|
||||
@@ -21,7 +21,7 @@ fn reveal_policy_accepts_only_secret_names_and_supported_sources() {
|
||||
|
||||
#[test]
|
||||
fn reveal_response_type_does_not_require_debug_or_clone_contracts() {
|
||||
let response = super::SecretRevealResponseDto {
|
||||
let response = crate::SecretRevealResponseDto {
|
||||
variable_name: "KSP_SECRET_TEST_VALUE".to_owned(),
|
||||
source: "dotenv".to_owned(),
|
||||
value: std::option::Option::Some("secret-canary".to_owned()),
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// file: crates/ksp-config-lib/src/bootstrap.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Default root containing KSP runtime configuration documents.
|
||||
pub const DEFAULT_CFG_PATH: &str = "config";
|
||||
/// Default root containing KSP JSON schemas.
|
||||
pub const DEFAULT_SCHEMA_PATH: &str = "config/schemas";
|
||||
/// Bootstrap argument used to replace the configuration document root.
|
||||
pub const ARG_CFG_PATH: &str = "--cfgpath";
|
||||
/// Bootstrap argument used to replace the schema root.
|
||||
pub const ARG_SCHEMA_PATH: &str = "--schemapath";
|
||||
/// Default root containing KSP runtime configuration documents.
|
||||
pub const DEFAULT_CFG_PATH: &str = "config";
|
||||
/// Default root containing KSP JSON schemas.
|
||||
pub const DEFAULT_SCHEMA_PATH: &str = "config/schemas";
|
||||
|
||||
/// Non-recursive bootstrap options required before Config can resolve any managed document.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/composite.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// One resolved document component selected by a composite profile.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -118,6 +118,7 @@ struct CompositeDocumentReferenceSource {
|
||||
profile_id: std::option::Option<String>,
|
||||
}
|
||||
|
||||
/// Validates composite document contract.
|
||||
pub(crate) fn validate_composite_document_contract(engine: &crate::ConfigDocumentEngine, document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
|
||||
if !document.file_id().as_str().starts_with("cfg.composite.") {
|
||||
return std::result::Result::Ok(());
|
||||
@@ -198,12 +199,8 @@ fn resolve_composite_document(
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let resolved = crate::profile::load_resolved_profile_with_source(
|
||||
engine,
|
||||
&file_id,
|
||||
reference.profile_id.as_deref(),
|
||||
crate::ConfigProfileSelectionSource::Composite,
|
||||
);
|
||||
let resolved =
|
||||
crate::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
|
||||
let resolved = match resolved {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -262,8 +259,7 @@ fn validate_reference(
|
||||
"referenced file_id is not a Config document",
|
||||
));
|
||||
}
|
||||
let resolved =
|
||||
crate::profile::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
|
||||
let resolved = crate::load_resolved_profile_with_source(engine, &file_id, reference.profile_id.as_deref(), crate::ConfigProfileSelectionSource::Composite);
|
||||
if let std::result::Result::Err(error) = resolved {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/document.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
/// A Config-managed JSON document that has passed syntax, schema and current semantic validation.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -85,6 +85,7 @@ impl ConfigDocumentEngine {
|
||||
return self.validate_document(document, &schema_file_id);
|
||||
}
|
||||
|
||||
/// Validates source candidate.
|
||||
pub(crate) fn validate_source_candidate(&self, file_id: &crate::ConfigFileId, source: &str) -> ksp_core_lib::Result<ConfigJsonDocument> {
|
||||
let path = self.registry.resolve_path(&self.bootstrap, file_id);
|
||||
let path = match path {
|
||||
@@ -99,6 +100,7 @@ impl ConfigDocumentEngine {
|
||||
return self.validate_candidate(file_id, value);
|
||||
}
|
||||
|
||||
/// Validates candidate.
|
||||
pub(crate) fn validate_candidate(&self, file_id: &crate::ConfigFileId, value: serde_json::Value) -> ksp_core_lib::Result<ConfigJsonDocument> {
|
||||
let descriptor = self.registry.descriptor(file_id);
|
||||
let descriptor = match descriptor {
|
||||
@@ -256,11 +258,11 @@ fn validate_instance(document: &ConfigJsonDocument, schema: &ConfigJsonDocument)
|
||||
}
|
||||
|
||||
fn validate_document_semantics(engine: &ConfigDocumentEngine, document: &ConfigJsonDocument) -> ksp_core_lib::Result<()> {
|
||||
let profile_validation = crate::profile::validate_document_profile_contract(document);
|
||||
let profile_validation = crate::validate_document_profile_contract(document);
|
||||
if let std::result::Result::Err(error) = profile_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let composite_validation = crate::composite::validate_composite_document_contract(engine, document);
|
||||
let composite_validation = crate::validate_composite_document_contract(engine, document);
|
||||
if let std::result::Result::Err(error) = composite_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// file: crates/ksp-config-lib/src/environment.rs
|
||||
// version: 5
|
||||
|
||||
/// Default local environment file read by Config from the process launch directory.
|
||||
pub const DEFAULT_DOTENV_PATH: &str = ".env";
|
||||
// version: 8
|
||||
|
||||
/// Versioned environment contract template expected at the repository/runtime root.
|
||||
pub const DEFAULT_DOTENV_EXAMPLE_PATH: &str = ".env.example";
|
||||
/// Default local environment file read by Config from the process launch directory.
|
||||
pub const DEFAULT_DOTENV_PATH: &str = ".env";
|
||||
|
||||
const LOGGING_DOMAIN: &str = "config.environment";
|
||||
|
||||
@@ -230,6 +229,7 @@ impl ConfigEnvironment {
|
||||
};
|
||||
}
|
||||
|
||||
/// Loads from dotenv path.
|
||||
pub(crate) fn load_from_dotenv_path(dotenv_path: &std::path::Path) -> ksp_core_lib::Result<Self> {
|
||||
let process = collect_process_environment(std::env::vars_os());
|
||||
let process = match process {
|
||||
@@ -244,57 +244,24 @@ impl ConfigEnvironment {
|
||||
return std::result::Result::Ok(Self { process, dotenv, dotenv_path: dotenv_path.to_path_buf() });
|
||||
}
|
||||
|
||||
/// Returns the current process values.
|
||||
pub(crate) const fn process_values(&self) -> &std::collections::BTreeMap<String, String> {
|
||||
return &self.process;
|
||||
}
|
||||
|
||||
/// Returns the current dotenv values.
|
||||
pub(crate) const fn dotenv_values(&self) -> &std::collections::BTreeMap<String, String> {
|
||||
return &self.dotenv;
|
||||
}
|
||||
|
||||
/// Builds `ConfigEnvironment` from maps.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn from_maps(process: std::collections::BTreeMap<String, String>, dotenv: std::collections::BTreeMap<String, String>) -> Self {
|
||||
return Self { process, dotenv, dotenv_path: std::path::PathBuf::from(DEFAULT_DOTENV_PATH) };
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_process_environment<I>(values: I) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>>
|
||||
where
|
||||
I: std::iter::IntoIterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
|
||||
{
|
||||
let mut output = std::collections::BTreeMap::<String, String>::new();
|
||||
for (name, value) in values {
|
||||
let name = match name.to_str() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
if !has_supported_namespace(name) {
|
||||
continue;
|
||||
}
|
||||
let validation = validate_supported_variable_name(name);
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let value = value.into_string();
|
||||
let value = match value {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(invalid_environment_value_error(name)),
|
||||
};
|
||||
output.insert(name.to_owned(), value);
|
||||
}
|
||||
return std::result::Result::Ok(output);
|
||||
}
|
||||
|
||||
fn load_dotenv_file(path: &std::path::Path) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
|
||||
let content = std::fs::read_to_string(path);
|
||||
let content = match content {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => return std::result::Result::Ok(std::collections::BTreeMap::new()),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(dotenv_read_error(path, error)),
|
||||
};
|
||||
return parse_dotenv_content(path, content.as_str());
|
||||
}
|
||||
|
||||
/// Executes the crate-internal parse dotenv content operation for the owning module.
|
||||
pub(crate) fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
|
||||
let mut output = std::collections::BTreeMap::<String, String>::new();
|
||||
for (line_index, raw_line) in content.lines().enumerate() {
|
||||
@@ -342,6 +309,61 @@ pub(crate) fn parse_dotenv_content(path: &std::path::Path, content: &str) -> ksp
|
||||
return std::result::Result::Ok(output);
|
||||
}
|
||||
|
||||
/// Validates supported variable name.
|
||||
pub(crate) fn validate_supported_variable_name(variable_name: &str) -> ksp_core_lib::Result<()> {
|
||||
if !has_supported_namespace(variable_name) {
|
||||
return std::result::Result::Err(invalid_variable_error(variable_name, "variable must use the KSP_ or KSPB_ namespace"));
|
||||
}
|
||||
let prefix_length = if variable_name.starts_with("KSPB_") { 5 } else { 4 };
|
||||
if variable_name.len() <= prefix_length {
|
||||
return std::result::Result::Err(invalid_variable_error(variable_name, "variable namespace must be followed by a name"));
|
||||
}
|
||||
for byte in variable_name.bytes() {
|
||||
let valid = byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_';
|
||||
if !valid {
|
||||
return std::result::Result::Err(invalid_variable_error(variable_name, "variable names use uppercase ASCII letters, digits and underscores"));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn collect_process_environment<I>(values: I) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>>
|
||||
where
|
||||
I: std::iter::IntoIterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
|
||||
{
|
||||
let mut output = std::collections::BTreeMap::<String, String>::new();
|
||||
for (name, value) in values {
|
||||
let name = match name.to_str() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
if !has_supported_namespace(name) {
|
||||
continue;
|
||||
}
|
||||
let validation = validate_supported_variable_name(name);
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let value = value.into_string();
|
||||
let value = match value {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(invalid_environment_value_error(name)),
|
||||
};
|
||||
output.insert(name.to_owned(), value);
|
||||
}
|
||||
return std::result::Result::Ok(output);
|
||||
}
|
||||
|
||||
fn load_dotenv_file(path: &std::path::Path) -> ksp_core_lib::Result<std::collections::BTreeMap<String, String>> {
|
||||
let content = std::fs::read_to_string(path);
|
||||
let content = match content {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => return std::result::Result::Ok(std::collections::BTreeMap::new()),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(dotenv_read_error(path, error)),
|
||||
};
|
||||
return crate::parse_dotenv_content(path, content.as_str());
|
||||
}
|
||||
|
||||
fn parse_dotenv_value(path: &std::path::Path, line_number: usize, raw_value: &str) -> ksp_core_lib::Result<String> {
|
||||
if raw_value.starts_with('\'') && (raw_value.len() < 2 || !raw_value.ends_with('\'')) {
|
||||
return std::result::Result::Err(dotenv_syntax_error(path, line_number, "single-quoted value is not terminated"));
|
||||
@@ -516,23 +538,6 @@ fn escape_json_pointer_token(value: &str) -> String {
|
||||
return value.replace('~', "~0").replace('/', "~1");
|
||||
}
|
||||
|
||||
pub(crate) fn validate_supported_variable_name(variable_name: &str) -> ksp_core_lib::Result<()> {
|
||||
if !has_supported_namespace(variable_name) {
|
||||
return std::result::Result::Err(invalid_variable_error(variable_name, "variable must use the KSP_ or KSPB_ namespace"));
|
||||
}
|
||||
let prefix_length = if variable_name.starts_with("KSPB_") { 5 } else { 4 };
|
||||
if variable_name.len() <= prefix_length {
|
||||
return std::result::Result::Err(invalid_variable_error(variable_name, "variable namespace must be followed by a name"));
|
||||
}
|
||||
for byte in variable_name.bytes() {
|
||||
let valid = byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_';
|
||||
if !valid {
|
||||
return std::result::Result::Err(invalid_variable_error(variable_name, "variable names use uppercase ASCII letters, digits and underscores"));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn has_supported_namespace(variable_name: &str) -> bool {
|
||||
return variable_name.starts_with("KSP_") || variable_name.starts_with("KSPB_");
|
||||
}
|
||||
|
||||
@@ -1,68 +1,47 @@
|
||||
// file: crates/ksp-config-lib/src/error.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
/// Error code used when a Config bootstrap argument is missing its value.
|
||||
pub const ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_argument_missing_value");
|
||||
|
||||
/// Error code used when a Config bootstrap path is empty, inaccessible, or resolves to an existing non-directory path.
|
||||
pub const ERROR_CODE_BOOTSTRAP_INVALID_PATH: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_invalid_path");
|
||||
|
||||
/// Error code used when a logical Config file identifier is malformed.
|
||||
pub const ERROR_CODE_FILE_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_invalid");
|
||||
|
||||
/// Error code used when a requested logical Config file identifier is not registered.
|
||||
pub const ERROR_CODE_FILE_ID_UNKNOWN: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_unknown");
|
||||
|
||||
/// Error code used when the same logical Config file identifier is registered more than once.
|
||||
pub const ERROR_CODE_FILE_ID_DUPLICATE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_duplicate");
|
||||
|
||||
/// Error code used when a Config filename mapping or descriptor relation is invalid.
|
||||
pub const ERROR_CODE_FILE_MAPPING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_mapping_invalid");
|
||||
|
||||
/// Error code used when a Config-managed JSON document or schema cannot be read from its resolved path.
|
||||
pub const ERROR_CODE_JSON_FILE_READ_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_file_read_failed");
|
||||
|
||||
/// Error code used when a Config-managed file contains invalid JSON syntax.
|
||||
pub const ERROR_CODE_JSON_SYNTAX_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_syntax_invalid");
|
||||
|
||||
/// Error code used when a JSON Schema document is itself invalid for the selected JSON Schema draft.
|
||||
pub const ERROR_CODE_SCHEMA_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_invalid");
|
||||
|
||||
/// Error code used when a Config document does not satisfy its registered JSON Schema.
|
||||
pub const ERROR_CODE_SCHEMA_VALIDATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_validation_failed");
|
||||
|
||||
/// Error code used when a schema-valid Config document violates KSP semantic invariants for its document type.
|
||||
pub const ERROR_CODE_DOCUMENT_SEMANTIC_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "document_semantic_invalid");
|
||||
|
||||
/// Error code used when an explicitly requested Config profile does not exist in a validated document.
|
||||
pub const ERROR_CODE_PROFILE_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "profile_not_found");
|
||||
|
||||
/// Error code used when a composite document references an invalid, unknown, or unsupported Config document.
|
||||
pub const ERROR_CODE_COMPOSITE_REFERENCE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "composite_reference_invalid");
|
||||
|
||||
/// Error code used when a schema-valid Config document violates KSP semantic invariants for its document type.
|
||||
pub const ERROR_CODE_DOCUMENT_SEMANTIC_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "document_semantic_invalid");
|
||||
/// Error code used when the local `.env` file cannot be read for a reason other than absence.
|
||||
pub const ERROR_CODE_DOTENV_FILE_READ_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "dotenv_file_read_failed");
|
||||
|
||||
/// Error code used when the local `.env` file contains syntax Config cannot interpret safely.
|
||||
pub const ERROR_CODE_DOTENV_SYNTAX_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "dotenv_syntax_invalid");
|
||||
|
||||
/// Error code used when a Config environment variable name is malformed or outside the KSP/KSPB namespaces.
|
||||
pub const ERROR_CODE_ENVIRONMENT_VARIABLE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_variable_invalid");
|
||||
|
||||
/// Error code used when a referenced Config environment variable is absent and has no fallback.
|
||||
pub const ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_variable_missing");
|
||||
|
||||
/// Error code used when a supported process environment variable has a value that cannot become a JSON UTF-8 string.
|
||||
pub const ERROR_CODE_ENVIRONMENT_VALUE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_value_invalid");
|
||||
|
||||
/// Error code used when a `${NAME}` / `${NAME:-fallback}` expression is malformed.
|
||||
pub const ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_placeholder_invalid");
|
||||
|
||||
/// Error code used when an environment-resolved Config cannot be mapped safely to a runtime consumer contract.
|
||||
pub const ERROR_CODE_EFFECTIVE_CONFIG_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "effective_config_invalid");
|
||||
|
||||
/// Error code used when a `${NAME}` / `${NAME:-fallback}` expression is malformed.
|
||||
pub const ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_placeholder_invalid");
|
||||
/// Error code used when a supported process environment variable has a value that cannot become a JSON UTF-8 string.
|
||||
pub const ERROR_CODE_ENVIRONMENT_VALUE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_value_invalid");
|
||||
/// Error code used when a Config environment variable name is malformed or outside the KSP/KSPB namespaces.
|
||||
pub const ERROR_CODE_ENVIRONMENT_VARIABLE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_variable_invalid");
|
||||
/// Error code used when a referenced Config environment variable is absent and has no fallback.
|
||||
pub const ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "environment_variable_missing");
|
||||
/// Error code used when the same logical Config file identifier is registered more than once.
|
||||
pub const ERROR_CODE_FILE_ID_DUPLICATE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_duplicate");
|
||||
/// Error code used when a logical Config file identifier is malformed.
|
||||
pub const ERROR_CODE_FILE_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_invalid");
|
||||
/// Error code used when a requested logical Config file identifier is not registered.
|
||||
pub const ERROR_CODE_FILE_ID_UNKNOWN: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_id_unknown");
|
||||
/// Error code used when a Config filename mapping or descriptor relation is invalid.
|
||||
pub const ERROR_CODE_FILE_MAPPING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "file_mapping_invalid");
|
||||
/// Error code used when a Config-managed JSON document or schema cannot be read from its resolved path.
|
||||
pub const ERROR_CODE_JSON_FILE_READ_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_file_read_failed");
|
||||
/// Error code used when a Config-managed file contains invalid JSON syntax.
|
||||
pub const ERROR_CODE_JSON_SYNTAX_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_syntax_invalid");
|
||||
/// Error code used when an explicit Config management operation is unsupported or targets the wrong managed resource kind.
|
||||
pub const ERROR_CODE_MANAGEMENT_OPERATION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "management_operation_invalid");
|
||||
|
||||
/// Error code used when an atomic managed Config or `.env` persistence operation fails before commit.
|
||||
pub const ERROR_CODE_PERSISTENCE_WRITE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "persistence_write_failed");
|
||||
/// Error code used when an explicitly requested Config profile does not exist in a validated document.
|
||||
pub const ERROR_CODE_PROFILE_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "profile_not_found");
|
||||
/// Error code used when a JSON Schema document is itself invalid for the selected JSON Schema draft.
|
||||
pub const ERROR_CODE_SCHEMA_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_invalid");
|
||||
/// Error code used when a Config document does not satisfy its registered JSON Schema.
|
||||
pub const ERROR_CODE_SCHEMA_VALIDATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "schema_validation_failed");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// file: crates/ksp-config-lib/src/lib.rs
|
||||
// version: 12
|
||||
// version: 14
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -25,8 +26,6 @@ mod registry;
|
||||
mod sensitivity;
|
||||
mod transport;
|
||||
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
|
||||
/// Bootstrap argument used to replace the configuration document root.
|
||||
pub use self::bootstrap::ARG_CFG_PATH;
|
||||
/// Bootstrap argument used to replace the schema root.
|
||||
@@ -171,3 +170,22 @@ pub use self::sensitivity::ResolvedConfigJson;
|
||||
pub use self::sensitivity::ResolvedConfigText;
|
||||
/// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`.
|
||||
pub use self::transport::ResolvedTransportConfig;
|
||||
|
||||
/// Validates composite document contract.
|
||||
pub(crate) use self::composite::validate_composite_document_contract;
|
||||
/// Owning tracing target for events emitted by the Config crate.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
/// Executes the crate-internal parse dotenv content operation for the owning module.
|
||||
pub(crate) use self::environment::parse_dotenv_content;
|
||||
/// Validates supported variable name.
|
||||
pub(crate) use self::environment::validate_supported_variable_name;
|
||||
/// Executes the crate-internal atomic write operation for the owning module.
|
||||
pub(crate) use self::persistence::atomic_write;
|
||||
/// Executes the crate-internal atomic write private operation for the owning module.
|
||||
pub(crate) use self::persistence::atomic_write_private;
|
||||
/// Loads resolved profile with source.
|
||||
pub(crate) use self::profile::load_resolved_profile_with_source;
|
||||
/// Validates document profile contract.
|
||||
pub(crate) use self::profile::validate_document_profile_contract;
|
||||
/// Executes the crate-internal build registry operation for the owning module.
|
||||
pub(crate) use self::registry::build_registry;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/management.rs
|
||||
// version: 4
|
||||
// version: 6
|
||||
|
||||
/// Raw source of one registered Config document read for explicit management/correction.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
@@ -727,7 +727,7 @@ impl ConfigManagement {
|
||||
/// Authentication/authorization of the human user belongs to the calling application. Calling this method is the explicit Config boundary that opts into
|
||||
/// real-value access; the returned value must never be logged.
|
||||
pub fn reveal_effective_environment_value(&self, variable_name: &str) -> ksp_core_lib::Result<std::option::Option<String>> {
|
||||
let validation = crate::environment::validate_supported_variable_name(variable_name);
|
||||
let validation = crate::validate_supported_variable_name(variable_name);
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
@@ -744,7 +744,7 @@ impl ConfigManagement {
|
||||
|
||||
/// Explicitly reveals the real persisted `.env` value for management display/editing.
|
||||
pub fn reveal_dotenv_value(&self, variable_name: &str) -> ksp_core_lib::Result<std::option::Option<String>> {
|
||||
let validation = crate::environment::validate_supported_variable_name(variable_name);
|
||||
let validation = crate::validate_supported_variable_name(variable_name);
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
@@ -758,7 +758,7 @@ impl ConfigManagement {
|
||||
|
||||
/// Creates or updates one supported KSP/KSPB `.env` entry atomically without mutating the inherited process environment.
|
||||
pub fn set_dotenv_value(&self, variable_name: &str, value: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeReport> {
|
||||
let validation = crate::environment::validate_supported_variable_name(variable_name);
|
||||
let validation = crate::validate_supported_variable_name(variable_name);
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
@@ -781,7 +781,7 @@ impl ConfigManagement {
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if candidate != content {
|
||||
let write = crate::persistence::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes());
|
||||
let write = crate::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes());
|
||||
match write {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -797,7 +797,7 @@ impl ConfigManagement {
|
||||
|
||||
/// Removes one supported KSP/KSPB `.env` entry atomically without mutating the inherited process environment.
|
||||
pub fn remove_dotenv_value(&self, variable_name: &str) -> ksp_core_lib::Result<ConfigEnvironmentChangeReport> {
|
||||
let validation = crate::environment::validate_supported_variable_name(variable_name);
|
||||
let validation = crate::validate_supported_variable_name(variable_name);
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
@@ -820,7 +820,7 @@ impl ConfigManagement {
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if candidate != content {
|
||||
let write = crate::persistence::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes());
|
||||
let write = crate::atomic_write_private(self.dotenv_path.as_path(), candidate.as_bytes());
|
||||
match write {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -838,6 +838,7 @@ impl ConfigManagement {
|
||||
return crate::ConfigEnvironment::load_from_dotenv_path(self.dotenv_path.as_path());
|
||||
}
|
||||
|
||||
/// Executes the crate-internal with dotenv path operation for `ConfigManagement`.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn with_dotenv_path(engine: crate::ConfigDocumentEngine, dotenv_path: std::path::PathBuf) -> Self {
|
||||
return Self { engine, dotenv_path };
|
||||
@@ -921,7 +922,7 @@ fn read_dotenv_source(path: &std::path::Path) -> ksp_core_lib::Result<String> {
|
||||
let content = std::fs::read_to_string(path);
|
||||
return match content {
|
||||
std::result::Result::Ok(value) => {
|
||||
let validation = crate::environment::parse_dotenv_content(path, value.as_str());
|
||||
let validation = crate::parse_dotenv_content(path, value.as_str());
|
||||
match validation {
|
||||
std::result::Result::Ok(_) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
@@ -937,7 +938,7 @@ fn read_dotenv_source(path: &std::path::Path) -> ksp_core_lib::Result<String> {
|
||||
}
|
||||
|
||||
fn update_dotenv_source(path: &std::path::Path, source: &str, variable_name: &str, replacement: std::option::Option<&str>) -> ksp_core_lib::Result<String> {
|
||||
let validation = crate::environment::parse_dotenv_content(path, source);
|
||||
let validation = crate::parse_dotenv_content(path, source);
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
@@ -967,7 +968,7 @@ fn update_dotenv_source(path: &std::path::Path, source: &str, variable_name: &st
|
||||
if !candidate.is_empty() {
|
||||
candidate.push('\n');
|
||||
}
|
||||
let candidate_validation = crate::environment::parse_dotenv_content(path, candidate.as_str());
|
||||
let candidate_validation = crate::parse_dotenv_content(path, candidate.as_str());
|
||||
return match candidate_validation {
|
||||
std::result::Result::Ok(_) => std::result::Result::Ok(candidate),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||
@@ -1030,7 +1031,7 @@ fn persist_document_source(file_id: &crate::ConfigFileId, path: &std::path::Path
|
||||
if existing.as_slice() == source {
|
||||
return std::result::Result::Ok(ConfigDocumentChangeReport { source_changed: false, reload_required: false });
|
||||
}
|
||||
let write = crate::persistence::atomic_write(path, source);
|
||||
let write = crate::atomic_write(path, source);
|
||||
if let std::result::Result::Err(error) = write {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// file: crates/ksp-config-lib/src/persistence.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
static NEXT_TEMPORARY_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
|
||||
/// Executes the crate-internal atomic write operation for the owning module.
|
||||
pub(crate) fn atomic_write(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
||||
return atomic_write_with_policy(path, content, false);
|
||||
}
|
||||
|
||||
/// Executes the crate-internal atomic write private operation for the owning module.
|
||||
pub(crate) fn atomic_write_private(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
||||
return atomic_write_with_policy(path, content, true);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/profile.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
/// Origin of one top-level value in a resolved standard Config profile.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -135,6 +135,7 @@ impl crate::ConfigDocumentEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads resolved profile with source.
|
||||
pub(crate) fn load_resolved_profile_with_source(
|
||||
engine: &crate::ConfigDocumentEngine,
|
||||
file_id: &crate::ConfigFileId,
|
||||
@@ -153,6 +154,7 @@ pub(crate) fn load_resolved_profile_with_source(
|
||||
return resolve_document_profile(&document, requested_profile, source);
|
||||
}
|
||||
|
||||
/// Validates document profile contract.
|
||||
pub(crate) fn validate_document_profile_contract(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> {
|
||||
let root = match document.value().as_object() {
|
||||
std::option::Option::Some(value) => value,
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
// file: crates/ksp-config-lib/src/registry.rs
|
||||
// version: 5
|
||||
// version: 7
|
||||
|
||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||
pub const ARG_FILE_MAP: &str = "--filemap";
|
||||
/// 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 Logging JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
|
||||
/// Logical file identifier for the standard HTTP Transport configuration document.
|
||||
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
|
||||
/// Logical file identifier for the standard HTTP Transport JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
|
||||
/// Logical file identifier for the generic composite JSON Schema document.
|
||||
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
|
||||
/// Default physical filename for the generic composite JSON Schema document.
|
||||
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
|
||||
/// Default physical filename for the standard Logging configuration document.
|
||||
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
|
||||
/// Default physical filename for the standard Logging JSON Schema document.
|
||||
@@ -21,8 +13,16 @@ pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
|
||||
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
|
||||
/// Default physical filename for the standard HTTP Transport JSON Schema document.
|
||||
pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json";
|
||||
/// Default physical filename for the generic composite JSON Schema document.
|
||||
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
|
||||
/// Logical file identifier for the generic composite JSON Schema document.
|
||||
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.
|
||||
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
|
||||
/// 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.
|
||||
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
|
||||
|
||||
/// Stable logical identifier for a Config-managed file.
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
@@ -89,6 +89,7 @@ impl ConfigFileDescriptor {
|
||||
return self.schema_file_id.as_ref();
|
||||
}
|
||||
|
||||
/// Creates a new `ConfigFileDescriptor` value.
|
||||
pub(crate) fn new(
|
||||
file_id: &'static str,
|
||||
kind: ConfigFileKind,
|
||||
@@ -165,7 +166,7 @@ impl ConfigFileRegistry {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return build_registry([logging, logging_schema, transport, transport_schema, composite_schema]);
|
||||
return crate::build_registry([logging, logging_schema, transport, transport_schema, composite_schema]);
|
||||
}
|
||||
|
||||
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.
|
||||
@@ -249,6 +250,7 @@ impl ConfigFileRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the crate-internal build registry operation for the owning module.
|
||||
pub(crate) fn build_registry<const N: usize>(descriptors: [ConfigFileDescriptor; N]) -> ksp_core_lib::Result<ConfigFileRegistry> {
|
||||
let mut registry = ConfigFileRegistry { descriptors: std::collections::BTreeMap::new() };
|
||||
for descriptor in descriptors {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/src/sensitivity.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Replacement used for secret environment fragments in safe diagnostic representations.
|
||||
pub const REDACTED_CONFIG_VALUE: &str = "********";
|
||||
@@ -18,7 +18,7 @@ pub enum ConfigSensitivity {
|
||||
impl ConfigSensitivity {
|
||||
/// Classifies one supported KSP/KSPB environment variable by its namespace.
|
||||
pub fn from_variable_name(variable_name: &str) -> ksp_core_lib::Result<Self> {
|
||||
let validation = crate::environment::validate_supported_variable_name(variable_name);
|
||||
let validation = crate::validate_supported_variable_name(variable_name);
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
@@ -103,6 +103,7 @@ pub struct ResolvedConfigText {
|
||||
}
|
||||
|
||||
impl ResolvedConfigText {
|
||||
/// Creates a new `ResolvedConfigText` value.
|
||||
pub(crate) fn new(value: String, safe_value: String, sensitivity: ConfigSensitivity, provenance: std::vec::Vec<ConfigValueProvenance>) -> Self {
|
||||
return Self { value, safe_value, sensitivity, provenance };
|
||||
}
|
||||
@@ -153,6 +154,7 @@ pub struct ResolvedConfigJson {
|
||||
}
|
||||
|
||||
impl ResolvedConfigJson {
|
||||
/// Creates a new `ResolvedConfigJson` value.
|
||||
pub(crate) fn new(
|
||||
value: serde_json::Value,
|
||||
safe_value: serde_json::Value,
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/bootstrap.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn defaults_use_hardcoded_ksp_roots() {
|
||||
let result = super::ConfigBootstrapOptions::defaults();
|
||||
let result = crate::ConfigBootstrapOptions::defaults();
|
||||
assert!(result.is_ok(), "default bootstrap paths should be valid: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH));
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new(crate::DEFAULT_CFG_PATH));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new(crate::DEFAULT_SCHEMA_PATH));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cfg_path_override_keeps_schema_default() {
|
||||
let defaults = super::ConfigBootstrapOptions::defaults();
|
||||
let defaults = crate::ConfigBootstrapOptions::defaults();
|
||||
assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}");
|
||||
if let std::result::Result::Ok(options) = defaults {
|
||||
let result = options.with_cfg_path("custom-config");
|
||||
assert!(result.is_ok(), "cfg path override should be valid: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new("custom-config"));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new(crate::DEFAULT_SCHEMA_PATH));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_path_override_keeps_cfg_default() {
|
||||
let defaults = super::ConfigBootstrapOptions::defaults();
|
||||
let defaults = crate::ConfigBootstrapOptions::defaults();
|
||||
assert!(defaults.is_ok(), "default bootstrap paths should be valid: {defaults:?}");
|
||||
if let std::result::Result::Ok(options) = defaults {
|
||||
let result = options.with_schema_path("custom-schemas");
|
||||
assert!(result.is_ok(), "schema path override should be valid: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH));
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new(crate::DEFAULT_CFG_PATH));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new("custom-schemas"));
|
||||
}
|
||||
}
|
||||
@@ -42,21 +42,21 @@ fn schema_path_override_keeps_cfg_default() {
|
||||
#[test]
|
||||
fn cfg_cli_override_keeps_schema_default() {
|
||||
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath=cli-config")];
|
||||
let result = super::ConfigBootstrapOptions::from_args(&args);
|
||||
let result = crate::ConfigBootstrapOptions::from_args(&args);
|
||||
assert!(result.is_ok(), "cfg CLI override should parse: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new("cli-config"));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new(super::DEFAULT_SCHEMA_PATH));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new(crate::DEFAULT_SCHEMA_PATH));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_cli_override_keeps_cfg_default() {
|
||||
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=cli-schemas")];
|
||||
let result = super::ConfigBootstrapOptions::from_args(&args);
|
||||
let result = crate::ConfigBootstrapOptions::from_args(&args);
|
||||
assert!(result.is_ok(), "schema CLI override should parse: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new(super::DEFAULT_CFG_PATH));
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new(crate::DEFAULT_CFG_PATH));
|
||||
assert_eq!(options.schema_path(), std::path::Path::new("cli-schemas"));
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ fn parser_accepts_inline_and_separate_forms_and_last_value_wins() {
|
||||
std::ffi::OsString::from("--schemapath"),
|
||||
std::ffi::OsString::from("second-schemas"),
|
||||
];
|
||||
let result = super::ConfigBootstrapOptions::from_args(&args);
|
||||
let result = crate::ConfigBootstrapOptions::from_args(&args);
|
||||
assert!(result.is_ok(), "bootstrap arguments should parse: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new("second-config"));
|
||||
@@ -84,7 +84,7 @@ fn parser_accepts_inline_and_separate_forms_and_last_value_wins() {
|
||||
#[test]
|
||||
fn parser_reports_missing_separate_value() {
|
||||
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath")];
|
||||
let result = super::ConfigBootstrapOptions::from_args(&args);
|
||||
let result = crate::ConfigBootstrapOptions::from_args(&args);
|
||||
assert!(result.is_err(), "missing value must be rejected");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE);
|
||||
@@ -94,7 +94,7 @@ fn parser_reports_missing_separate_value() {
|
||||
#[test]
|
||||
fn parser_reports_another_option_as_missing_separate_value() {
|
||||
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--cfgpath"), std::ffi::OsString::from("--other-option")];
|
||||
let result = super::ConfigBootstrapOptions::from_args(&args);
|
||||
let result = crate::ConfigBootstrapOptions::from_args(&args);
|
||||
assert!(result.is_err(), "another option must not become a bootstrap path value");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE);
|
||||
@@ -104,7 +104,7 @@ fn parser_reports_another_option_as_missing_separate_value() {
|
||||
#[test]
|
||||
fn empty_inline_path_is_rejected() {
|
||||
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--schemapath=")];
|
||||
let result = super::ConfigBootstrapOptions::from_args(&args);
|
||||
let result = crate::ConfigBootstrapOptions::from_args(&args);
|
||||
assert!(result.is_err(), "empty path must be rejected");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_BOOTSTRAP_INVALID_PATH);
|
||||
@@ -113,7 +113,7 @@ fn empty_inline_path_is_rejected() {
|
||||
|
||||
#[test]
|
||||
fn explicit_programmatic_paths_do_not_depend_on_default_roots() {
|
||||
let result = super::ConfigBootstrapOptions::from_paths("programmatic-config", "programmatic-schemas");
|
||||
let result = crate::ConfigBootstrapOptions::from_paths("programmatic-config", "programmatic-schemas");
|
||||
assert!(result.is_ok(), "explicit programmatic paths should be valid: {result:?}");
|
||||
if let std::result::Result::Ok(options) = result {
|
||||
assert_eq!(options.cfg_path(), std::path::Path::new("programmatic-config"));
|
||||
@@ -126,7 +126,7 @@ fn existing_non_directory_path_is_rejected() {
|
||||
let fixture = unique_fixture_path("existing-file");
|
||||
let create = std::fs::write(fixture.as_path(), b"fixture");
|
||||
assert!(create.is_ok(), "fixture file should be creatable: {create:?}");
|
||||
let result = super::ConfigBootstrapOptions::from_paths(fixture.as_path(), "programmatic-schemas");
|
||||
let result = crate::ConfigBootstrapOptions::from_paths(fixture.as_path(), "programmatic-schemas");
|
||||
let remove = std::fs::remove_file(fixture.as_path());
|
||||
assert!(remove.is_ok(), "fixture file should be removable: {remove:?}");
|
||||
assert!(result.is_err(), "existing file must not be accepted as a bootstrap directory");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/composite.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
const TEST_COMPOSITE_FILE_ID: &str = "cfg.composite.test";
|
||||
const TEST_COMPOSITE_FILENAME: &str = "examples/composite.example.json";
|
||||
const TEST_COMPOSITE_FILE_ID: &str = "cfg.composite.test";
|
||||
|
||||
#[test]
|
||||
fn fixture_composite_example_resolves_default_document_profile() {
|
||||
@@ -197,7 +197,7 @@ fn test_registry(composite_filename: &'static str) -> ksp_core_lib::Result<crate
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::registry::build_registry([logging, logging_schema, composite_schema, composite]);
|
||||
return crate::build_registry([logging, logging_schema, composite_schema, composite]);
|
||||
}
|
||||
|
||||
fn prepare_fixture(fixture: &FixtureRoots, composite: &str) -> std::io::Result<()> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/document.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
#[test]
|
||||
fn committed_logging_document_passes_registered_schema_and_semantic_validation() {
|
||||
@@ -11,7 +11,7 @@ fn committed_logging_document_passes_registered_schema_and_semantic_validation()
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}");
|
||||
if let (std::result::Result::Ok(bootstrap), std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (bootstrap, registry, file_id) {
|
||||
let engine = super::ConfigDocumentEngine::new(bootstrap, registry);
|
||||
let engine = crate::ConfigDocumentEngine::new(bootstrap, registry);
|
||||
let document = engine.load_validated_document(&file_id);
|
||||
assert!(document.is_ok(), "committed std.logging.json should validate: {document:?}");
|
||||
if let std::result::Result::Ok(document) = document {
|
||||
@@ -228,7 +228,7 @@ fn valid_logging_profile(profile_id: &str, output_id: &str) -> String {
|
||||
);
|
||||
}
|
||||
|
||||
fn load_fixture(fixture: &FixtureRoots) -> ksp_core_lib::Result<super::ConfigJsonDocument> {
|
||||
fn load_fixture(fixture: &FixtureRoots) -> ksp_core_lib::Result<crate::ConfigJsonDocument> {
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture.config.as_path(), fixture.schemas.as_path());
|
||||
let bootstrap = match bootstrap {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -244,7 +244,7 @@ fn load_fixture(fixture: &FixtureRoots) -> ksp_core_lib::Result<super::ConfigJso
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let engine = super::ConfigDocumentEngine::new(bootstrap, registry);
|
||||
let engine = crate::ConfigDocumentEngine::new(bootstrap, registry);
|
||||
return engine.load_validated_document(&file_id);
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ fn valid_minimal_logging_schema() -> &'static str {
|
||||
}"#;
|
||||
}
|
||||
|
||||
fn assert_error_code(result: ksp_core_lib::Result<super::ConfigJsonDocument>, expected: ksp_core_lib::ErrorCode) {
|
||||
fn assert_error_code(result: ksp_core_lib::Result<crate::ConfigJsonDocument>, expected: ksp_core_lib::ErrorCode) {
|
||||
assert!(result.is_err(), "fixture should fail with {expected:?}: {result:?}");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), expected);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/environment.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
#[test]
|
||||
fn process_environment_wins_over_dotenv_and_fallback_even_when_empty() {
|
||||
@@ -7,7 +7,7 @@ fn process_environment_wins_over_dotenv_and_fallback_even_when_empty() {
|
||||
process.insert("KSP_LOGS_DIRECTORY".to_owned(), String::new());
|
||||
let mut dotenv = std::collections::BTreeMap::<String, String>::new();
|
||||
dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "dotenv-logs".to_owned());
|
||||
let environment = super::ConfigEnvironment::from_maps(process, dotenv);
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, dotenv);
|
||||
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs"));
|
||||
assert!(resolved.is_ok(), "process value should resolve");
|
||||
let resolved = match resolved {
|
||||
@@ -15,7 +15,7 @@ fn process_environment_wins_over_dotenv_and_fallback_even_when_empty() {
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(resolved.value(), "");
|
||||
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::Process);
|
||||
assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::Process);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -23,7 +23,7 @@ fn dotenv_wins_over_fallback_when_process_value_is_absent() {
|
||||
let process = std::collections::BTreeMap::<String, String>::new();
|
||||
let mut dotenv = std::collections::BTreeMap::<String, String>::new();
|
||||
dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "dotenv-logs".to_owned());
|
||||
let environment = super::ConfigEnvironment::from_maps(process, dotenv);
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, dotenv);
|
||||
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs"));
|
||||
assert!(resolved.is_ok(), "dotenv value should resolve");
|
||||
let resolved = match resolved {
|
||||
@@ -31,25 +31,25 @@ fn dotenv_wins_over_fallback_when_process_value_is_absent() {
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(resolved.value(), "dotenv-logs");
|
||||
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::DotEnv);
|
||||
assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::DotEnv);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_dotenv_value_is_defined_and_beats_fallback() {
|
||||
let mut dotenv = std::collections::BTreeMap::<String, String>::new();
|
||||
dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), String::new());
|
||||
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv);
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv);
|
||||
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs"));
|
||||
assert!(resolved.is_ok(), "empty dotenv value should resolve");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.value(), "");
|
||||
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::DotEnv);
|
||||
assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::DotEnv);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_is_used_only_when_external_sources_are_absent() {
|
||||
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::Some("fallback-logs"));
|
||||
assert!(resolved.is_ok(), "fallback should resolve");
|
||||
let resolved = match resolved {
|
||||
@@ -57,12 +57,12 @@ fn fallback_is_used_only_when_external_sources_are_absent() {
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert_eq!(resolved.value(), "fallback-logs");
|
||||
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::Fallback);
|
||||
assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::Fallback);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_variable_without_fallback_is_a_distinct_error() {
|
||||
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let resolved = environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::None);
|
||||
let error = match resolved {
|
||||
std::result::Result::Ok(_) => return,
|
||||
@@ -78,7 +78,7 @@ fn ksp_and_kspb_namespaces_are_supported_but_external_names_are_rejected() {
|
||||
process.insert("KSP_LOGS_DIRECTORY".to_owned(), "logs".to_owned());
|
||||
let bot_variable = ["KSPB_", "TEST_KEY"].concat();
|
||||
process.insert(bot_variable.clone(), "hidden".to_owned());
|
||||
let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
assert!(environment.resolve_variable("KSP_LOGS_DIRECTORY", std::option::Option::None).is_ok());
|
||||
assert!(environment.resolve_variable(bot_variable.as_str(), std::option::Option::None).is_ok());
|
||||
let external = environment.resolve_variable("OTHER_NETWORK", std::option::Option::None);
|
||||
@@ -91,7 +91,7 @@ fn ksp_and_kspb_namespaces_are_supported_but_external_names_are_rejected() {
|
||||
|
||||
#[test]
|
||||
fn text_resolver_supports_multiple_placeholders_and_literal_fallbacks() {
|
||||
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let resolved = environment.resolve_text("logs=${KSP_LOGS_DIRECTORY:-logs};second=${KSP_LOGS_DIRECTORY:-other}");
|
||||
assert!(resolved.is_ok(), "multiple placeholders should resolve");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
@@ -101,7 +101,7 @@ fn text_resolver_supports_multiple_placeholders_and_literal_fallbacks() {
|
||||
|
||||
#[test]
|
||||
fn malformed_or_nested_placeholders_are_rejected() {
|
||||
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let unclosed = environment.resolve_text("${KSP_LOGS_DIRECTORY");
|
||||
let unclosed = match unclosed {
|
||||
std::result::Result::Ok(_) => return,
|
||||
@@ -120,7 +120,7 @@ fn malformed_or_nested_placeholders_are_rejected() {
|
||||
fn json_resolver_walks_objects_and_arrays_without_changing_keys() {
|
||||
let mut dotenv = std::collections::BTreeMap::<String, String>::new();
|
||||
dotenv.insert("KSP_LOGS_DIRECTORY".to_owned(), "runtime-logs".to_owned());
|
||||
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv);
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv);
|
||||
let source = serde_json::json!({"path": "${KSP_LOGS_DIRECTORY}", "items": [1, "${KSP_LOGS_DIRECTORY}"], "enabled": true});
|
||||
let resolved = environment.resolve_json(&source);
|
||||
assert!(resolved.is_ok(), "recursive JSON resolution should succeed");
|
||||
@@ -137,7 +137,7 @@ fn json_resolver_walks_objects_and_arrays_without_changing_keys() {
|
||||
fn dotenv_parser_supports_comments_export_quotes_empty_values_and_ignores_external_keys() {
|
||||
let path = std::path::Path::new("fixture.env");
|
||||
let content = "# comment\nexport KSP_LOGS_DIRECTORY = 'quoted logs'\nOTHER_TOOL=value\n";
|
||||
let parsed = super::parse_dotenv_content(path, content);
|
||||
let parsed = crate::parse_dotenv_content(path, content);
|
||||
assert!(parsed.is_ok(), "dotenv fixture should parse");
|
||||
let parsed = match parsed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -149,7 +149,7 @@ fn dotenv_parser_supports_comments_export_quotes_empty_values_and_ignores_extern
|
||||
|
||||
#[test]
|
||||
fn dotenv_duplicate_ksp_key_is_rejected() {
|
||||
let parsed = super::parse_dotenv_content(std::path::Path::new("fixture.env"), "KSP_LOGS_DIRECTORY=one\nKSP_LOGS_DIRECTORY=two\n");
|
||||
let parsed = crate::parse_dotenv_content(std::path::Path::new("fixture.env"), "KSP_LOGS_DIRECTORY=one\nKSP_LOGS_DIRECTORY=two\n");
|
||||
let error = match parsed {
|
||||
std::result::Result::Ok(_) => return,
|
||||
std::result::Result::Err(error) => error,
|
||||
@@ -199,7 +199,7 @@ fn logging_fixture_profile_resolves_environment_fallback_without_changing_source
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let effective = profile.resolve_effective_environment(&environment);
|
||||
assert!(effective.is_ok(), "committed Logging environment fallback should resolve");
|
||||
let effective = match effective {
|
||||
@@ -245,7 +245,7 @@ fn secret_environment_value_keeps_real_value_but_redacts_safe_and_debug_views()
|
||||
let canary = "KSP_SECRET_CANARY_91b7c6";
|
||||
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||
process.insert("KSP_SECRET_TEST_TOKEN".to_owned(), canary.to_owned());
|
||||
let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
let resolved = environment.resolve_variable("KSP_SECRET_TEST_TOKEN", std::option::Option::None);
|
||||
assert!(resolved.is_ok(), "secret process value should resolve");
|
||||
let resolved = match resolved {
|
||||
@@ -255,7 +255,7 @@ fn secret_environment_value_keeps_real_value_but_redacts_safe_and_debug_views()
|
||||
assert_eq!(resolved.value(), canary);
|
||||
assert_eq!(resolved.safe_value(), crate::REDACTED_CONFIG_VALUE);
|
||||
assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret);
|
||||
assert_eq!(resolved.source(), super::ConfigEnvironmentSource::Process);
|
||||
assert_eq!(resolved.source(), crate::ConfigEnvironmentSource::Process);
|
||||
let debug = format!("{resolved:?}");
|
||||
assert!(!debug.contains(canary), "Debug must not reveal the secret canary");
|
||||
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
|
||||
@@ -267,7 +267,7 @@ fn detailed_text_redacts_only_secret_segments_and_keeps_ordered_provenance() {
|
||||
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||
process.insert("KSP_PUBLIC_HOST".to_owned(), "rpc.example.test".to_owned());
|
||||
process.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned());
|
||||
let environment = super::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||
let resolved = environment.resolve_text_detailed("https://${KSP_PUBLIC_HOST}/?token=${KSP_SECRET_TOKEN}");
|
||||
assert!(resolved.is_ok(), "composed secret URL should resolve");
|
||||
let resolved = match resolved {
|
||||
@@ -288,7 +288,7 @@ fn detailed_text_redacts_only_secret_segments_and_keeps_ordered_provenance() {
|
||||
|
||||
#[test]
|
||||
fn secret_fallback_inherits_secret_sensitivity_and_is_redacted() {
|
||||
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let resolved = environment.resolve_text_detailed("token=${KSP_SECRET_TOKEN:-false-secret}");
|
||||
assert!(resolved.is_ok(), "secret fallback should resolve");
|
||||
let resolved = match resolved {
|
||||
@@ -298,7 +298,7 @@ fn secret_fallback_inherits_secret_sensitivity_and_is_redacted() {
|
||||
assert_eq!(resolved.value(), "token=false-secret");
|
||||
assert_eq!(resolved.safe_value(), "token=********");
|
||||
assert_eq!(resolved.sensitivity(), crate::ConfigSensitivity::Secret);
|
||||
assert_eq!(resolved.provenance()[1].environment_source(), std::option::Option::Some(super::ConfigEnvironmentSource::Fallback));
|
||||
assert_eq!(resolved.provenance()[1].environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Fallback));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -306,7 +306,7 @@ fn detailed_json_preserves_safe_tree_sensitivity_and_pointer_provenance() {
|
||||
let secret = "nested-secret-canary-2d11";
|
||||
let mut dotenv = std::collections::BTreeMap::<String, String>::new();
|
||||
dotenv.insert("KSP_SECRET_TOKEN".to_owned(), secret.to_owned());
|
||||
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv);
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), dotenv);
|
||||
let source = serde_json::json!({"transport": {"url": "https://host/?token=${KSP_SECRET_TOKEN}"}, "items": ["plain", 7]});
|
||||
let resolved = environment.resolve_json_detailed(&source);
|
||||
assert!(resolved.is_ok(), "detailed JSON should resolve");
|
||||
@@ -349,7 +349,7 @@ fn detailed_fixture_profile_environment_keeps_global_origin_and_adds_environment
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let environment = super::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||
let effective = profile.resolve_effective_environment_detailed(&environment);
|
||||
assert!(effective.is_ok(), "detailed committed Logging profile should resolve");
|
||||
let effective = match effective {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/profile.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn fixture_default_profile_resolves_globals_profile_and_provenance() {
|
||||
@@ -15,9 +15,9 @@ fn fixture_default_profile_resolves_globals_profile_and_provenance() {
|
||||
if let (std::result::Result::Ok(document), std::result::Result::Ok(resolved)) = (document, resolved) {
|
||||
let default_profile = document.value().get("default_profile").and_then(serde_json::Value::as_str);
|
||||
assert_eq!(default_profile, std::option::Option::Some(resolved.profile_id()));
|
||||
assert_eq!(resolved.selection_source(), super::ConfigProfileSelectionSource::DefaultProfile);
|
||||
assert_eq!(resolved.origin("logs_directory"), std::option::Option::Some(super::ConfigValueOrigin::Global));
|
||||
assert_eq!(resolved.origin("default_filter"), std::option::Option::Some(super::ConfigValueOrigin::Profile));
|
||||
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
|
||||
assert_eq!(resolved.origin("logs_directory"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
||||
assert_eq!(resolved.origin("default_filter"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
|
||||
assert_eq!(
|
||||
resolved.profile().get("default_filter").and_then(serde_json::Value::as_str),
|
||||
resolved.effective().get("default_filter").and_then(serde_json::Value::as_str),
|
||||
@@ -45,7 +45,7 @@ fn explicit_profile_selection_is_distinct_from_default_selection() {
|
||||
assert!(resolved.is_ok(), "explicit committed profile should resolve: {resolved:?}");
|
||||
if let std::result::Result::Ok(resolved) = resolved {
|
||||
assert_eq!(resolved.profile_id(), profile_id);
|
||||
assert_eq!(resolved.selection_source(), super::ConfigProfileSelectionSource::Explicit);
|
||||
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/registry.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
#[test]
|
||||
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
||||
let registry = super::ConfigFileRegistry::defaults();
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let descriptors: std::vec::Vec<&super::ConfigFileDescriptor> = registry.descriptors().collect();
|
||||
let descriptors: std::vec::Vec<&crate::ConfigFileDescriptor> = registry.descriptors().collect();
|
||||
assert_eq!(descriptors.len(), 5);
|
||||
assert_eq!(descriptors[0].file_id().as_str(), super::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[0].kind(), super::ConfigFileKind::Config);
|
||||
assert_eq!(descriptors[0].filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_FILENAME));
|
||||
assert_eq!(descriptors[0].file_id().as_str(), crate::FILE_ID_STD_LOGGING);
|
||||
assert_eq!(descriptors[0].kind(), crate::ConfigFileKind::Config);
|
||||
assert_eq!(descriptors[0].filename(), std::path::Path::new(crate::DEFAULT_STD_LOGGING_FILENAME));
|
||||
let logging_schema_file_id = descriptors[0].schema_file_id();
|
||||
assert!(logging_schema_file_id.is_some(), "logging descriptor should expose its validation schema");
|
||||
if let std::option::Option::Some(schema_file_id) = logging_schema_file_id {
|
||||
assert_eq!(schema_file_id.as_str(), super::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(schema_file_id.as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
}
|
||||
assert_eq!(descriptors[1].file_id().as_str(), super::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[1].kind(), super::ConfigFileKind::Config);
|
||||
assert_eq!(descriptors[1].filename(), std::path::Path::new(super::DEFAULT_STD_TRANSPORT_FILENAME));
|
||||
assert_eq!(descriptors[1].file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[1].kind(), crate::ConfigFileKind::Config);
|
||||
assert_eq!(descriptors[1].filename(), std::path::Path::new(crate::DEFAULT_STD_TRANSPORT_FILENAME));
|
||||
let transport_schema_file_id = descriptors[1].schema_file_id();
|
||||
assert!(transport_schema_file_id.is_some(), "transport descriptor should expose its validation schema");
|
||||
if let std::option::Option::Some(schema_file_id) = transport_schema_file_id {
|
||||
assert_eq!(schema_file_id.as_str(), super::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(schema_file_id.as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
}
|
||||
assert_eq!(descriptors[2].file_id().as_str(), super::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[2].kind(), super::ConfigFileKind::Schema);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), super::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[3].kind(), super::ConfigFileKind::Schema);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), super::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[4].kind(), super::ConfigFileKind::Schema);
|
||||
assert_eq!(descriptors[2].file_id().as_str(), crate::FILE_ID_SCHEMA_COMPOSITE);
|
||||
assert_eq!(descriptors[2].kind(), crate::ConfigFileKind::Schema);
|
||||
assert_eq!(descriptors[3].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(descriptors[3].kind(), crate::ConfigFileKind::Schema);
|
||||
assert_eq!(descriptors[4].file_id().as_str(), crate::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert_eq!(descriptors[4].kind(), crate::ConfigFileKind::Schema);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descriptors_reflect_filename_overrides_without_changing_logical_metadata() {
|
||||
let registry = super::ConfigFileRegistry::defaults();
|
||||
let file_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING);
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}");
|
||||
if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) {
|
||||
let overridden = registry.with_filename_override(&file_id, "profiles/desktop.logging.json");
|
||||
assert!(overridden.is_ok(), "filename override should remain valid: {overridden:?}");
|
||||
if let std::result::Result::Ok(overridden) = overridden {
|
||||
let mut found: std::option::Option<&super::ConfigFileDescriptor> = std::option::Option::None;
|
||||
let mut found: std::option::Option<&crate::ConfigFileDescriptor> = std::option::Option::None;
|
||||
for descriptor in overridden.descriptors() {
|
||||
if descriptor.file_id() == &file_id {
|
||||
found = std::option::Option::Some(descriptor);
|
||||
@@ -52,12 +52,12 @@ fn descriptors_reflect_filename_overrides_without_changing_logical_metadata() {
|
||||
assert!(found.is_some(), "public descriptor inventory should retain the overridden logging descriptor");
|
||||
if let std::option::Option::Some(descriptor) = found {
|
||||
assert_eq!(descriptor.file_id(), &file_id);
|
||||
assert_eq!(descriptor.kind(), super::ConfigFileKind::Config);
|
||||
assert_eq!(descriptor.kind(), crate::ConfigFileKind::Config);
|
||||
assert_eq!(descriptor.filename(), std::path::Path::new("profiles/desktop.logging.json"));
|
||||
let schema_file_id = descriptor.schema_file_id();
|
||||
assert!(schema_file_id.is_some(), "filename override should preserve schema association");
|
||||
if let std::option::Option::Some(schema_file_id) = schema_file_id {
|
||||
assert_eq!(schema_file_id.as_str(), super::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert_eq!(schema_file_id.as_str(), crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,11 +66,11 @@ fn descriptors_reflect_filename_overrides_without_changing_logical_metadata() {
|
||||
|
||||
#[test]
|
||||
fn defaults_register_logging_document_and_schema_with_distinct_roots() {
|
||||
let registry = super::ConfigFileRegistry::defaults();
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let logging_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING);
|
||||
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
let logging_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
||||
let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert!(logging_id.is_ok(), "logging file_id should be valid: {logging_id:?}");
|
||||
assert!(schema_id.is_ok(), "logging schema file_id should be valid: {schema_id:?}");
|
||||
if let (std::result::Result::Ok(logging_id), std::result::Result::Ok(schema_id)) = (logging_id, schema_id) {
|
||||
@@ -79,15 +79,15 @@ fn defaults_register_logging_document_and_schema_with_distinct_roots() {
|
||||
assert!(logging.is_ok(), "logging descriptor should exist: {logging:?}");
|
||||
assert!(schema.is_ok(), "logging schema descriptor should exist: {schema:?}");
|
||||
if let (std::result::Result::Ok(logging), std::result::Result::Ok(schema)) = (logging, schema) {
|
||||
assert_eq!(logging.kind(), super::ConfigFileKind::Config);
|
||||
assert_eq!(logging.filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_FILENAME));
|
||||
assert_eq!(logging.kind(), crate::ConfigFileKind::Config);
|
||||
assert_eq!(logging.filename(), std::path::Path::new(crate::DEFAULT_STD_LOGGING_FILENAME));
|
||||
let logging_schema = logging.schema_file_id();
|
||||
assert!(logging_schema.is_some(), "logging document should declare its validation schema");
|
||||
if let std::option::Option::Some(logging_schema) = logging_schema {
|
||||
assert_eq!(logging_schema, &schema_id);
|
||||
}
|
||||
assert_eq!(schema.kind(), super::ConfigFileKind::Schema);
|
||||
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_SCHEMA_FILENAME));
|
||||
assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
|
||||
assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_STD_LOGGING_SCHEMA_FILENAME));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,11 +95,11 @@ fn defaults_register_logging_document_and_schema_with_distinct_roots() {
|
||||
|
||||
#[test]
|
||||
fn defaults_register_transport_document_and_schema_with_distinct_roots() {
|
||||
let registry = super::ConfigFileRegistry::defaults();
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let transport_id = super::ConfigFileId::new(super::FILE_ID_STD_TRANSPORT);
|
||||
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
let transport_id = crate::ConfigFileId::new(crate::FILE_ID_STD_TRANSPORT);
|
||||
let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||
assert!(transport_id.is_ok(), "transport file_id should be valid: {transport_id:?}");
|
||||
assert!(schema_id.is_ok(), "transport schema file_id should be valid: {schema_id:?}");
|
||||
if let (std::result::Result::Ok(transport_id), std::result::Result::Ok(schema_id)) = (transport_id, schema_id) {
|
||||
@@ -108,11 +108,11 @@ fn defaults_register_transport_document_and_schema_with_distinct_roots() {
|
||||
assert!(transport.is_ok(), "transport descriptor should exist: {transport:?}");
|
||||
assert!(schema.is_ok(), "transport schema descriptor should exist: {schema:?}");
|
||||
if let (std::result::Result::Ok(transport), std::result::Result::Ok(schema)) = (transport, schema) {
|
||||
assert_eq!(transport.kind(), super::ConfigFileKind::Config);
|
||||
assert_eq!(transport.filename(), std::path::Path::new(super::DEFAULT_STD_TRANSPORT_FILENAME));
|
||||
assert_eq!(transport.kind(), crate::ConfigFileKind::Config);
|
||||
assert_eq!(transport.filename(), std::path::Path::new(crate::DEFAULT_STD_TRANSPORT_FILENAME));
|
||||
assert_eq!(transport.schema_file_id(), std::option::Option::Some(&schema_id));
|
||||
assert_eq!(schema.kind(), super::ConfigFileKind::Schema);
|
||||
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME));
|
||||
assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
|
||||
assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,13 +120,13 @@ fn defaults_register_transport_document_and_schema_with_distinct_roots() {
|
||||
|
||||
#[test]
|
||||
fn resolve_path_uses_descriptor_kind_to_select_bootstrap_root() {
|
||||
let registry = super::ConfigFileRegistry::defaults();
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let bootstrap = crate::ConfigBootstrapOptions::from_paths("runtime-config", "runtime-schemas");
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
assert!(bootstrap.is_ok(), "bootstrap paths should be valid: {bootstrap:?}");
|
||||
if let (std::result::Result::Ok(registry), std::result::Result::Ok(bootstrap)) = (registry, bootstrap) {
|
||||
let logging_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING);
|
||||
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
let logging_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
||||
let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
if let (std::result::Result::Ok(logging_id), std::result::Result::Ok(schema_id)) = (logging_id, schema_id) {
|
||||
let logging = registry.resolve_path(&bootstrap, &logging_id);
|
||||
let schema = registry.resolve_path(&bootstrap, &schema_id);
|
||||
@@ -150,16 +150,16 @@ fn cli_filemap_override_replaces_filename_and_last_value_wins() {
|
||||
std::ffi::OsString::from("--other-option"),
|
||||
std::ffi::OsString::from("--filemap=cfg.std.logging=profiles/custom.logging.json"),
|
||||
];
|
||||
let registry = super::ConfigFileRegistry::from_args(&args);
|
||||
let registry = crate::ConfigFileRegistry::from_args(&args);
|
||||
assert!(registry.is_ok(), "filemap overrides should parse: {registry:?}");
|
||||
if let std::result::Result::Ok(registry) = registry {
|
||||
let file_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING);
|
||||
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
||||
if let std::result::Result::Ok(file_id) = file_id {
|
||||
let descriptor = registry.descriptor(&file_id);
|
||||
assert!(descriptor.is_ok(), "logging descriptor should remain registered: {descriptor:?}");
|
||||
if let std::result::Result::Ok(descriptor) = descriptor {
|
||||
assert_eq!(descriptor.filename(), std::path::Path::new("profiles/custom.logging.json"));
|
||||
assert_eq!(descriptor.kind(), super::ConfigFileKind::Config);
|
||||
assert_eq!(descriptor.kind(), crate::ConfigFileKind::Config);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,8 +167,8 @@ fn cli_filemap_override_replaces_filename_and_last_value_wins() {
|
||||
|
||||
#[test]
|
||||
fn programmatic_override_preserves_file_id_and_kind() {
|
||||
let registry = super::ConfigFileRegistry::defaults();
|
||||
let file_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let file_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_STD_LOGGING);
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
assert!(file_id.is_ok(), "schema file_id should be valid: {file_id:?}");
|
||||
if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) {
|
||||
@@ -178,7 +178,7 @@ fn programmatic_override_preserves_file_id_and_kind() {
|
||||
let descriptor = overridden.descriptor(&file_id);
|
||||
if let std::result::Result::Ok(descriptor) = descriptor {
|
||||
assert_eq!(descriptor.file_id(), &file_id);
|
||||
assert_eq!(descriptor.kind(), super::ConfigFileKind::Schema);
|
||||
assert_eq!(descriptor.kind(), crate::ConfigFileKind::Schema);
|
||||
assert_eq!(descriptor.filename(), std::path::Path::new("alternate/logging.schema.json"));
|
||||
}
|
||||
}
|
||||
@@ -188,7 +188,7 @@ fn programmatic_override_preserves_file_id_and_kind() {
|
||||
#[test]
|
||||
fn unknown_file_id_override_is_rejected() {
|
||||
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from("--filemap=cfg.unknown=unknown.json")];
|
||||
let result = super::ConfigFileRegistry::from_args(&args);
|
||||
let result = crate::ConfigFileRegistry::from_args(&args);
|
||||
assert!(result.is_err(), "unknown logical files must not be introduced by CLI override");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN);
|
||||
@@ -198,7 +198,7 @@ fn unknown_file_id_override_is_rejected() {
|
||||
#[test]
|
||||
fn invalid_file_ids_are_rejected() {
|
||||
for value in ["", ".cfg", "cfg.", "cfg..logging", "CFG.logging", "cfg/logging"] {
|
||||
let result = super::ConfigFileId::new(value);
|
||||
let result = crate::ConfigFileId::new(value);
|
||||
assert!(result.is_err(), "invalid file_id must be rejected: {value}");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_INVALID);
|
||||
@@ -208,8 +208,8 @@ fn invalid_file_ids_are_rejected() {
|
||||
|
||||
#[test]
|
||||
fn absolute_and_traversing_filenames_are_rejected() {
|
||||
let registry = super::ConfigFileRegistry::defaults();
|
||||
let file_id = super::ConfigFileId::new(super::FILE_ID_STD_LOGGING);
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_LOGGING);
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
assert!(file_id.is_ok(), "logging file_id should be valid: {file_id:?}");
|
||||
if let (std::result::Result::Ok(registry), std::result::Result::Ok(file_id)) = (registry, file_id) {
|
||||
@@ -226,7 +226,7 @@ fn absolute_and_traversing_filenames_are_rejected() {
|
||||
fn malformed_filemap_arguments_are_rejected() {
|
||||
for argument in ["--filemap", "--filemap=cfg.std.logging", "--filemap==logging.json", "--filemap=cfg.std.logging="] {
|
||||
let args = [std::ffi::OsString::from("ksp-app"), std::ffi::OsString::from(argument)];
|
||||
let result = super::ConfigFileRegistry::from_args(&args);
|
||||
let result = crate::ConfigFileRegistry::from_args(&args);
|
||||
assert!(result.is_err(), "malformed filemap argument must be rejected: {argument}");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID);
|
||||
@@ -236,12 +236,12 @@ fn malformed_filemap_arguments_are_rejected() {
|
||||
|
||||
#[test]
|
||||
fn duplicate_registry_ids_are_rejected() {
|
||||
let first = super::ConfigFileDescriptor::new("cfg.duplicate", super::ConfigFileKind::Config, "first.json", std::option::Option::None);
|
||||
let second = super::ConfigFileDescriptor::new("cfg.duplicate", super::ConfigFileKind::Config, "second.json", std::option::Option::None);
|
||||
let first = crate::ConfigFileDescriptor::new("cfg.duplicate", crate::ConfigFileKind::Config, "first.json", std::option::Option::None);
|
||||
let second = crate::ConfigFileDescriptor::new("cfg.duplicate", crate::ConfigFileKind::Config, "second.json", std::option::Option::None);
|
||||
assert!(first.is_ok(), "first descriptor should be valid: {first:?}");
|
||||
assert!(second.is_ok(), "second descriptor should be valid: {second:?}");
|
||||
if let (std::result::Result::Ok(first), std::result::Result::Ok(second)) = (first, second) {
|
||||
let result = super::build_registry([first, second]);
|
||||
let result = crate::build_registry([first, second]);
|
||||
assert!(result.is_err(), "duplicate file_ids must be rejected");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_DUPLICATE);
|
||||
@@ -251,7 +251,7 @@ fn duplicate_registry_ids_are_rejected() {
|
||||
|
||||
#[test]
|
||||
fn descriptor_kind_must_match_file_id_namespace() {
|
||||
let result = super::ConfigFileDescriptor::new("schema.invalid-kind", super::ConfigFileKind::Config, "invalid.json", std::option::Option::None);
|
||||
let result = crate::ConfigFileDescriptor::new("schema.invalid-kind", crate::ConfigFileKind::Config, "invalid.json", std::option::Option::None);
|
||||
assert!(result.is_err(), "descriptor kind mismatch must be rejected");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID);
|
||||
@@ -260,10 +260,10 @@ fn descriptor_kind_must_match_file_id_namespace() {
|
||||
|
||||
#[test]
|
||||
fn config_schema_association_must_reference_registered_schema_descriptor() {
|
||||
let config = super::ConfigFileDescriptor::new("cfg.test", super::ConfigFileKind::Config, "test.json", std::option::Option::Some("schema.test"));
|
||||
let config = crate::ConfigFileDescriptor::new("cfg.test", crate::ConfigFileKind::Config, "test.json", std::option::Option::Some("schema.test"));
|
||||
assert!(config.is_ok(), "config descriptor should be valid before registry association validation: {config:?}");
|
||||
if let std::result::Result::Ok(config) = config {
|
||||
let result = super::build_registry([config]);
|
||||
let result = crate::build_registry([config]);
|
||||
assert!(result.is_err(), "registry must reject a missing schema association");
|
||||
if let std::result::Result::Err(error) = result {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FILE_MAPPING_INVALID);
|
||||
@@ -286,9 +286,9 @@ fn absolute_fixture_path() -> std::path::PathBuf {
|
||||
|
||||
#[test]
|
||||
fn defaults_register_generic_composite_schema_without_runtime_composite() {
|
||||
let registry = super::ConfigFileRegistry::defaults();
|
||||
let schema_id = super::ConfigFileId::new(super::FILE_ID_SCHEMA_COMPOSITE);
|
||||
let runtime_id = super::ConfigFileId::new("cfg.composite.ksp-app-wallet-desk");
|
||||
let registry = crate::ConfigFileRegistry::defaults();
|
||||
let schema_id = crate::ConfigFileId::new(crate::FILE_ID_SCHEMA_COMPOSITE);
|
||||
let runtime_id = crate::ConfigFileId::new("cfg.composite.ksp-app-wallet-desk");
|
||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||
assert!(schema_id.is_ok(), "composite schema file_id should be valid: {schema_id:?}");
|
||||
assert!(runtime_id.is_ok(), "future composite runtime file_id syntax should be valid: {runtime_id:?}");
|
||||
@@ -298,8 +298,8 @@ fn defaults_register_generic_composite_schema_without_runtime_composite() {
|
||||
assert!(schema.is_ok(), "generic composite schema should be registered: {schema:?}");
|
||||
assert!(runtime.is_err(), "no fictitious runtime composite should be registered");
|
||||
if let std::result::Result::Ok(schema) = schema {
|
||||
assert_eq!(schema.kind(), super::ConfigFileKind::Schema);
|
||||
assert_eq!(schema.filename(), std::path::Path::new(super::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
|
||||
assert_eq!(schema.kind(), crate::ConfigFileKind::Schema);
|
||||
assert_eq!(schema.filename(), std::path::Path::new(crate::DEFAULT_COMPOSITE_SCHEMA_FILENAME));
|
||||
}
|
||||
if let std::result::Result::Err(error) = runtime {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FILE_ID_UNKNOWN);
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
// file: crates/ksp-config-lib/unit_tests/sensitivity.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn environment_names_map_to_expected_sensitivity() {
|
||||
assert_eq!(super::ConfigSensitivity::from_variable_name("KSP_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(super::ConfigSensitivity::Public));
|
||||
assert_eq!(super::ConfigSensitivity::from_variable_name("KSP_MODE").ok(), std::option::Option::Some(super::ConfigSensitivity::Internal));
|
||||
assert_eq!(super::ConfigSensitivity::from_variable_name("KSP_SECRET_PASSWORD").ok(), std::option::Option::Some(super::ConfigSensitivity::Secret));
|
||||
assert_eq!(super::ConfigSensitivity::from_variable_name("KSPB_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(super::ConfigSensitivity::Public));
|
||||
assert_eq!(super::ConfigSensitivity::from_variable_name("KSPB_MODE").ok(), std::option::Option::Some(super::ConfigSensitivity::Internal));
|
||||
assert_eq!(super::ConfigSensitivity::from_variable_name("KSPB_SECRET_PASSWORD").ok(), std::option::Option::Some(super::ConfigSensitivity::Secret));
|
||||
assert_eq!(crate::ConfigSensitivity::from_variable_name("KSP_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(crate::ConfigSensitivity::Public));
|
||||
assert_eq!(crate::ConfigSensitivity::from_variable_name("KSP_MODE").ok(), std::option::Option::Some(crate::ConfigSensitivity::Internal));
|
||||
assert_eq!(crate::ConfigSensitivity::from_variable_name("KSP_SECRET_PASSWORD").ok(), std::option::Option::Some(crate::ConfigSensitivity::Secret));
|
||||
assert_eq!(crate::ConfigSensitivity::from_variable_name("KSPB_PUBLIC_ENDPOINT").ok(), std::option::Option::Some(crate::ConfigSensitivity::Public));
|
||||
assert_eq!(crate::ConfigSensitivity::from_variable_name("KSPB_MODE").ok(), std::option::Option::Some(crate::ConfigSensitivity::Internal));
|
||||
assert_eq!(crate::ConfigSensitivity::from_variable_name("KSPB_SECRET_PASSWORD").ok(), std::option::Option::Some(crate::ConfigSensitivity::Secret));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strongest_sensitivity_follows_secret_internal_public_order() {
|
||||
assert_eq!(super::ConfigSensitivity::Public.strongest(super::ConfigSensitivity::Internal), super::ConfigSensitivity::Internal);
|
||||
assert_eq!(super::ConfigSensitivity::Internal.strongest(super::ConfigSensitivity::Secret), super::ConfigSensitivity::Secret);
|
||||
assert_eq!(super::ConfigSensitivity::Secret.strongest(super::ConfigSensitivity::Public), super::ConfigSensitivity::Secret);
|
||||
assert_eq!(crate::ConfigSensitivity::Public.strongest(crate::ConfigSensitivity::Internal), crate::ConfigSensitivity::Internal);
|
||||
assert_eq!(crate::ConfigSensitivity::Internal.strongest(crate::ConfigSensitivity::Secret), crate::ConfigSensitivity::Secret);
|
||||
assert_eq!(crate::ConfigSensitivity::Secret.strongest(crate::ConfigSensitivity::Public), crate::ConfigSensitivity::Secret);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provenance_exposes_names_and_sources_without_values() {
|
||||
let process = super::ConfigValueProvenance::EnvironmentProcess { variable_name: "KSP_SECRET_TOKEN".to_owned() };
|
||||
let dotenv = super::ConfigValueProvenance::EnvironmentDotEnv { variable_name: "KSP_MODE".to_owned() };
|
||||
let fallback = super::ConfigValueProvenance::EnvironmentFallback { variable_name: "KSP_PUBLIC_HOST".to_owned() };
|
||||
let process = crate::ConfigValueProvenance::EnvironmentProcess { variable_name: "KSP_SECRET_TOKEN".to_owned() };
|
||||
let dotenv = crate::ConfigValueProvenance::EnvironmentDotEnv { variable_name: "KSP_MODE".to_owned() };
|
||||
let fallback = crate::ConfigValueProvenance::EnvironmentFallback { variable_name: "KSP_PUBLIC_HOST".to_owned() };
|
||||
assert_eq!(process.variable_name(), std::option::Option::Some("KSP_SECRET_TOKEN"));
|
||||
assert_eq!(process.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Process));
|
||||
assert_eq!(dotenv.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::DotEnv));
|
||||
assert_eq!(fallback.environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Fallback));
|
||||
assert_eq!(super::ConfigValueProvenance::DocumentLiteral.variable_name(), std::option::Option::None);
|
||||
assert_eq!(crate::ConfigValueProvenance::DocumentLiteral.variable_name(), std::option::Option::None);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// file: crates/ksp-core-lib/src/lib.rs
|
||||
// version: 6
|
||||
// version: 8
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-core-lib/src/program_ids.rs
|
||||
// version: 2
|
||||
// version: 4
|
||||
|
||||
const DOMAIN_SOLANA: &str = "solana";
|
||||
const FAMILY_CONSENSUS: &str = "consensus";
|
||||
@@ -266,6 +266,12 @@ impl<'a> ProgramIdFilter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the canonical KSP Program ID registry.
|
||||
#[must_use]
|
||||
pub const fn entries() -> &'static [crate::ProgramIdEntry] {
|
||||
return PROGRAM_ID_ENTRIES;
|
||||
}
|
||||
|
||||
const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[
|
||||
crate::ProgramIdEntry::new(
|
||||
"solana.address_lookup_table",
|
||||
@@ -438,12 +444,6 @@ const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[
|
||||
),
|
||||
];
|
||||
|
||||
/// Returns the canonical KSP Program ID registry.
|
||||
#[must_use]
|
||||
pub const fn entries() -> &'static [crate::ProgramIdEntry] {
|
||||
return PROGRAM_ID_ENTRIES;
|
||||
}
|
||||
|
||||
/// Returns a lazy view of Program IDs matching all configured filter axes.
|
||||
pub fn program_ids<'a>(filter: crate::ProgramIdFilter<'a>) -> impl std::iter::Iterator<Item = &'static crate::ProgramIdEntry> + 'a {
|
||||
return PROGRAM_ID_ENTRIES.iter().filter(move |entry| {
|
||||
|
||||
@@ -1,37 +1,15 @@
|
||||
// file: crates/ksp-logging-lib/src/domain.rs
|
||||
// version: 1
|
||||
// version: 3
|
||||
|
||||
std::thread_local! {
|
||||
static CURRENT_DOMAIN: std::cell::RefCell<std::option::Option<std::string::String>> = const { std::cell::RefCell::new(std::option::Option::None) };
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
struct SpanDomain {
|
||||
value: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DomainVisitor {
|
||||
value: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
impl tracing::field::Visit for DomainVisitor {
|
||||
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
|
||||
if field.name() == "domain" {
|
||||
self.value = std::option::Option::Some(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
||||
if field.name() == "domain" {
|
||||
self.value = std::option::Option::Some(format!("{value:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Crate-internal `DomainContextLayer` state shared across the owning crate.
|
||||
pub(crate) struct DomainContextLayer;
|
||||
|
||||
impl DomainContextLayer {
|
||||
/// Creates a new `DomainContextLayer` value.
|
||||
pub(crate) const fn new() -> Self {
|
||||
return Self;
|
||||
}
|
||||
@@ -99,6 +77,31 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
struct SpanDomain {
|
||||
value: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DomainVisitor {
|
||||
value: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
impl tracing::field::Visit for DomainVisitor {
|
||||
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
|
||||
if field.name() == "domain" {
|
||||
self.value = std::option::Option::Some(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
||||
if field.name() == "domain" {
|
||||
self.value = std::option::Option::Some(format!("{value:?}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the crate-internal current domain matches operation for the owning module.
|
||||
pub(crate) fn current_domain_matches(selectors: &[std::string::String]) -> bool {
|
||||
if let [selector] = selectors
|
||||
&& selector == "*"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// file: crates/ksp-logging-lib/src/error.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
/// Error code used when runtime logging settings are invalid.
|
||||
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_settings");
|
||||
/// Error code used when a global logging subscriber is already installed.
|
||||
pub const ERROR_CODE_ALREADY_INITIALIZED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "already_initialized");
|
||||
/// Error code used when a hot reload cannot replace the active runtime layers.
|
||||
pub const ERROR_CODE_RELOAD_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "reload_failed");
|
||||
/// Error code used when the rolling file output cannot be initialized.
|
||||
pub const ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "file_output_initialization_failed");
|
||||
/// Error code used when an application Logging runtime identity is invalid.
|
||||
pub const ERROR_CODE_INVALID_RUNTIME_IDENTITY: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_runtime_identity");
|
||||
/// Error code used when runtime logging settings are invalid.
|
||||
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_settings");
|
||||
/// Error code used when a hot reload cannot replace the active runtime layers.
|
||||
pub const ERROR_CODE_RELOAD_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "reload_failed");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/identity.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Stable runtime identity used to separate persistent file outputs between application launches.
|
||||
|
||||
@@ -41,6 +41,7 @@ impl LoggingRuntimeIdentity {
|
||||
return self.launch_timestamp.as_str();
|
||||
}
|
||||
|
||||
/// Executes the crate-internal file name prefix operation for `LoggingRuntimeIdentity`.
|
||||
pub(crate) fn file_name_prefix(&self, configured_prefix: &str) -> std::string::String {
|
||||
return format!("{}.{}.{}", self.application_id, self.launch_timestamp, configured_prefix);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// file: crates/ksp-logging-lib/src/lib.rs
|
||||
// version: 8
|
||||
// version: 10
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -69,6 +70,17 @@ pub use self::span::Span;
|
||||
/// Instruments an asynchronous future with a KSP span.
|
||||
pub use self::span::instrument;
|
||||
|
||||
/// Crate-internal `DomainContextLayer` state shared across the owning crate.
|
||||
pub(crate) use self::domain::DomainContextLayer;
|
||||
/// Executes the crate-internal current domain matches operation for the owning module.
|
||||
pub(crate) use self::domain::current_domain_matches;
|
||||
/// Crate-internal `RouteMakeWriter` state shared across the owning crate.
|
||||
pub(crate) use self::writer::RouteMakeWriter;
|
||||
/// Crate-internal `RoutedWriter` variants used by the owning crate.
|
||||
pub(crate) use self::writer::RoutedWriter;
|
||||
/// Crate-internal `StripAnsiWriter` state shared across the owning crate.
|
||||
pub(crate) use self::writer::StripAnsiWriter;
|
||||
|
||||
/// Hidden tracing crate bridge used exclusively by exported KSP logging macros.
|
||||
#[doc(hidden)]
|
||||
/// Internal macro bridge. KSP consumers must not use this reexport directly.
|
||||
pub extern crate tracing as __private_tracing;
|
||||
pub extern crate tracing;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// file: crates/ksp-logging-lib/src/macros.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
/// Emits a KSP error event with an explicit owning target.
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
(target: $target:expr, $($argument:tt)+) => {{
|
||||
$crate::__private_tracing::error!(target: $target, $($argument)+);
|
||||
$crate::tracing::error!(target: $target, $($argument)+);
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ macro_rules! error {
|
||||
#[macro_export]
|
||||
macro_rules! warn {
|
||||
(target: $target:expr, $($argument:tt)+) => {{
|
||||
$crate::__private_tracing::warn!(target: $target, $($argument)+);
|
||||
$crate::tracing::warn!(target: $target, $($argument)+);
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ macro_rules! warn {
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
(target: $target:expr, $($argument:tt)+) => {{
|
||||
$crate::__private_tracing::info!(target: $target, $($argument)+);
|
||||
$crate::tracing::info!(target: $target, $($argument)+);
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ macro_rules! info {
|
||||
#[macro_export]
|
||||
macro_rules! debug {
|
||||
(target: $target:expr, $($argument:tt)+) => {{
|
||||
$crate::__private_tracing::debug!(target: $target, $($argument)+);
|
||||
$crate::tracing::debug!(target: $target, $($argument)+);
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ macro_rules! debug {
|
||||
#[macro_export]
|
||||
macro_rules! trace {
|
||||
(target: $target:expr, $($argument:tt)+) => {{
|
||||
$crate::__private_tracing::trace!(target: $target, $($argument)+);
|
||||
$crate::tracing::trace!(target: $target, $($argument)+);
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -45,10 +45,10 @@ macro_rules! trace {
|
||||
#[macro_export]
|
||||
macro_rules! error_span {
|
||||
(target: $target:expr, $name:expr) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::error_span!(target: $target, $name))
|
||||
$crate::Span::__from_tracing($crate::tracing::error_span!(target: $target, $name))
|
||||
}};
|
||||
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::error_span!(target: $target, $name, $($field)+))
|
||||
$crate::Span::__from_tracing($crate::tracing::error_span!(target: $target, $name, $($field)+))
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -56,10 +56,10 @@ macro_rules! error_span {
|
||||
#[macro_export]
|
||||
macro_rules! warn_span {
|
||||
(target: $target:expr, $name:expr) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::warn_span!(target: $target, $name))
|
||||
$crate::Span::__from_tracing($crate::tracing::warn_span!(target: $target, $name))
|
||||
}};
|
||||
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::warn_span!(target: $target, $name, $($field)+))
|
||||
$crate::Span::__from_tracing($crate::tracing::warn_span!(target: $target, $name, $($field)+))
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -67,10 +67,10 @@ macro_rules! warn_span {
|
||||
#[macro_export]
|
||||
macro_rules! info_span {
|
||||
(target: $target:expr, $name:expr) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::info_span!(target: $target, $name))
|
||||
$crate::Span::__from_tracing($crate::tracing::info_span!(target: $target, $name))
|
||||
}};
|
||||
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::info_span!(target: $target, $name, $($field)+))
|
||||
$crate::Span::__from_tracing($crate::tracing::info_span!(target: $target, $name, $($field)+))
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -78,10 +78,10 @@ macro_rules! info_span {
|
||||
#[macro_export]
|
||||
macro_rules! debug_span {
|
||||
(target: $target:expr, $name:expr) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::debug_span!(target: $target, $name))
|
||||
$crate::Span::__from_tracing($crate::tracing::debug_span!(target: $target, $name))
|
||||
}};
|
||||
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::debug_span!(target: $target, $name, $($field)+))
|
||||
$crate::Span::__from_tracing($crate::tracing::debug_span!(target: $target, $name, $($field)+))
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -89,9 +89,9 @@ macro_rules! debug_span {
|
||||
#[macro_export]
|
||||
macro_rules! trace_span {
|
||||
(target: $target:expr, $name:expr) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::trace_span!(target: $target, $name))
|
||||
$crate::Span::__from_tracing($crate::tracing::trace_span!(target: $target, $name))
|
||||
}};
|
||||
(target: $target:expr, $name:expr, $($field:tt)+) => {{
|
||||
$crate::Span::__from_tracing($crate::__private_tracing::trace_span!(target: $target, $name, $($field)+))
|
||||
$crate::Span::__from_tracing($crate::tracing::trace_span!(target: $target, $name, $($field)+))
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/runtime.rs
|
||||
// version: 13
|
||||
// version: 16
|
||||
|
||||
use tracing_subscriber::Layer; // rust-rules: trait-import
|
||||
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
||||
@@ -228,31 +228,6 @@ pub fn initialize_with_identity(settings: &crate::LoggingSettings, identity: &cr
|
||||
return initialize_runtime(settings, std::option::Option::Some(identity.clone()));
|
||||
}
|
||||
|
||||
fn initialize_runtime(
|
||||
settings: &crate::LoggingSettings,
|
||||
runtime_identity: std::option::Option<crate::LoggingRuntimeIdentity>,
|
||||
) -> ksp_core_lib::Result<crate::LoggingGuard> {
|
||||
return prepare_runtime_with_identity(settings, runtime_identity.as_ref()).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
|
||||
let PreparedRuntime { layers, outputs } = prepared;
|
||||
let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(layers);
|
||||
let subscriber = tracing_subscriber::registry().with(reload_layer);
|
||||
let install_result = tracing::subscriber::set_global_default(subscriber);
|
||||
return match install_result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(crate::LoggingGuard {
|
||||
reload_handle,
|
||||
settings: settings.clone(),
|
||||
runtime_identity,
|
||||
outputs,
|
||||
retired_dropped_lines: crate::DroppedLines::zero(),
|
||||
retired_file_dropped_lines: std::collections::HashMap::new(),
|
||||
}),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_ALREADY_INITIALIZED, "the global KSP tracing subscriber is already installed").with_source(error),
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// Replaces the active KSP logging settings and non-blocking outputs without reinstalling the global subscriber.
|
||||
///
|
||||
/// New runtime layers, writers and guards are fully prepared before the reload is attempted. If validation or preparation fails, the currently active
|
||||
@@ -282,6 +257,31 @@ pub fn reinitialize(guard: &mut crate::LoggingGuard, settings: &crate::LoggingSe
|
||||
});
|
||||
}
|
||||
|
||||
fn initialize_runtime(
|
||||
settings: &crate::LoggingSettings,
|
||||
runtime_identity: std::option::Option<crate::LoggingRuntimeIdentity>,
|
||||
) -> ksp_core_lib::Result<crate::LoggingGuard> {
|
||||
return prepare_runtime_with_identity(settings, runtime_identity.as_ref()).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
|
||||
let PreparedRuntime { layers, outputs } = prepared;
|
||||
let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(layers);
|
||||
let subscriber = tracing_subscriber::registry().with(reload_layer);
|
||||
let install_result = tracing::subscriber::set_global_default(subscriber);
|
||||
return match install_result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(crate::LoggingGuard {
|
||||
reload_handle,
|
||||
settings: settings.clone(),
|
||||
runtime_identity,
|
||||
outputs,
|
||||
retired_dropped_lines: crate::DroppedLines::zero(),
|
||||
retired_file_dropped_lines: std::collections::HashMap::new(),
|
||||
}),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_ALREADY_INITIALIZED, "the global KSP tracing subscriber is already installed").with_source(error),
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fn prepare_runtime_with_identity(
|
||||
settings: &crate::LoggingSettings,
|
||||
runtime_identity: std::option::Option<&crate::LoggingRuntimeIdentity>,
|
||||
@@ -315,7 +315,7 @@ fn prepare_runtime_with_identity(
|
||||
if output_layers.is_empty() {
|
||||
return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs });
|
||||
}
|
||||
output_layers.insert(0, crate::domain::DomainContextLayer::new().boxed());
|
||||
output_layers.insert(0, crate::DomainContextLayer::new().boxed());
|
||||
let takeover_layer = build_target_filter(settings).and_then(output_layers).boxed();
|
||||
return std::result::Result::Ok(PreparedRuntime { layers: vec![takeover_layer], outputs });
|
||||
}
|
||||
@@ -368,7 +368,7 @@ fn build_file_output(
|
||||
);
|
||||
},
|
||||
};
|
||||
let stripped_writer = crate::writer::StripAnsiWriter::new(appender);
|
||||
let stripped_writer = crate::StripAnsiWriter::new(appender);
|
||||
let thread_name = format!("ksp-logging-{}", file.output_id());
|
||||
let prepared = build_non_blocking_output(stripped_writer, thread_name.as_str(), settings.span_events(), false, false, file.format(), file.filter());
|
||||
let metadata = crate::RuntimeFileMetadata {
|
||||
@@ -413,7 +413,7 @@ fn build_format_layer(
|
||||
let span_events = map_span_events(span_events);
|
||||
return match format {
|
||||
crate::LogFormat::Human => tracing_subscriber::fmt::layer()
|
||||
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
|
||||
.with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
|
||||
.with_ansi(ansi)
|
||||
.with_ansi_sanitization(ansi_sanitization)
|
||||
.with_target(true)
|
||||
@@ -423,7 +423,7 @@ fn build_format_layer(
|
||||
.boxed(),
|
||||
crate::LogFormat::Compact => tracing_subscriber::fmt::layer()
|
||||
.compact()
|
||||
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
|
||||
.with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
|
||||
.with_ansi(ansi)
|
||||
.with_ansi_sanitization(ansi_sanitization)
|
||||
.with_target(true)
|
||||
@@ -433,7 +433,7 @@ fn build_format_layer(
|
||||
.boxed(),
|
||||
crate::LogFormat::Pretty => tracing_subscriber::fmt::layer()
|
||||
.pretty()
|
||||
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
|
||||
.with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
|
||||
.with_ansi(ansi)
|
||||
.with_ansi_sanitization(ansi_sanitization)
|
||||
.with_target(true)
|
||||
@@ -443,7 +443,7 @@ fn build_format_layer(
|
||||
.boxed(),
|
||||
crate::LogFormat::Json => tracing_subscriber::fmt::layer()
|
||||
.json()
|
||||
.with_writer(crate::writer::RouteMakeWriter::new(writer, filter.clone()))
|
||||
.with_writer(crate::RouteMakeWriter::new(writer, filter.clone()))
|
||||
.with_ansi(false)
|
||||
.with_target(true)
|
||||
.with_file(true)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/src/writer.rs
|
||||
// version: 3
|
||||
// version: 5
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum StripAnsiState {
|
||||
@@ -12,12 +12,14 @@ enum StripAnsiState {
|
||||
StringEscape,
|
||||
}
|
||||
|
||||
/// Crate-internal `StripAnsiWriter` state shared across the owning crate.
|
||||
pub(crate) struct StripAnsiWriter<W> {
|
||||
inner: W,
|
||||
state: StripAnsiState,
|
||||
}
|
||||
|
||||
impl<W> StripAnsiWriter<W> {
|
||||
/// Creates a new `StripAnsiWriter` value.
|
||||
pub(crate) const fn new(inner: W) -> Self {
|
||||
return Self { inner, state: StripAnsiState::Text };
|
||||
}
|
||||
@@ -100,6 +102,7 @@ impl<W> StripAnsiWriter<W> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Crate-internal `RouteMakeWriter` state shared across the owning crate.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RouteMakeWriter<W> {
|
||||
inner: W,
|
||||
@@ -107,17 +110,19 @@ pub(crate) struct RouteMakeWriter<W> {
|
||||
}
|
||||
|
||||
impl<W> RouteMakeWriter<W> {
|
||||
/// Creates a new `RouteMakeWriter` value.
|
||||
pub(crate) fn new(inner: W, filter: crate::OutputFilter) -> Self {
|
||||
return Self { inner, filter };
|
||||
}
|
||||
}
|
||||
|
||||
/// Crate-internal `RoutedWriter` variants used by the owning crate.
|
||||
pub(crate) enum RoutedWriter<W> {
|
||||
Enabled(W),
|
||||
Disabled,
|
||||
}
|
||||
|
||||
impl<W> std::io::Write for RoutedWriter<W>
|
||||
impl<W> std::io::Write for crate::RoutedWriter<W>
|
||||
where
|
||||
W: std::io::Write,
|
||||
{
|
||||
@@ -140,17 +145,17 @@ impl<'writer, W> tracing_subscriber::fmt::MakeWriter<'writer> for RouteMakeWrite
|
||||
where
|
||||
W: tracing_subscriber::fmt::MakeWriter<'writer>,
|
||||
{
|
||||
type Writer = RoutedWriter<W::Writer>;
|
||||
type Writer = crate::RoutedWriter<W::Writer>;
|
||||
|
||||
fn make_writer(&'writer self) -> Self::Writer {
|
||||
return RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer(&self.inner));
|
||||
return crate::RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer(&self.inner));
|
||||
}
|
||||
|
||||
fn make_writer_for(&'writer self, metadata: &tracing::Metadata<'_>) -> Self::Writer {
|
||||
if metadata_matches_filter(metadata, &self.filter) {
|
||||
return RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer_for(&self.inner, metadata));
|
||||
return crate::RoutedWriter::Enabled(tracing_subscriber::fmt::MakeWriter::make_writer_for(&self.inner, metadata));
|
||||
}
|
||||
return RoutedWriter::Disabled;
|
||||
return crate::RoutedWriter::Disabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +168,7 @@ fn metadata_matches_filter(metadata: &tracing::Metadata<'_>, filter: &crate::Out
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
return crate::domain::current_domain_matches(filter.domains());
|
||||
return crate::current_domain_matches(filter.domains());
|
||||
}
|
||||
|
||||
fn level_is_enabled(level: &tracing::Level, filter: crate::LogFilterLevel) -> bool {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// file: crates/ksp-logging-lib/tests/overhead.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Diagnostic gross-overhead probe for the reload layer used by KSP Logging.
|
||||
|
||||
use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import
|
||||
|
||||
const TEST_TARGET: &str = "ksp-logging-lib";
|
||||
const ITERATIONS: u64 = 200_000;
|
||||
const TEST_TARGET: &str = "ksp-logging-lib";
|
||||
|
||||
fn emit_probe_events() {
|
||||
for sequence in 0..ITERATIONS {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-logging-lib/tests/ownership.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Integration audit ensuring KSP crates do not bypass the logging facade.
|
||||
|
||||
@@ -157,6 +157,11 @@ fn workspace_crates_do_not_bypass_ksp_logging_facade() {
|
||||
std::result::Result::Err(_) => continue,
|
||||
};
|
||||
assert!(!source_uses_direct_path(source.as_str(), "tracing::"), "{} bypasses ksp-logging-lib via tracing", rust_file.display());
|
||||
assert!(
|
||||
!source_uses_direct_path(source.as_str(), "ksp_logging_lib::tracing::"),
|
||||
"{} bypasses ksp-logging-lib via its hidden tracing bridge",
|
||||
rust_file.display(),
|
||||
);
|
||||
assert!(
|
||||
!source_uses_direct_path(source.as_str(), "tracing_subscriber::"),
|
||||
"{} bypasses ksp-logging-lib via tracing-subscriber",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// file: crates/ksp-logging-lib/tests/runtime.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload.
|
||||
|
||||
const EXTERNAL_TARGET: &str = "sqlx";
|
||||
const JSON_KSP_TARGET: &str = "ksp-logging-json-test";
|
||||
const LOGGING_TARGET: &str = "ksp-logging-lib";
|
||||
const OTHER_KSP_TARGET: &str = "ksp-store-lib";
|
||||
const JSON_KSP_TARGET: &str = "ksp-logging-json-test";
|
||||
const EXTERNAL_TARGET: &str = "sqlx";
|
||||
|
||||
fn logging_trace_enabled() -> bool {
|
||||
return tracing::enabled!(target: LOGGING_TARGET, tracing::Level::TRACE);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
// file: crates/ksp-logging-lib/unit_tests/domain.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn wildcard_domain_matches_with_or_without_current_domain() {
|
||||
super::set_current_domain(std::option::Option::None);
|
||||
assert!(super::current_domain_matches(&["*".to_string()]));
|
||||
assert!(crate::current_domain_matches(&["*".to_string()]));
|
||||
super::set_current_domain(std::option::Option::Some("logging"));
|
||||
assert!(super::current_domain_matches(&["*".to_string()]));
|
||||
assert!(crate::current_domain_matches(&["*".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_domain_requires_current_matching_prefix() {
|
||||
super::set_current_domain(std::option::Option::None);
|
||||
assert!(!super::current_domain_matches(&["logging".to_string()]));
|
||||
assert!(!crate::current_domain_matches(&["logging".to_string()]));
|
||||
super::set_current_domain(std::option::Option::Some("logging.runtime"));
|
||||
assert!(super::current_domain_matches(&["logging".to_string()]));
|
||||
assert!(super::current_domain_matches(&["store".to_string(), "logging.runtime".to_string()]));
|
||||
assert!(!super::current_domain_matches(&["store".to_string()]));
|
||||
assert!(crate::current_domain_matches(&["logging".to_string()]));
|
||||
assert!(crate::current_domain_matches(&["store".to_string(), "logging.runtime".to_string()]));
|
||||
assert!(!crate::current_domain_matches(&["store".to_string()]));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-logging-lib/unit_tests/writer.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn ansi_writer_strips_csi_sequences() {
|
||||
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||
let mut writer = crate::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||
let write_result = std::io::Write::write_all(&mut writer, b"before\x1b[31mred\x1b[0mafter");
|
||||
assert!(write_result.is_ok());
|
||||
assert_eq!(writer.into_inner(), b"beforeredafter");
|
||||
@@ -11,7 +11,7 @@ fn ansi_writer_strips_csi_sequences() {
|
||||
|
||||
#[test]
|
||||
fn ansi_writer_preserves_state_across_split_writes() {
|
||||
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||
let mut writer = crate::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||
let first = std::io::Write::write_all(&mut writer, b"a\x1b[");
|
||||
let second = std::io::Write::write_all(&mut writer, b"32mb");
|
||||
assert!(first.is_ok());
|
||||
@@ -21,7 +21,7 @@ fn ansi_writer_preserves_state_across_split_writes() {
|
||||
|
||||
#[test]
|
||||
fn ansi_writer_strips_osc_sequences_terminated_by_bell_or_st() {
|
||||
let mut writer = super::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||
let mut writer = crate::StripAnsiWriter::new(std::vec::Vec::<u8>::new());
|
||||
let first = std::io::Write::write_all(&mut writer, b"a\x1b]0;title\x07b");
|
||||
let second = std::io::Write::write_all(&mut writer, b"c\x1b]8;;https://example.invalid\x1b\\d");
|
||||
assert!(first.is_ok());
|
||||
@@ -43,7 +43,7 @@ fn output_level_routing_covers_all_filter_levels() {
|
||||
|
||||
#[test]
|
||||
fn disabled_routed_writer_discards_bytes_without_error() {
|
||||
let mut writer = super::RoutedWriter::<std::vec::Vec<u8>>::Disabled;
|
||||
let mut writer = crate::RoutedWriter::<std::vec::Vec<u8>>::Disabled;
|
||||
let write_result = std::io::Write::write_all(&mut writer, b"discarded");
|
||||
let flush_result = std::io::Write::flush(&mut writer);
|
||||
assert!(write_result.is_ok());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/client.rs
|
||||
// version: 5
|
||||
// version: 7
|
||||
|
||||
/// Passive runtime availability reported for one logical HTTP endpoint or role.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -163,26 +163,6 @@ impl HttpEndpointSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct HttpEndpointHttpResponse {
|
||||
status: u16,
|
||||
retry_after: std::option::Option<std::time::Duration>,
|
||||
body: std::vec::Vec<u8>,
|
||||
}
|
||||
|
||||
impl HttpEndpointHttpResponse {
|
||||
pub(crate) const fn status(&self) -> u16 {
|
||||
return self.status;
|
||||
}
|
||||
|
||||
pub(crate) const fn retry_after(&self) -> std::option::Option<std::time::Duration> {
|
||||
return self.retry_after;
|
||||
}
|
||||
|
||||
pub(crate) fn body(&self) -> &[u8] {
|
||||
return self.body.as_slice();
|
||||
}
|
||||
}
|
||||
|
||||
/// Shareable logical HTTP endpoint client owned by KSP Transport.
|
||||
///
|
||||
/// The underlying `reqwest::Client` owns socket pooling. KSP keeps the configured URL private from diagnostics and exposes only safe routing metadata.
|
||||
@@ -191,20 +171,15 @@ pub struct HttpEndpointClient {
|
||||
inner: std::sync::Arc<HttpEndpointClientInner>,
|
||||
}
|
||||
|
||||
struct HttpEndpointClientInner {
|
||||
settings: crate::HttpEndpointSettings,
|
||||
client: reqwest::Client,
|
||||
role_runtimes: std::vec::Vec<std::sync::Arc<crate::resilience::HttpRoleRuntime>>,
|
||||
}
|
||||
|
||||
impl HttpEndpointClient {
|
||||
/// Builds one logical endpoint client from KSP-owned runtime settings.
|
||||
pub fn new(settings: crate::HttpEndpointSettings) -> ksp_core_lib::Result<Self> {
|
||||
return Self::new_with_notify(settings, std::sync::Arc::new(tokio::sync::Notify::new()));
|
||||
}
|
||||
|
||||
/// Creates a new with notify value for `HttpEndpointClient`.
|
||||
pub(crate) fn new_with_notify(settings: crate::HttpEndpointSettings, notify: std::sync::Arc<tokio::sync::Notify>) -> ksp_core_lib::Result<Self> {
|
||||
let validation = crate::settings::validate_endpoint_settings(&settings);
|
||||
let validation = crate::validate_endpoint_settings(&settings);
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
@@ -221,7 +196,7 @@ impl HttpEndpointClient {
|
||||
};
|
||||
let mut role_runtimes = std::vec::Vec::with_capacity(settings.roles().len());
|
||||
for role in settings.roles() {
|
||||
role_runtimes.push(std::sync::Arc::new(crate::resilience::HttpRoleRuntime::new(role, std::sync::Arc::clone(¬ify))));
|
||||
role_runtimes.push(std::sync::Arc::new(crate::HttpRoleRuntime::new(role, std::sync::Arc::clone(¬ify))));
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
@@ -265,6 +240,7 @@ impl HttpEndpointClient {
|
||||
return self.inner.settings.request_timeout();
|
||||
}
|
||||
|
||||
/// Executes the crate-internal post json rpc operation for `HttpEndpointClient`.
|
||||
pub(crate) async fn post_json_rpc(&self, payload: &str, timeout: std::time::Duration) -> ksp_core_lib::Result<HttpEndpointHttpResponse> {
|
||||
if timeout.is_zero() {
|
||||
return std::result::Result::Err(
|
||||
@@ -327,6 +303,7 @@ impl HttpEndpointClient {
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes the crate-internal matching role operation for `HttpEndpointClient`.
|
||||
pub(crate) fn matching_role<'a>(
|
||||
&'a self,
|
||||
role: &crate::HttpRoleName,
|
||||
@@ -348,11 +325,12 @@ impl HttpEndpointClient {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
/// Executes the crate-internal matching role runtime operation for `HttpEndpointClient`.
|
||||
pub(crate) fn matching_role_runtime(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
request_kind: &crate::HttpRequestKind,
|
||||
) -> std::option::Option<(u32, std::sync::Arc<crate::resilience::HttpRoleRuntime>)> {
|
||||
) -> std::option::Option<(u32, std::sync::Arc<crate::HttpRoleRuntime>)> {
|
||||
if !self.enabled() {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
@@ -378,6 +356,7 @@ impl HttpEndpointClient {
|
||||
return std::option::Option::None;
|
||||
}
|
||||
|
||||
/// Returns the current availability.
|
||||
pub(crate) fn availability(&self) -> crate::HttpEndpointAvailability {
|
||||
if !self.enabled() {
|
||||
return crate::HttpEndpointAvailability::Disabled;
|
||||
@@ -419,10 +398,40 @@ impl std::fmt::Debug for HttpEndpointClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Crate-internal `HttpEndpointHttpResponse` state shared across the owning crate.
|
||||
pub(crate) struct HttpEndpointHttpResponse {
|
||||
status: u16,
|
||||
retry_after: std::option::Option<std::time::Duration>,
|
||||
body: std::vec::Vec<u8>,
|
||||
}
|
||||
|
||||
impl HttpEndpointHttpResponse {
|
||||
/// Returns the current status.
|
||||
pub(crate) const fn status(&self) -> u16 {
|
||||
return self.status;
|
||||
}
|
||||
|
||||
/// Returns the current retry after.
|
||||
pub(crate) const fn retry_after(&self) -> std::option::Option<std::time::Duration> {
|
||||
return self.retry_after;
|
||||
}
|
||||
|
||||
/// Returns the current body.
|
||||
pub(crate) fn body(&self) -> &[u8] {
|
||||
return self.body.as_slice();
|
||||
}
|
||||
}
|
||||
|
||||
struct HttpEndpointClientInner {
|
||||
settings: crate::HttpEndpointSettings,
|
||||
client: reqwest::Client,
|
||||
role_runtimes: std::vec::Vec<std::sync::Arc<crate::HttpRoleRuntime>>,
|
||||
}
|
||||
|
||||
fn role_snapshot(
|
||||
role: &crate::HttpEndpointRoleSettings,
|
||||
request_kinds: std::vec::Vec<std::string::String>,
|
||||
runtime: &crate::resilience::HttpRoleRuntime,
|
||||
runtime: &crate::HttpRoleRuntime,
|
||||
) -> crate::HttpEndpointRoleSnapshot {
|
||||
let availability = if role.enabled() { runtime.availability(std::time::Instant::now()) } else { crate::HttpEndpointAvailability::Disabled };
|
||||
return crate::HttpEndpointRoleSnapshot {
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/error.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
/// Error code used when HTTP transport runtime settings are invalid.
|
||||
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_settings");
|
||||
/// Error code used when no logical endpoint can satisfy a request.
|
||||
pub const ERROR_CODE_ENDPOINT_SELECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "endpoint_selection_failed");
|
||||
/// Error code used when an HTTP connection cannot be established.
|
||||
pub const ERROR_CODE_HTTP_CONNECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "http_connection_failed");
|
||||
/// Error code used when an HTTP request fails after a connection exists.
|
||||
pub const ERROR_CODE_HTTP_REQUEST_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "http_request_failed");
|
||||
/// Error code used when a transport deadline expires.
|
||||
pub const ERROR_CODE_TIMEOUT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "timeout");
|
||||
/// Error code used when an endpoint or provider rate-limits a request.
|
||||
pub const ERROR_CODE_RATE_LIMITED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "rate_limited");
|
||||
/// Error code used when a JSON-RPC request cannot be encoded.
|
||||
pub const ERROR_CODE_JSON_ENCODE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_encode_failed");
|
||||
/// Error code used when an HTTP JSON-RPC payload cannot be decoded as JSON.
|
||||
pub const ERROR_CODE_JSON_DECODE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_decode_failed");
|
||||
/// Error code used when a decoded JSON-RPC envelope violates protocol invariants.
|
||||
pub const ERROR_CODE_JSON_RPC_PROTOCOL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_rpc_protocol_invalid");
|
||||
/// Error code used when a remote JSON-RPC endpoint returns an application-level RPC error.
|
||||
pub const ERROR_CODE_RPC_APPLICATION_ERROR: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "rpc_application_error");
|
||||
/// Error code used when a historically documented RPC method is no longer supported by the targeted runtime.
|
||||
pub const ERROR_CODE_METHOD_REMOVED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "method_removed");
|
||||
/// Error code used when a decoded response cannot satisfy the KSP transport contract expected by the caller.
|
||||
pub const ERROR_CODE_INVALID_RESPONSE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_response");
|
||||
/// Error code used when typed Solana RPC parameters violate a locally enforceable method contract.
|
||||
pub const ERROR_CODE_INVALID_RPC_PARAMETERS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_rpc_parameters");
|
||||
/// Error code used when HTTP transport runtime settings are invalid.
|
||||
pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "invalid_settings");
|
||||
/// Error code used when an HTTP JSON-RPC payload cannot be decoded as JSON.
|
||||
pub const ERROR_CODE_JSON_DECODE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_decode_failed");
|
||||
/// Error code used when a JSON-RPC request cannot be encoded.
|
||||
pub const ERROR_CODE_JSON_ENCODE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_encode_failed");
|
||||
/// Error code used when a decoded JSON-RPC envelope violates protocol invariants.
|
||||
pub const ERROR_CODE_JSON_RPC_PROTOCOL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "json_rpc_protocol_invalid");
|
||||
/// Error code used when a historically documented RPC method is no longer supported by the targeted runtime.
|
||||
pub const ERROR_CODE_METHOD_REMOVED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "method_removed");
|
||||
/// Error code used when an endpoint or provider rate-limits a request.
|
||||
pub const ERROR_CODE_RATE_LIMITED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "rate_limited");
|
||||
/// Error code used when a remote JSON-RPC endpoint returns an application-level RPC error.
|
||||
pub const ERROR_CODE_RPC_APPLICATION_ERROR: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "rpc_application_error");
|
||||
/// Error code used when a transport deadline expires.
|
||||
pub const ERROR_CODE_TIMEOUT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "timeout");
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/executor.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
const HTTP_REQUEST_TIMEOUT: u16 = 408;
|
||||
const HTTP_TOO_MANY_REQUESTS: u16 = 429;
|
||||
const HTTP_INTERNAL_SERVER_ERROR: u16 = 500;
|
||||
const HTTP_BAD_GATEWAY: u16 = 502;
|
||||
const HTTP_SERVICE_UNAVAILABLE: u16 = 503;
|
||||
const HTTP_GATEWAY_TIMEOUT: u16 = 504;
|
||||
const HTTP_INTERNAL_SERVER_ERROR: u16 = 500;
|
||||
const HTTP_REQUEST_TIMEOUT: u16 = 408;
|
||||
const HTTP_SERVICE_UNAVAILABLE: u16 = 503;
|
||||
const HTTP_TOO_MANY_REQUESTS: u16 = 429;
|
||||
|
||||
impl crate::HttpTransportPool {
|
||||
/// Executes one audited standard Solana HTTP JSON-RPC method through KSP routing, admission and bounded retry policy.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||
// version: 19
|
||||
// version: 21
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -34,8 +35,6 @@ mod rpc_tokens;
|
||||
mod rpc_transactions;
|
||||
mod settings;
|
||||
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
|
||||
/// Passive runtime availability reported for one logical HTTP endpoint.
|
||||
pub use self::client::HttpEndpointAvailability;
|
||||
/// Shareable logical HTTP endpoint client owned by KSP Transport.
|
||||
@@ -194,10 +193,6 @@ pub use self::rpc_common::SolanaContextConfig;
|
||||
pub use self::rpc_common::SolanaRpcContext;
|
||||
/// Generic contextual result returned by typed Solana HTTP RPC adapters.
|
||||
pub use self::rpc_common::SolanaRpcResponse;
|
||||
/// Decodes one private serde wire type into the shared Transport error domain for typed RPC adapters.
|
||||
pub(crate) use self::rpc_common::decode_wire_json;
|
||||
/// Parses a base58 public key without echoing its wire value into diagnostics for typed RPC adapters.
|
||||
pub(crate) use self::rpc_common::parse_wire_pubkey;
|
||||
/// Inflation-governor values returned by `getInflationGovernor`.
|
||||
pub use self::rpc_economics::SolanaInflationGovernor;
|
||||
/// Current inflation-rate values returned by `getInflationRate`.
|
||||
@@ -296,3 +291,18 @@ pub use self::settings::HttpRoleLimits;
|
||||
pub use self::settings::HttpRoleName;
|
||||
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
|
||||
pub use self::settings::HttpTransportSettings;
|
||||
|
||||
/// Owning tracing target for events emitted by the on-chain transport crate.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
/// Crate-internal `HttpConcurrencyPermit` state shared across the owning crate.
|
||||
pub(crate) use self::resilience::HttpConcurrencyPermit;
|
||||
/// Crate-internal `HttpRoleRuntime` state shared across the owning crate.
|
||||
pub(crate) use self::resilience::HttpRoleRuntime;
|
||||
/// Crate-internal `RoleAdmissionAttempt` variants used by the owning crate.
|
||||
pub(crate) use self::resilience::RoleAdmissionAttempt;
|
||||
/// Decodes one private serde wire type into the shared Transport error domain for typed RPC adapters.
|
||||
pub(crate) use self::rpc_common::decode_wire_json;
|
||||
/// Parses a base58 public key without echoing its wire value into diagnostics for typed RPC adapters.
|
||||
pub(crate) use self::rpc_common::parse_wire_pubkey;
|
||||
/// Validates endpoint settings.
|
||||
pub(crate) use self::settings::validate_endpoint_settings;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/pool.rs
|
||||
// version: 5
|
||||
// version: 7
|
||||
|
||||
/// Safe snapshot of the logical HTTP endpoint pool.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -74,8 +74,8 @@ impl HttpEndpointSelection {
|
||||
pub struct HttpRequestPermit {
|
||||
selection: crate::HttpEndpointSelection,
|
||||
deadline: std::time::Instant,
|
||||
role_runtime: std::sync::Arc<crate::resilience::HttpRoleRuntime>,
|
||||
_concurrency_permit: crate::resilience::HttpConcurrencyPermit,
|
||||
role_runtime: std::sync::Arc<crate::HttpRoleRuntime>,
|
||||
_concurrency_permit: crate::HttpConcurrencyPermit,
|
||||
}
|
||||
|
||||
impl HttpRequestPermit {
|
||||
@@ -165,14 +165,6 @@ pub struct HttpTransportPool {
|
||||
inner: std::sync::Arc<HttpTransportPoolInner>,
|
||||
}
|
||||
|
||||
struct HttpTransportPoolInner {
|
||||
clients: std::vec::Vec<crate::HttpEndpointClient>,
|
||||
retry: crate::HttpRetrySettings,
|
||||
notify: std::sync::Arc<tokio::sync::Notify>,
|
||||
request_ids: std::sync::atomic::AtomicU64,
|
||||
cursors: std::sync::Mutex<std::collections::BTreeMap<(std::string::String, std::string::String, u32), usize>>,
|
||||
}
|
||||
|
||||
impl HttpTransportPool {
|
||||
/// Builds a logical endpoint pool after validating all Transport-owned runtime settings.
|
||||
pub fn new(settings: crate::HttpTransportSettings) -> ksp_core_lib::Result<Self> {
|
||||
@@ -215,6 +207,7 @@ impl HttpTransportPool {
|
||||
return &self.inner.retry;
|
||||
}
|
||||
|
||||
/// Executes the crate-internal next request id operation for `HttpTransportPool`.
|
||||
pub(crate) fn next_request_id(&self) -> u64 {
|
||||
let id = self.inner.request_ids.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
if id == 0 {
|
||||
@@ -369,7 +362,7 @@ impl HttpTransportPool {
|
||||
let candidate = &candidates[position];
|
||||
let admission = candidate.runtime.try_acquire(now);
|
||||
match admission {
|
||||
crate::resilience::RoleAdmissionAttempt::Ready(concurrency_permit) => {
|
||||
crate::RoleAdmissionAttempt::Ready(concurrency_permit) => {
|
||||
let selection_result = self.selection_from_runtime_candidate(role, request_kind, candidate);
|
||||
let selection = match selection_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -391,13 +384,13 @@ impl HttpTransportPool {
|
||||
_concurrency_permit: concurrency_permit,
|
||||
});
|
||||
},
|
||||
crate::resilience::RoleAdmissionAttempt::BlockedUntil(ready_at) => {
|
||||
crate::RoleAdmissionAttempt::BlockedUntil(ready_at) => {
|
||||
earliest_ready = earlier_instant(earliest_ready, ready_at);
|
||||
},
|
||||
crate::resilience::RoleAdmissionAttempt::ConcurrencySaturated => {
|
||||
crate::RoleAdmissionAttempt::ConcurrencySaturated => {
|
||||
concurrency_saturated = true;
|
||||
},
|
||||
crate::resilience::RoleAdmissionAttempt::Unavailable => {},
|
||||
crate::RoleAdmissionAttempt::Unavailable => {},
|
||||
}
|
||||
offset = offset.saturating_add(1);
|
||||
}
|
||||
@@ -434,6 +427,7 @@ impl HttpTransportPool {
|
||||
return std::time::Instant::now() < deadline;
|
||||
}
|
||||
|
||||
/// Executes the crate-internal common request timeout operation for `HttpTransportPool`.
|
||||
pub(crate) fn common_request_timeout(
|
||||
&self,
|
||||
role: &crate::HttpRoleName,
|
||||
@@ -548,6 +542,14 @@ impl std::fmt::Debug for HttpTransportPool {
|
||||
}
|
||||
}
|
||||
|
||||
struct HttpTransportPoolInner {
|
||||
clients: std::vec::Vec<crate::HttpEndpointClient>,
|
||||
retry: crate::HttpRetrySettings,
|
||||
notify: std::sync::Arc<tokio::sync::Notify>,
|
||||
request_ids: std::sync::atomic::AtomicU64,
|
||||
cursors: std::sync::Mutex<std::collections::BTreeMap<(std::string::String, std::string::String, u32), usize>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PoolCandidate {
|
||||
client_index: usize,
|
||||
@@ -557,7 +559,7 @@ struct PoolCandidate {
|
||||
struct RuntimePoolCandidate {
|
||||
client_index: usize,
|
||||
priority: u32,
|
||||
runtime: std::sync::Arc<crate::resilience::HttpRoleRuntime>,
|
||||
runtime: std::sync::Arc<crate::HttpRoleRuntime>,
|
||||
}
|
||||
|
||||
enum RuntimeSelectionAttempt {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/resilience.rs
|
||||
// version: 1
|
||||
// version: 3
|
||||
|
||||
const DEFAULT_RATE_LIMIT_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(1);
|
||||
const MAX_PROVIDER_RETRY_AFTER: std::time::Duration = std::time::Duration::from_secs(60);
|
||||
@@ -71,61 +71,7 @@ impl HttpRetryDecision {
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method.
|
||||
///
|
||||
/// `completed_retries` counts retries already performed after the initial attempt. Provider `Retry-After` values are defensively bounded to sixty seconds
|
||||
/// before they can extend the local exponential backoff. RPC application errors are never converted into transport retries.
|
||||
#[must_use]
|
||||
pub fn evaluate_transport_retry(
|
||||
method: &crate::HttpRpcMethodDescriptor,
|
||||
settings: &crate::HttpRetrySettings,
|
||||
cause: crate::HttpRetryCause,
|
||||
dispatch_state: crate::HttpDispatchState,
|
||||
completed_retries: u32,
|
||||
provider_retry_after: std::option::Option<std::time::Duration>,
|
||||
) -> crate::HttpRetryDecision {
|
||||
if completed_retries >= settings.max_retries() || !cause.is_retryable() {
|
||||
return crate::HttpRetryDecision::Stop;
|
||||
}
|
||||
if method.transport_retry_class() == crate::TransportRetryClass::NotApplicable {
|
||||
return crate::HttpRetryDecision::Stop;
|
||||
}
|
||||
if method.transport_retry_class() == crate::TransportRetryClass::NeverAfterDispatch && dispatch_state == crate::HttpDispatchState::DispatchedAmbiguous {
|
||||
return crate::HttpRetryDecision::Stop;
|
||||
}
|
||||
let retry_number = completed_retries.saturating_add(1);
|
||||
let mut delay = retry_backoff(settings, retry_number);
|
||||
if cause == crate::HttpRetryCause::RateLimited
|
||||
&& let std::option::Option::Some(provider_delay) = provider_retry_after
|
||||
{
|
||||
let bounded_provider_delay = std::cmp::min(provider_delay, MAX_PROVIDER_RETRY_AFTER);
|
||||
if bounded_provider_delay > delay {
|
||||
delay = bounded_provider_delay;
|
||||
}
|
||||
}
|
||||
return crate::HttpRetryDecision::RetryAfter(delay);
|
||||
}
|
||||
|
||||
pub(crate) fn retry_backoff(settings: &crate::HttpRetrySettings, retry_number: u32) -> std::time::Duration {
|
||||
let mut delay = settings.initial_backoff();
|
||||
if retry_number <= 1 {
|
||||
return std::cmp::min(delay, settings.max_backoff());
|
||||
}
|
||||
let mut step = 1_u32;
|
||||
while step < retry_number {
|
||||
let doubled = match delay.checked_mul(2) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => settings.max_backoff(),
|
||||
};
|
||||
delay = std::cmp::min(doubled, settings.max_backoff());
|
||||
if delay >= settings.max_backoff() {
|
||||
return settings.max_backoff();
|
||||
}
|
||||
step = step.saturating_add(1);
|
||||
}
|
||||
return delay;
|
||||
}
|
||||
|
||||
/// Crate-internal `HttpRoleRuntime` state shared across the owning crate.
|
||||
pub(crate) struct HttpRoleRuntime {
|
||||
limits: crate::HttpRoleLimits,
|
||||
bucket: std::sync::Mutex<std::option::Option<HttpTokenBucketState>>,
|
||||
@@ -139,6 +85,7 @@ pub(crate) struct HttpRoleRuntime {
|
||||
}
|
||||
|
||||
impl HttpRoleRuntime {
|
||||
/// Creates a new `HttpRoleRuntime` value.
|
||||
pub(crate) fn new(settings: &crate::HttpEndpointRoleSettings, notify: std::sync::Arc<tokio::sync::Notify>) -> Self {
|
||||
let bucket = match settings.limits().requests_per_second() {
|
||||
std::option::Option::Some(requests_per_second) => {
|
||||
@@ -169,6 +116,7 @@ impl HttpRoleRuntime {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the current availability.
|
||||
pub(crate) fn availability(&self, now: std::time::Instant) -> crate::HttpEndpointAvailability {
|
||||
if self.cooldown_remaining_at(now).is_some() {
|
||||
return crate::HttpEndpointAvailability::RateLimited;
|
||||
@@ -179,14 +127,17 @@ impl HttpRoleRuntime {
|
||||
return crate::HttpEndpointAvailability::Available;
|
||||
}
|
||||
|
||||
/// Returns the current cooldown remaining.
|
||||
pub(crate) fn cooldown_remaining(&self) -> std::option::Option<std::time::Duration> {
|
||||
return self.cooldown_remaining_at(std::time::Instant::now());
|
||||
}
|
||||
|
||||
/// Returns the current max concurrent requests.
|
||||
pub(crate) fn max_concurrent_requests(&self) -> std::option::Option<u32> {
|
||||
return self.limits.max_concurrent_requests().map(|value| return value.get());
|
||||
}
|
||||
|
||||
/// Returns the current in flight requests.
|
||||
pub(crate) fn in_flight_requests(&self) -> std::option::Option<u32> {
|
||||
let semaphore = match &self.semaphore {
|
||||
std::option::Option::Some(value) => value,
|
||||
@@ -204,33 +155,37 @@ impl HttpRoleRuntime {
|
||||
return std::option::Option::Some(maximum.saturating_sub(available_u32));
|
||||
}
|
||||
|
||||
/// Returns the current success count.
|
||||
pub(crate) fn success_count(&self) -> u64 {
|
||||
return self.success_count.load(std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Returns the current failure count.
|
||||
pub(crate) fn failure_count(&self) -> u64 {
|
||||
return self.failure_count.load(std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Returns the current rate limit count.
|
||||
pub(crate) fn rate_limit_count(&self) -> u64 {
|
||||
return self.rate_limit_count.load(std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn try_acquire(self: &std::sync::Arc<Self>, now: std::time::Instant) -> RoleAdmissionAttempt {
|
||||
/// Attempts to acquire.
|
||||
pub(crate) fn try_acquire(self: &std::sync::Arc<Self>, now: std::time::Instant) -> crate::RoleAdmissionAttempt {
|
||||
if let std::option::Option::Some(remaining) = self.cooldown_remaining_at(now) {
|
||||
let ready_at = match now.checked_add(remaining) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => now,
|
||||
};
|
||||
return RoleAdmissionAttempt::BlockedUntil(ready_at);
|
||||
return crate::RoleAdmissionAttempt::BlockedUntil(ready_at);
|
||||
}
|
||||
let semaphore_permit = match &self.semaphore {
|
||||
std::option::Option::Some(semaphore) => {
|
||||
let permit_result = std::sync::Arc::clone(semaphore).try_acquire_owned();
|
||||
match permit_result {
|
||||
std::result::Result::Ok(permit) => std::option::Option::Some(permit),
|
||||
std::result::Result::Err(tokio::sync::TryAcquireError::NoPermits) => return RoleAdmissionAttempt::ConcurrencySaturated,
|
||||
std::result::Result::Err(tokio::sync::TryAcquireError::Closed) => return RoleAdmissionAttempt::Unavailable,
|
||||
std::result::Result::Err(tokio::sync::TryAcquireError::NoPermits) => return crate::RoleAdmissionAttempt::ConcurrencySaturated,
|
||||
std::result::Result::Err(tokio::sync::TryAcquireError::Closed) => return crate::RoleAdmissionAttempt::Unavailable,
|
||||
}
|
||||
},
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
@@ -239,11 +194,12 @@ impl HttpRoleRuntime {
|
||||
if let std::option::Option::Some(ready_at) = token_result {
|
||||
drop(semaphore_permit);
|
||||
self.notify.notify_one();
|
||||
return RoleAdmissionAttempt::BlockedUntil(ready_at);
|
||||
return crate::RoleAdmissionAttempt::BlockedUntil(ready_at);
|
||||
}
|
||||
return RoleAdmissionAttempt::Ready(HttpConcurrencyPermit { semaphore_permit, notify: std::sync::Arc::clone(&self.notify) });
|
||||
return crate::RoleAdmissionAttempt::Ready(HttpConcurrencyPermit { semaphore_permit, notify: std::sync::Arc::clone(&self.notify) });
|
||||
}
|
||||
|
||||
/// Records success.
|
||||
pub(crate) fn record_success(&self) {
|
||||
self.success_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
self.degraded.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
@@ -251,6 +207,7 @@ impl HttpRoleRuntime {
|
||||
return;
|
||||
}
|
||||
|
||||
/// Records failure.
|
||||
pub(crate) fn record_failure(&self) {
|
||||
self.failure_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
self.degraded.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
@@ -258,6 +215,7 @@ impl HttpRoleRuntime {
|
||||
return;
|
||||
}
|
||||
|
||||
/// Records rate limited.
|
||||
pub(crate) fn record_rate_limited(&self, provider_retry_after: std::option::Option<std::time::Duration>) -> std::time::Duration {
|
||||
self.failure_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
self.rate_limit_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
@@ -324,6 +282,7 @@ impl HttpRoleRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// Crate-internal `RoleAdmissionAttempt` variants used by the owning crate.
|
||||
pub(crate) enum RoleAdmissionAttempt {
|
||||
Ready(HttpConcurrencyPermit),
|
||||
BlockedUntil(std::time::Instant),
|
||||
@@ -331,6 +290,7 @@ pub(crate) enum RoleAdmissionAttempt {
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
/// Crate-internal `HttpConcurrencyPermit` state shared across the owning crate.
|
||||
pub(crate) struct HttpConcurrencyPermit {
|
||||
semaphore_permit: std::option::Option<tokio::sync::OwnedSemaphorePermit>,
|
||||
notify: std::sync::Arc<tokio::sync::Notify>,
|
||||
@@ -385,6 +345,61 @@ impl HttpTokenBucketState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method.
|
||||
///
|
||||
/// `completed_retries` counts retries already performed after the initial attempt. Provider `Retry-After` values are defensively bounded to sixty seconds
|
||||
/// before they can extend the local exponential backoff. RPC application errors are never converted into transport retries.
|
||||
#[must_use]
|
||||
pub fn evaluate_transport_retry(
|
||||
method: &crate::HttpRpcMethodDescriptor,
|
||||
settings: &crate::HttpRetrySettings,
|
||||
cause: crate::HttpRetryCause,
|
||||
dispatch_state: crate::HttpDispatchState,
|
||||
completed_retries: u32,
|
||||
provider_retry_after: std::option::Option<std::time::Duration>,
|
||||
) -> crate::HttpRetryDecision {
|
||||
if completed_retries >= settings.max_retries() || !cause.is_retryable() {
|
||||
return crate::HttpRetryDecision::Stop;
|
||||
}
|
||||
if method.transport_retry_class() == crate::TransportRetryClass::NotApplicable {
|
||||
return crate::HttpRetryDecision::Stop;
|
||||
}
|
||||
if method.transport_retry_class() == crate::TransportRetryClass::NeverAfterDispatch && dispatch_state == crate::HttpDispatchState::DispatchedAmbiguous {
|
||||
return crate::HttpRetryDecision::Stop;
|
||||
}
|
||||
let retry_number = completed_retries.saturating_add(1);
|
||||
let mut delay = retry_backoff(settings, retry_number);
|
||||
if cause == crate::HttpRetryCause::RateLimited
|
||||
&& let std::option::Option::Some(provider_delay) = provider_retry_after
|
||||
{
|
||||
let bounded_provider_delay = std::cmp::min(provider_delay, MAX_PROVIDER_RETRY_AFTER);
|
||||
if bounded_provider_delay > delay {
|
||||
delay = bounded_provider_delay;
|
||||
}
|
||||
}
|
||||
return crate::HttpRetryDecision::RetryAfter(delay);
|
||||
}
|
||||
|
||||
fn retry_backoff(settings: &crate::HttpRetrySettings, retry_number: u32) -> std::time::Duration {
|
||||
let mut delay = settings.initial_backoff();
|
||||
if retry_number <= 1 {
|
||||
return std::cmp::min(delay, settings.max_backoff());
|
||||
}
|
||||
let mut step = 1_u32;
|
||||
while step < retry_number {
|
||||
let doubled = match delay.checked_mul(2) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => settings.max_backoff(),
|
||||
};
|
||||
delay = std::cmp::min(doubled, settings.max_backoff());
|
||||
if delay >= settings.max_backoff() {
|
||||
return settings.max_backoff();
|
||||
}
|
||||
step = step.saturating_add(1);
|
||||
}
|
||||
return delay;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/resilience.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_accounts.rs
|
||||
// version: 4
|
||||
// version: 6
|
||||
|
||||
const MAX_MEMCMP_BYTES: usize = 128;
|
||||
const MAX_MULTIPLE_ACCOUNTS: usize = 100;
|
||||
const MAX_PROGRAM_ACCOUNT_FILTERS: usize = 4;
|
||||
|
||||
/// Account-data encoding accepted by Solana HTTP account methods.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -116,6 +120,7 @@ impl SolanaAccountInfoConfig {
|
||||
return self.context.min_context_slot();
|
||||
}
|
||||
|
||||
/// Returns whether empty.
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
return self.encoding.is_none() && self.data_slice.is_none() && self.commitment().is_none() && self.min_context_slot().is_none();
|
||||
}
|
||||
@@ -592,10 +597,6 @@ pub enum SolanaProgramAccountsResult {
|
||||
Context(crate::SolanaRpcResponse<std::vec::Vec<crate::SolanaKeyedAccount>>),
|
||||
}
|
||||
|
||||
const MAX_MULTIPLE_ACCOUNTS: usize = 100;
|
||||
const MAX_PROGRAM_ACCOUNT_FILTERS: usize = 4;
|
||||
const MAX_MEMCMP_BYTES: usize = 128;
|
||||
|
||||
impl crate::HttpTransportPool {
|
||||
/// Executes typed `getAccountInfo` through the common KSP HTTP transport path.
|
||||
pub async fn get_account_info(
|
||||
@@ -752,6 +753,58 @@ impl crate::HttpTransportPool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcResponse<T> {
|
||||
context: serde_json::Value,
|
||||
value: T,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum WireProgramAccountsResult {
|
||||
Context(WireRpcResponse<std::vec::Vec<serde_json::Value>>),
|
||||
Accounts(std::vec::Vec<serde_json::Value>),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum WireAccountData {
|
||||
LegacyBinary(std::string::String),
|
||||
JsonParsed(WireParsedAccountData),
|
||||
Encoded((std::string::String, std::string::String)),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireParsedAccountData {
|
||||
program: std::string::String,
|
||||
parsed: serde_json::Value,
|
||||
space: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireAccount {
|
||||
lamports: u64,
|
||||
data: serde_json::Value,
|
||||
owner: std::string::String,
|
||||
executable: bool,
|
||||
#[serde(rename = "rentEpoch")]
|
||||
rent_epoch: u64,
|
||||
#[serde(default)]
|
||||
space: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireKeyedAccount {
|
||||
pubkey: std::string::String,
|
||||
account: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireAccountBalance {
|
||||
address: std::string::String,
|
||||
lamports: u64,
|
||||
}
|
||||
|
||||
fn account_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
|
||||
let descriptor = crate::find_http_rpc_method(method);
|
||||
return match descriptor {
|
||||
@@ -934,58 +987,6 @@ fn decode_keyed_accounts(method: &str, values: std::vec::Vec<serde_json::Value>)
|
||||
return std::result::Result::Ok(decoded_values);
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcResponse<T> {
|
||||
context: serde_json::Value,
|
||||
value: T,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum WireProgramAccountsResult {
|
||||
Context(WireRpcResponse<std::vec::Vec<serde_json::Value>>),
|
||||
Accounts(std::vec::Vec<serde_json::Value>),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum WireAccountData {
|
||||
LegacyBinary(std::string::String),
|
||||
JsonParsed(WireParsedAccountData),
|
||||
Encoded((std::string::String, std::string::String)),
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireParsedAccountData {
|
||||
program: std::string::String,
|
||||
parsed: serde_json::Value,
|
||||
space: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireAccount {
|
||||
lamports: u64,
|
||||
data: serde_json::Value,
|
||||
owner: std::string::String,
|
||||
executable: bool,
|
||||
#[serde(rename = "rentEpoch")]
|
||||
rent_epoch: u64,
|
||||
#[serde(default)]
|
||||
space: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireKeyedAccount {
|
||||
pubkey: std::string::String,
|
||||
account: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireAccountBalance {
|
||||
address: std::string::String,
|
||||
lamports: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_accounts.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_blocks.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
const MAX_GET_BLOCKS_RANGE: u64 = 500_000;
|
||||
const MAX_GET_RECENT_PERFORMANCE_SAMPLES: u64 = 720;
|
||||
|
||||
/// Transaction detail level accepted by modern `getBlock` requests.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||
@@ -828,8 +831,85 @@ impl crate::HttpTransportPool {
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_GET_BLOCKS_RANGE: u64 = 500_000;
|
||||
const MAX_GET_RECENT_PERFORMANCE_SAMPLES: u64 = 720;
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireBlockCommitment {
|
||||
commitment: std::option::Option<std::vec::Vec<u64>>,
|
||||
total_stake: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireBlockProductionRange {
|
||||
first_slot: u64,
|
||||
last_slot: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireBlockProduction {
|
||||
by_identity: std::collections::BTreeMap<std::string::String, (usize, usize)>,
|
||||
range: WireBlockProductionRange,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireBlockProductionRpcResponse {
|
||||
context: serde_json::Value,
|
||||
value: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireBlockReward {
|
||||
pubkey: std::string::String,
|
||||
lamports: i64,
|
||||
post_balance: u64,
|
||||
#[serde(default)]
|
||||
reward_type: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
commission: std::option::Option<u8>,
|
||||
#[serde(default)]
|
||||
commission_bps: crate::SolanaWireField<u16>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireBlockTransaction {
|
||||
transaction: serde_json::Value,
|
||||
#[serde(default)]
|
||||
meta: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
version: crate::SolanaWireField<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireConfirmedBlock {
|
||||
previous_blockhash: std::string::String,
|
||||
blockhash: std::string::String,
|
||||
parent_slot: u64,
|
||||
#[serde(default)]
|
||||
transactions: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
|
||||
#[serde(default)]
|
||||
signatures: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
|
||||
#[serde(default)]
|
||||
rewards: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
|
||||
#[serde(default)]
|
||||
num_reward_partitions: crate::SolanaWireField<u64>,
|
||||
block_time: std::option::Option<i64>,
|
||||
block_height: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WirePerformanceSample {
|
||||
slot: u64,
|
||||
num_transactions: u64,
|
||||
#[serde(default)]
|
||||
num_non_vote_transactions: std::option::Option<u64>,
|
||||
num_slots: u64,
|
||||
sample_period_secs: u16,
|
||||
}
|
||||
|
||||
fn validate_blocks_context_commitment(method: &'static str, config: std::option::Option<&crate::SolanaContextConfig>) -> ksp_core_lib::Result<()> {
|
||||
if let std::option::Option::Some(config) = config
|
||||
@@ -978,86 +1058,6 @@ fn decode_block_rewards(
|
||||
return std::result::Result::Ok(crate::SolanaWireField::Value(rewards));
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireBlockCommitment {
|
||||
commitment: std::option::Option<std::vec::Vec<u64>>,
|
||||
total_stake: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireBlockProductionRange {
|
||||
first_slot: u64,
|
||||
last_slot: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireBlockProduction {
|
||||
by_identity: std::collections::BTreeMap<std::string::String, (usize, usize)>,
|
||||
range: WireBlockProductionRange,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireBlockProductionRpcResponse {
|
||||
context: serde_json::Value,
|
||||
value: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireBlockReward {
|
||||
pubkey: std::string::String,
|
||||
lamports: i64,
|
||||
post_balance: u64,
|
||||
#[serde(default)]
|
||||
reward_type: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
commission: std::option::Option<u8>,
|
||||
#[serde(default)]
|
||||
commission_bps: crate::SolanaWireField<u16>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireBlockTransaction {
|
||||
transaction: serde_json::Value,
|
||||
#[serde(default)]
|
||||
meta: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
version: crate::SolanaWireField<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireConfirmedBlock {
|
||||
previous_blockhash: std::string::String,
|
||||
blockhash: std::string::String,
|
||||
parent_slot: u64,
|
||||
#[serde(default)]
|
||||
transactions: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
|
||||
#[serde(default)]
|
||||
signatures: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
|
||||
#[serde(default)]
|
||||
rewards: crate::SolanaWireField<std::vec::Vec<serde_json::Value>>,
|
||||
#[serde(default)]
|
||||
num_reward_partitions: crate::SolanaWireField<u64>,
|
||||
block_time: std::option::Option<i64>,
|
||||
block_height: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WirePerformanceSample {
|
||||
slot: u64,
|
||||
num_transactions: u64,
|
||||
#[serde(default)]
|
||||
num_non_vote_transactions: std::option::Option<u64>,
|
||||
num_slots: u64,
|
||||
sample_period_secs: u16,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_blocks.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_cluster.rs
|
||||
// version: 4
|
||||
// version: 7
|
||||
|
||||
const MAX_GET_SLOT_LEADERS: u64 = 5_000;
|
||||
|
||||
@@ -29,71 +29,85 @@ impl SolanaClusterNode {
|
||||
pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.pubkey;
|
||||
}
|
||||
|
||||
/// Returns the optional feature-set identifier.
|
||||
#[must_use]
|
||||
pub const fn feature_set(&self) -> std::option::Option<u32> {
|
||||
return self.feature_set;
|
||||
}
|
||||
|
||||
/// Returns the optional gossip endpoint.
|
||||
#[must_use]
|
||||
pub fn gossip(&self) -> std::option::Option<&str> {
|
||||
return self.gossip.as_deref();
|
||||
}
|
||||
|
||||
/// Returns the optional PubSub endpoint.
|
||||
#[must_use]
|
||||
pub fn pubsub(&self) -> std::option::Option<&str> {
|
||||
return self.pubsub.as_deref();
|
||||
}
|
||||
|
||||
/// Returns the optional JSON-RPC endpoint.
|
||||
#[must_use]
|
||||
pub fn rpc(&self) -> std::option::Option<&str> {
|
||||
return self.rpc.as_deref();
|
||||
}
|
||||
|
||||
/// Returns the optional repair endpoint.
|
||||
#[must_use]
|
||||
pub fn serve_repair(&self) -> std::option::Option<&str> {
|
||||
return self.serve_repair.as_deref();
|
||||
}
|
||||
|
||||
/// Returns the optional shred version.
|
||||
#[must_use]
|
||||
pub const fn shred_version(&self) -> std::option::Option<u16> {
|
||||
return self.shred_version;
|
||||
}
|
||||
|
||||
/// Returns the optional TPU endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu(&self) -> std::option::Option<&str> {
|
||||
return self.tpu.as_deref();
|
||||
}
|
||||
|
||||
/// Returns the optional TPU forwards endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu_forwards(&self) -> std::option::Option<&str> {
|
||||
return self.tpu_forwards.as_deref();
|
||||
}
|
||||
|
||||
/// Returns the optional TPU forwards QUIC endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu_forwards_quic(&self) -> std::option::Option<&str> {
|
||||
return self.tpu_forwards_quic.as_deref();
|
||||
}
|
||||
|
||||
/// Returns the optional TPU QUIC endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu_quic(&self) -> std::option::Option<&str> {
|
||||
return self.tpu_quic.as_deref();
|
||||
}
|
||||
|
||||
/// Returns the optional TPU vote endpoint.
|
||||
#[must_use]
|
||||
pub fn tpu_vote(&self) -> std::option::Option<&str> {
|
||||
return self.tpu_vote.as_deref();
|
||||
}
|
||||
|
||||
/// Returns the optional TVU endpoint.
|
||||
#[must_use]
|
||||
pub fn tvu(&self) -> std::option::Option<&str> {
|
||||
return self.tvu.as_deref();
|
||||
}
|
||||
|
||||
/// Returns the optional software-version string.
|
||||
#[must_use]
|
||||
pub fn version(&self) -> std::option::Option<&str> {
|
||||
return self.version.as_deref();
|
||||
}
|
||||
|
||||
/// Returns the optional Agave client identifier extension.
|
||||
#[must_use]
|
||||
pub fn client_id(&self) -> std::option::Option<&str> {
|
||||
@@ -149,26 +163,31 @@ impl SolanaEpochInfo {
|
||||
pub const fn absolute_slot(&self) -> u64 {
|
||||
return self.absolute_slot;
|
||||
}
|
||||
|
||||
/// Returns the block height.
|
||||
#[must_use]
|
||||
pub const fn block_height(&self) -> u64 {
|
||||
return self.block_height;
|
||||
}
|
||||
|
||||
/// Returns the epoch number.
|
||||
#[must_use]
|
||||
pub const fn epoch(&self) -> u64 {
|
||||
return self.epoch;
|
||||
}
|
||||
|
||||
/// Returns the slot index within the epoch.
|
||||
#[must_use]
|
||||
pub const fn slot_index(&self) -> u64 {
|
||||
return self.slot_index;
|
||||
}
|
||||
|
||||
/// Returns the number of slots in the epoch.
|
||||
#[must_use]
|
||||
pub const fn slots_in_epoch(&self) -> u64 {
|
||||
return self.slots_in_epoch;
|
||||
}
|
||||
|
||||
/// Returns the nullable transaction count.
|
||||
#[must_use]
|
||||
pub const fn transaction_count(&self) -> std::option::Option<u64> {
|
||||
@@ -208,21 +227,25 @@ impl SolanaEpochSchedule {
|
||||
pub const fn first_normal_epoch(&self) -> u64 {
|
||||
return self.first_normal_epoch;
|
||||
}
|
||||
|
||||
/// Returns the first normal slot.
|
||||
#[must_use]
|
||||
pub const fn first_normal_slot(&self) -> u64 {
|
||||
return self.first_normal_slot;
|
||||
}
|
||||
|
||||
/// Returns the leader-schedule slot offset.
|
||||
#[must_use]
|
||||
pub const fn leader_schedule_slot_offset(&self) -> u64 {
|
||||
return self.leader_schedule_slot_offset;
|
||||
}
|
||||
|
||||
/// Returns the number of slots per epoch.
|
||||
#[must_use]
|
||||
pub const fn slots_per_epoch(&self) -> u64 {
|
||||
return self.slots_per_epoch;
|
||||
}
|
||||
|
||||
/// Returns whether epoch warmup is enabled.
|
||||
#[must_use]
|
||||
pub const fn warmup(&self) -> bool {
|
||||
@@ -258,6 +281,7 @@ impl SolanaSnapshotSlotInfo {
|
||||
pub const fn full(&self) -> u64 {
|
||||
return self.full;
|
||||
}
|
||||
|
||||
/// Returns the optional highest incremental snapshot slot.
|
||||
#[must_use]
|
||||
pub const fn incremental(&self) -> std::option::Option<u64> {
|
||||
@@ -287,19 +311,25 @@ impl SolanaLeaderScheduleConfig {
|
||||
pub const fn new(identity: std::option::Option<ksp_core_lib::Pubkey>, commitment: std::option::Option<crate::SolanaCommitment>) -> Self {
|
||||
return Self { identity, commitment };
|
||||
}
|
||||
|
||||
/// Returns the optional validator identity filter.
|
||||
#[must_use]
|
||||
pub const fn identity(&self) -> std::option::Option<&ksp_core_lib::Pubkey> {
|
||||
return self.identity.as_ref();
|
||||
}
|
||||
|
||||
/// Returns the optional commitment level.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.commitment;
|
||||
}
|
||||
|
||||
/// Returns whether empty.
|
||||
pub(crate) const fn is_empty(&self) -> bool {
|
||||
return self.identity.is_none() && self.commitment.is_none();
|
||||
}
|
||||
|
||||
/// Executes the crate-internal to json value operation for `SolanaLeaderScheduleConfig`.
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
let mut object = serde_json::Map::new();
|
||||
if let std::option::Option::Some(identity) = self.identity.as_ref() {
|
||||
@@ -400,29 +430,36 @@ impl SolanaVoteAccountsConfig {
|
||||
) -> Self {
|
||||
return Self { commitment, vote_pubkey, keep_unstaked_delinquents, delinquent_slot_distance };
|
||||
}
|
||||
|
||||
/// Returns the optional commitment.
|
||||
#[must_use]
|
||||
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||
return self.commitment;
|
||||
}
|
||||
|
||||
/// Returns the optional vote-account public key filter.
|
||||
#[must_use]
|
||||
pub const fn vote_pubkey(&self) -> std::option::Option<&ksp_core_lib::Pubkey> {
|
||||
return self.vote_pubkey.as_ref();
|
||||
}
|
||||
|
||||
/// Returns whether unstaked delinquent validators should be kept.
|
||||
#[must_use]
|
||||
pub const fn keep_unstaked_delinquents(&self) -> std::option::Option<bool> {
|
||||
return self.keep_unstaked_delinquents;
|
||||
}
|
||||
|
||||
/// Returns the optional delinquent slot distance.
|
||||
#[must_use]
|
||||
pub const fn delinquent_slot_distance(&self) -> std::option::Option<u64> {
|
||||
return self.delinquent_slot_distance;
|
||||
}
|
||||
|
||||
/// Returns whether empty.
|
||||
pub(crate) const fn is_empty(&self) -> bool {
|
||||
return self.commitment.is_none() && self.vote_pubkey.is_none() && self.keep_unstaked_delinquents.is_none() && self.delinquent_slot_distance.is_none();
|
||||
}
|
||||
|
||||
/// Serializes this config to the Solana JSON-RPC wire object.
|
||||
#[must_use]
|
||||
pub(crate) fn to_json_value(&self) -> serde_json::Value {
|
||||
@@ -457,11 +494,13 @@ impl SolanaEpochCredits {
|
||||
pub const fn epoch(&self) -> u64 {
|
||||
return self.epoch;
|
||||
}
|
||||
|
||||
/// Returns cumulative credits at the end of the epoch.
|
||||
#[must_use]
|
||||
pub const fn credits(&self) -> u64 {
|
||||
return self.credits;
|
||||
}
|
||||
|
||||
/// Returns cumulative credits before the epoch.
|
||||
#[must_use]
|
||||
pub const fn previous_credits(&self) -> u64 {
|
||||
@@ -489,41 +528,49 @@ impl SolanaVoteAccountInfo {
|
||||
pub const fn vote_pubkey(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.vote_pubkey;
|
||||
}
|
||||
|
||||
/// Returns the validator identity public key.
|
||||
#[must_use]
|
||||
pub const fn node_pubkey(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.node_pubkey;
|
||||
}
|
||||
|
||||
/// Returns the activated stake in lamports.
|
||||
#[must_use]
|
||||
pub const fn activated_stake(&self) -> u64 {
|
||||
return self.activated_stake;
|
||||
}
|
||||
|
||||
/// Returns the legacy/effective percentage commission field.
|
||||
#[must_use]
|
||||
pub const fn commission(&self) -> u8 {
|
||||
return self.commission;
|
||||
}
|
||||
|
||||
/// Returns the optional raw inflation-rewards commission in basis points.
|
||||
#[must_use]
|
||||
pub const fn inflation_rewards_commission_bps(&self) -> std::option::Option<u16> {
|
||||
return self.inflation_rewards_commission_bps;
|
||||
}
|
||||
|
||||
/// Returns whether the vote account is staked for the current epoch.
|
||||
#[must_use]
|
||||
pub const fn epoch_vote_account(&self) -> bool {
|
||||
return self.epoch_vote_account;
|
||||
}
|
||||
|
||||
/// Returns the bounded RPC epoch-credit history.
|
||||
#[must_use]
|
||||
pub fn epoch_credits(&self) -> &[crate::SolanaEpochCredits] {
|
||||
return self.epoch_credits.as_slice();
|
||||
}
|
||||
|
||||
/// Returns the latest voted slot or zero when no vote exists.
|
||||
#[must_use]
|
||||
pub const fn last_vote(&self) -> u64 {
|
||||
return self.last_vote;
|
||||
}
|
||||
|
||||
/// Returns the current root slot or zero when no root exists.
|
||||
#[must_use]
|
||||
pub const fn root_slot(&self) -> u64 {
|
||||
@@ -578,6 +625,7 @@ impl SolanaVoteAccountStatus {
|
||||
pub fn current(&self) -> &[crate::SolanaVoteAccountInfo] {
|
||||
return self.current.as_slice();
|
||||
}
|
||||
|
||||
/// Returns delinquent vote accounts.
|
||||
#[must_use]
|
||||
pub fn delinquent(&self) -> &[crate::SolanaVoteAccountInfo] {
|
||||
@@ -807,55 +855,6 @@ impl crate::HttpTransportPool {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_context_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaContextConfig>) {
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& (config.commitment().is_some() || config.min_context_slot().is_some())
|
||||
{
|
||||
params.push((*config).to_json_value());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fn decode_pubkey_list(method: &str, field: &'static str, value: serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<ksp_core_lib::Pubkey>> {
|
||||
let decoded = crate::decode_wire_json::<std::vec::Vec<std::string::String>>(method, value);
|
||||
let values = match decoded {
|
||||
std::result::Result::Ok(values) => values,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut pubkeys = std::vec::Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
let pubkey = crate::parse_wire_pubkey(method, field, value.as_str());
|
||||
match pubkey {
|
||||
std::result::Result::Ok(pubkey) => pubkeys.push(pubkey),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(pubkeys);
|
||||
}
|
||||
|
||||
fn invalid_cluster_parameters<T>(method: &str, message: &str, field: &'static str, value: u64) -> ksp_core_lib::Result<T> {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message)
|
||||
.with_context("rpc_method", method)
|
||||
.with_context(field, value.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn cluster_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
|
||||
let descriptor = crate::find_http_rpc_method(method);
|
||||
return match descriptor {
|
||||
std::option::Option::Some(descriptor)
|
||||
if descriptor.category() == crate::HttpRpcCategory::Cluster && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_2 =>
|
||||
{
|
||||
std::result::Result::Ok(descriptor)
|
||||
},
|
||||
_ => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Cluster descriptor is missing from the audited 0.2.2 registry")
|
||||
.with_context("rpc_method", method),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireClusterNode {
|
||||
@@ -938,6 +937,55 @@ struct WireVoteAccountStatus {
|
||||
delinquent: std::vec::Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
fn push_context_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaContextConfig>) {
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& (config.commitment().is_some() || config.min_context_slot().is_some())
|
||||
{
|
||||
params.push((*config).to_json_value());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fn decode_pubkey_list(method: &str, field: &'static str, value: serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<ksp_core_lib::Pubkey>> {
|
||||
let decoded = crate::decode_wire_json::<std::vec::Vec<std::string::String>>(method, value);
|
||||
let values = match decoded {
|
||||
std::result::Result::Ok(values) => values,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut pubkeys = std::vec::Vec::with_capacity(values.len());
|
||||
for value in values {
|
||||
let pubkey = crate::parse_wire_pubkey(method, field, value.as_str());
|
||||
match pubkey {
|
||||
std::result::Result::Ok(pubkey) => pubkeys.push(pubkey),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(pubkeys);
|
||||
}
|
||||
|
||||
fn invalid_cluster_parameters<T>(method: &str, message: &str, field: &'static str, value: u64) -> ksp_core_lib::Result<T> {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, message)
|
||||
.with_context("rpc_method", method)
|
||||
.with_context(field, value.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn cluster_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::HttpRpcMethodDescriptor> {
|
||||
let descriptor = crate::find_http_rpc_method(method);
|
||||
return match descriptor {
|
||||
std::option::Option::Some(descriptor)
|
||||
if descriptor.category() == crate::HttpRpcCategory::Cluster && descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_2 =>
|
||||
{
|
||||
std::result::Result::Ok(descriptor)
|
||||
},
|
||||
_ => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, "typed Cluster descriptor is missing from the audited 0.2.2 registry")
|
||||
.with_context("rpc_method", method),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_cluster.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_common.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
/// Commitment level accepted by typed Solana HTTP RPC adapters.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -155,6 +155,13 @@ impl<T> SolanaRpcResponse<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcContext {
|
||||
slot: u64,
|
||||
#[serde(rename = "apiVersion", default)]
|
||||
api_version: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// Decodes one private serde wire type and maps shape failures to the shared Transport error domain.
|
||||
pub(crate) fn decode_wire_json<T: serde::de::DeserializeOwned>(method: &str, value: serde_json::Value) -> ksp_core_lib::Result<T> {
|
||||
let decoded = serde_json::from_value::<T>(value);
|
||||
@@ -181,13 +188,6 @@ pub(crate) fn parse_wire_pubkey(method: &str, field: &str, value: &str) -> ksp_c
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireRpcContext {
|
||||
slot: u64,
|
||||
#[serde(rename = "apiVersion", default)]
|
||||
api_version: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_common.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_economics.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
/// Inflation-governor values returned by `getInflationGovernor`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@@ -441,6 +441,53 @@ impl crate::HttpTransportPool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireEconomicsRpcResponse<T> {
|
||||
context: serde_json::Value,
|
||||
value: T,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireInflationGovernor {
|
||||
initial: f64,
|
||||
terminal: f64,
|
||||
taper: f64,
|
||||
foundation: f64,
|
||||
foundation_term: f64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireInflationRate {
|
||||
total: f64,
|
||||
validator: f64,
|
||||
foundation: f64,
|
||||
epoch: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireInflationReward {
|
||||
epoch: u64,
|
||||
effective_slot: u64,
|
||||
amount: u64,
|
||||
post_balance: u64,
|
||||
#[serde(default)]
|
||||
commission: std::option::Option<u8>,
|
||||
#[serde(default)]
|
||||
commission_bps: crate::SolanaWireField<u16>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireSupply {
|
||||
total: u64,
|
||||
circulating: u64,
|
||||
non_circulating: u64,
|
||||
non_circulating_accounts: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
fn push_economics_commitment_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaCommitmentConfig>) {
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& config.commitment().is_some()
|
||||
@@ -539,53 +586,6 @@ fn economics_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::Ht
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireEconomicsRpcResponse<T> {
|
||||
context: serde_json::Value,
|
||||
value: T,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireInflationGovernor {
|
||||
initial: f64,
|
||||
terminal: f64,
|
||||
taper: f64,
|
||||
foundation: f64,
|
||||
foundation_term: f64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireInflationRate {
|
||||
total: f64,
|
||||
validator: f64,
|
||||
foundation: f64,
|
||||
epoch: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireInflationReward {
|
||||
epoch: u64,
|
||||
effective_slot: u64,
|
||||
amount: u64,
|
||||
post_balance: u64,
|
||||
#[serde(default)]
|
||||
commission: std::option::Option<u8>,
|
||||
#[serde(default)]
|
||||
commission_bps: crate::SolanaWireField<u16>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireSupply {
|
||||
total: u64,
|
||||
circulating: u64,
|
||||
non_circulating: u64,
|
||||
non_circulating_accounts: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_economics.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,281 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_method.rs
|
||||
// version: 2
|
||||
|
||||
/// Functional category used by the audited Solana HTTP JSON-RPC registry.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum HttpRpcCategory {
|
||||
/// Account state and rent queries.
|
||||
Accounts,
|
||||
/// SPL token-oriented RPC queries exposed by the standard Solana HTTP surface.
|
||||
Tokens,
|
||||
/// Transaction, signature, fee, simulation and submission methods.
|
||||
Transactions,
|
||||
/// Block, slot-history and performance-sample methods.
|
||||
Blocks,
|
||||
/// Cluster identity, epoch, leader, health and validator methods.
|
||||
Cluster,
|
||||
/// Supply, inflation and stake-economics methods.
|
||||
Economics,
|
||||
/// Historically documented methods removed from the targeted Agave runtime generation.
|
||||
Historical,
|
||||
}
|
||||
|
||||
/// Documentation lifecycle status of one audited RPC method.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum RpcDocumentationStatus {
|
||||
/// The method is documented as stable.
|
||||
Stable,
|
||||
/// The method is documented as deprecated or obsolete.
|
||||
Deprecated,
|
||||
/// The method is documented as unstable or experimental.
|
||||
Unstable,
|
||||
}
|
||||
|
||||
impl RpcDocumentationStatus {
|
||||
/// Returns the stable machine-readable status code.
|
||||
#[must_use]
|
||||
pub const fn code(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Stable => "stable",
|
||||
Self::Deprecated => "deprecated",
|
||||
Self::Unstable => "unstable",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Runtime availability status of one audited RPC method.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum RpcRuntimeStatus {
|
||||
/// The targeted runtime generation still supports the method.
|
||||
Supported,
|
||||
/// The method is historically documented but removed from the targeted runtime generation.
|
||||
Removed,
|
||||
}
|
||||
|
||||
impl RpcRuntimeStatus {
|
||||
/// Returns the stable machine-readable runtime status code.
|
||||
#[must_use]
|
||||
pub const fn code(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Supported => "supported",
|
||||
Self::Removed => "removed",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Request-form policy attached to a stable RPC method.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum RpcRequestFormStatus {
|
||||
/// Only the currently documented stable request form is tracked by KSP.
|
||||
Stable,
|
||||
/// The method is stable but also has a documented deprecated legacy request form that must warn when explicitly used.
|
||||
StableWithDeprecatedLegacy,
|
||||
}
|
||||
|
||||
impl RpcRequestFormStatus {
|
||||
/// Returns whether the method has a documented deprecated legacy request form.
|
||||
#[must_use]
|
||||
pub const fn has_deprecated_legacy(self) -> bool {
|
||||
return match self {
|
||||
Self::Stable => false,
|
||||
Self::StableWithDeprecatedLegacy => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Technical operation kind used to separate reads, simulations and submissions.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum RpcOperationKind {
|
||||
/// Read-only RPC operation.
|
||||
Read,
|
||||
/// Simulation operation that does not submit a transaction for execution.
|
||||
Simulation,
|
||||
/// Technical write/submission operation whose ambiguous post-dispatch outcome must not be resent automatically.
|
||||
WriteSubmission,
|
||||
}
|
||||
|
||||
/// HTTP transport retry classification attached to an RPC method descriptor.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum TransportRetryClass {
|
||||
/// The identical transport request may be retried when the transport failure is classified as retryable.
|
||||
RetrySafe,
|
||||
/// The request must not be resent automatically after an ambiguous dispatch.
|
||||
NeverAfterDispatch,
|
||||
/// Retry classification does not apply because the method is not callable on the targeted runtime.
|
||||
NotApplicable,
|
||||
}
|
||||
|
||||
/// Release that owns the typed KSP coverage for one audited current HTTP method.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum HttpRpcCoverageRelease {
|
||||
/// `0.2.1` HTTP foundation and four canary methods.
|
||||
V0_2_1,
|
||||
/// `0.2.2` Accounts + Tokens + remaining Cluster methods.
|
||||
V0_2_2,
|
||||
/// `0.2.3` Transactions methods.
|
||||
V0_2_3,
|
||||
/// `0.2.4` Blocks + Economics methods and final HTTP compliance.
|
||||
V0_2_4,
|
||||
/// Historical registry entry with no callable typed release.
|
||||
Historical,
|
||||
}
|
||||
|
||||
/// Immutable audited descriptor for one Solana HTTP JSON-RPC method.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct HttpRpcMethodDescriptor {
|
||||
method: &'static str,
|
||||
category: crate::HttpRpcCategory,
|
||||
request_kind: &'static str,
|
||||
documentation_status: crate::RpcDocumentationStatus,
|
||||
runtime_status: crate::RpcRuntimeStatus,
|
||||
request_form_status: crate::RpcRequestFormStatus,
|
||||
operation_kind: crate::RpcOperationKind,
|
||||
transport_retry_class: crate::TransportRetryClass,
|
||||
replacement: std::option::Option<&'static str>,
|
||||
coverage_release: crate::HttpRpcCoverageRelease,
|
||||
}
|
||||
|
||||
impl HttpRpcMethodDescriptor {
|
||||
const fn new(
|
||||
method: &'static str,
|
||||
category: crate::HttpRpcCategory,
|
||||
request_kind: &'static str,
|
||||
documentation_status: crate::RpcDocumentationStatus,
|
||||
runtime_status: crate::RpcRuntimeStatus,
|
||||
request_form_status: crate::RpcRequestFormStatus,
|
||||
operation_kind: crate::RpcOperationKind,
|
||||
transport_retry_class: crate::TransportRetryClass,
|
||||
replacement: std::option::Option<&'static str>,
|
||||
coverage_release: crate::HttpRpcCoverageRelease,
|
||||
) -> Self {
|
||||
return Self {
|
||||
method,
|
||||
category,
|
||||
request_kind,
|
||||
documentation_status,
|
||||
runtime_status,
|
||||
request_form_status,
|
||||
operation_kind,
|
||||
transport_retry_class,
|
||||
replacement,
|
||||
coverage_release,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the exact JSON-RPC method name.
|
||||
#[must_use]
|
||||
pub const fn method(&self) -> &'static str {
|
||||
return self.method;
|
||||
}
|
||||
|
||||
/// Returns the audited functional category.
|
||||
#[must_use]
|
||||
pub const fn category(&self) -> crate::HttpRpcCategory {
|
||||
return self.category;
|
||||
}
|
||||
|
||||
/// Returns the stable open request-kind descriptor used by future endpoint role matching.
|
||||
#[must_use]
|
||||
pub const fn request_kind(&self) -> &'static str {
|
||||
return self.request_kind;
|
||||
}
|
||||
|
||||
/// Returns the method documentation lifecycle status.
|
||||
#[must_use]
|
||||
pub const fn documentation_status(&self) -> crate::RpcDocumentationStatus {
|
||||
return self.documentation_status;
|
||||
}
|
||||
|
||||
/// Returns runtime availability on the targeted Agave generation.
|
||||
#[must_use]
|
||||
pub const fn runtime_status(&self) -> crate::RpcRuntimeStatus {
|
||||
return self.runtime_status;
|
||||
}
|
||||
|
||||
/// Returns the request-form policy, including whether a documented deprecated legacy form exists.
|
||||
#[must_use]
|
||||
pub const fn request_form_status(&self) -> crate::RpcRequestFormStatus {
|
||||
return self.request_form_status;
|
||||
}
|
||||
|
||||
/// Returns the technical operation kind.
|
||||
#[must_use]
|
||||
pub const fn operation_kind(&self) -> crate::RpcOperationKind {
|
||||
return self.operation_kind;
|
||||
}
|
||||
|
||||
/// Returns the transport retry classification.
|
||||
#[must_use]
|
||||
pub const fn transport_retry_class(&self) -> crate::TransportRetryClass {
|
||||
return self.transport_retry_class;
|
||||
}
|
||||
|
||||
/// Returns the documented replacement or migration direction when one exists.
|
||||
#[must_use]
|
||||
pub const fn replacement(&self) -> std::option::Option<&'static str> {
|
||||
return self.replacement;
|
||||
}
|
||||
|
||||
/// Returns the release assigned to typed KSP coverage.
|
||||
#[must_use]
|
||||
pub const fn coverage_release(&self) -> crate::HttpRpcCoverageRelease {
|
||||
return self.coverage_release;
|
||||
}
|
||||
|
||||
/// Returns whether calling the method itself must emit a lifecycle warning when runtime support exists.
|
||||
#[must_use]
|
||||
pub const fn requires_method_usage_warning(&self) -> bool {
|
||||
return match self.runtime_status {
|
||||
crate::RpcRuntimeStatus::Removed => false,
|
||||
crate::RpcRuntimeStatus::Supported => match self.documentation_status {
|
||||
crate::RpcDocumentationStatus::Stable => false,
|
||||
crate::RpcDocumentationStatus::Deprecated | crate::RpcDocumentationStatus::Unstable => true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Checks runtime support and centrally emits the KSP warning required for deprecated, unstable or removed methods.
|
||||
///
|
||||
/// Removed methods return [`crate::ERROR_CODE_METHOD_REMOVED`] and are never presented as callable standard RPC operations.
|
||||
pub fn ensure_runtime_supported(&self) -> ksp_core_lib::Result<()> {
|
||||
if self.runtime_status == crate::RpcRuntimeStatus::Removed {
|
||||
let replacement = match self.replacement {
|
||||
std::option::Option::Some(replacement) => replacement,
|
||||
std::option::Option::None => "none",
|
||||
};
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
rpc_method = self.method,
|
||||
documentation_status = self.documentation_status.code(),
|
||||
runtime_status = self.runtime_status.code(),
|
||||
replacement,
|
||||
"removed Solana HTTP RPC method requested"
|
||||
);
|
||||
let mut error = ksp_core_lib::Error::new(crate::ERROR_CODE_METHOD_REMOVED, "Solana HTTP RPC method is removed from the targeted runtime")
|
||||
.with_context("rpc_method", self.method)
|
||||
.with_context("documentation_status", self.documentation_status.code());
|
||||
if let std::option::Option::Some(replacement) = self.replacement {
|
||||
error = error.with_context("replacement", replacement);
|
||||
}
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if self.requires_method_usage_warning() {
|
||||
let replacement = match self.replacement {
|
||||
std::option::Option::Some(replacement) => replacement,
|
||||
std::option::Option::None => "none",
|
||||
};
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
rpc_method = self.method,
|
||||
documentation_status = self.documentation_status.code(),
|
||||
runtime_status = self.runtime_status.code(),
|
||||
replacement,
|
||||
"non-stable Solana HTTP RPC method requested"
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
// version: 5
|
||||
|
||||
const CURRENT_HTTP_RPC_METHODS: [crate::HttpRpcMethodDescriptor; 52] = [
|
||||
crate::HttpRpcMethodDescriptor::new(
|
||||
@@ -1075,6 +799,282 @@ const HISTORICAL_HTTP_RPC_METHODS: [crate::HttpRpcMethodDescriptor; 14] = [
|
||||
),
|
||||
];
|
||||
|
||||
/// Functional category used by the audited Solana HTTP JSON-RPC registry.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum HttpRpcCategory {
|
||||
/// Account state and rent queries.
|
||||
Accounts,
|
||||
/// SPL token-oriented RPC queries exposed by the standard Solana HTTP surface.
|
||||
Tokens,
|
||||
/// Transaction, signature, fee, simulation and submission methods.
|
||||
Transactions,
|
||||
/// Block, slot-history and performance-sample methods.
|
||||
Blocks,
|
||||
/// Cluster identity, epoch, leader, health and validator methods.
|
||||
Cluster,
|
||||
/// Supply, inflation and stake-economics methods.
|
||||
Economics,
|
||||
/// Historically documented methods removed from the targeted Agave runtime generation.
|
||||
Historical,
|
||||
}
|
||||
|
||||
/// Documentation lifecycle status of one audited RPC method.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum RpcDocumentationStatus {
|
||||
/// The method is documented as stable.
|
||||
Stable,
|
||||
/// The method is documented as deprecated or obsolete.
|
||||
Deprecated,
|
||||
/// The method is documented as unstable or experimental.
|
||||
Unstable,
|
||||
}
|
||||
|
||||
impl RpcDocumentationStatus {
|
||||
/// Returns the stable machine-readable status code.
|
||||
#[must_use]
|
||||
pub const fn code(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Stable => "stable",
|
||||
Self::Deprecated => "deprecated",
|
||||
Self::Unstable => "unstable",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Runtime availability status of one audited RPC method.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum RpcRuntimeStatus {
|
||||
/// The targeted runtime generation still supports the method.
|
||||
Supported,
|
||||
/// The method is historically documented but removed from the targeted runtime generation.
|
||||
Removed,
|
||||
}
|
||||
|
||||
impl RpcRuntimeStatus {
|
||||
/// Returns the stable machine-readable runtime status code.
|
||||
#[must_use]
|
||||
pub const fn code(self) -> &'static str {
|
||||
return match self {
|
||||
Self::Supported => "supported",
|
||||
Self::Removed => "removed",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Request-form policy attached to a stable RPC method.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum RpcRequestFormStatus {
|
||||
/// Only the currently documented stable request form is tracked by KSP.
|
||||
Stable,
|
||||
/// The method is stable but also has a documented deprecated legacy request form that must warn when explicitly used.
|
||||
StableWithDeprecatedLegacy,
|
||||
}
|
||||
|
||||
impl RpcRequestFormStatus {
|
||||
/// Returns whether the method has a documented deprecated legacy request form.
|
||||
#[must_use]
|
||||
pub const fn has_deprecated_legacy(self) -> bool {
|
||||
return match self {
|
||||
Self::Stable => false,
|
||||
Self::StableWithDeprecatedLegacy => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Technical operation kind used to separate reads, simulations and submissions.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum RpcOperationKind {
|
||||
/// Read-only RPC operation.
|
||||
Read,
|
||||
/// Simulation operation that does not submit a transaction for execution.
|
||||
Simulation,
|
||||
/// Technical write/submission operation whose ambiguous post-dispatch outcome must not be resent automatically.
|
||||
WriteSubmission,
|
||||
}
|
||||
|
||||
/// HTTP transport retry classification attached to an RPC method descriptor.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum TransportRetryClass {
|
||||
/// The identical transport request may be retried when the transport failure is classified as retryable.
|
||||
RetrySafe,
|
||||
/// The request must not be resent automatically after an ambiguous dispatch.
|
||||
NeverAfterDispatch,
|
||||
/// Retry classification does not apply because the method is not callable on the targeted runtime.
|
||||
NotApplicable,
|
||||
}
|
||||
|
||||
/// Release that owns the typed KSP coverage for one audited current HTTP method.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum HttpRpcCoverageRelease {
|
||||
/// `0.2.1` HTTP foundation and four canary methods.
|
||||
V0_2_1,
|
||||
/// `0.2.2` Accounts + Tokens + remaining Cluster methods.
|
||||
V0_2_2,
|
||||
/// `0.2.3` Transactions methods.
|
||||
V0_2_3,
|
||||
/// `0.2.4` Blocks + Economics methods and final HTTP compliance.
|
||||
V0_2_4,
|
||||
/// Historical registry entry with no callable typed release.
|
||||
Historical,
|
||||
}
|
||||
|
||||
/// Immutable audited descriptor for one Solana HTTP JSON-RPC method.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct HttpRpcMethodDescriptor {
|
||||
method: &'static str,
|
||||
category: crate::HttpRpcCategory,
|
||||
request_kind: &'static str,
|
||||
documentation_status: crate::RpcDocumentationStatus,
|
||||
runtime_status: crate::RpcRuntimeStatus,
|
||||
request_form_status: crate::RpcRequestFormStatus,
|
||||
operation_kind: crate::RpcOperationKind,
|
||||
transport_retry_class: crate::TransportRetryClass,
|
||||
replacement: std::option::Option<&'static str>,
|
||||
coverage_release: crate::HttpRpcCoverageRelease,
|
||||
}
|
||||
|
||||
impl HttpRpcMethodDescriptor {
|
||||
const fn new(
|
||||
method: &'static str,
|
||||
category: crate::HttpRpcCategory,
|
||||
request_kind: &'static str,
|
||||
documentation_status: crate::RpcDocumentationStatus,
|
||||
runtime_status: crate::RpcRuntimeStatus,
|
||||
request_form_status: crate::RpcRequestFormStatus,
|
||||
operation_kind: crate::RpcOperationKind,
|
||||
transport_retry_class: crate::TransportRetryClass,
|
||||
replacement: std::option::Option<&'static str>,
|
||||
coverage_release: crate::HttpRpcCoverageRelease,
|
||||
) -> Self {
|
||||
return Self {
|
||||
method,
|
||||
category,
|
||||
request_kind,
|
||||
documentation_status,
|
||||
runtime_status,
|
||||
request_form_status,
|
||||
operation_kind,
|
||||
transport_retry_class,
|
||||
replacement,
|
||||
coverage_release,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the exact JSON-RPC method name.
|
||||
#[must_use]
|
||||
pub const fn method(&self) -> &'static str {
|
||||
return self.method;
|
||||
}
|
||||
|
||||
/// Returns the audited functional category.
|
||||
#[must_use]
|
||||
pub const fn category(&self) -> crate::HttpRpcCategory {
|
||||
return self.category;
|
||||
}
|
||||
|
||||
/// Returns the stable open request-kind descriptor used by future endpoint role matching.
|
||||
#[must_use]
|
||||
pub const fn request_kind(&self) -> &'static str {
|
||||
return self.request_kind;
|
||||
}
|
||||
|
||||
/// Returns the method documentation lifecycle status.
|
||||
#[must_use]
|
||||
pub const fn documentation_status(&self) -> crate::RpcDocumentationStatus {
|
||||
return self.documentation_status;
|
||||
}
|
||||
|
||||
/// Returns runtime availability on the targeted Agave generation.
|
||||
#[must_use]
|
||||
pub const fn runtime_status(&self) -> crate::RpcRuntimeStatus {
|
||||
return self.runtime_status;
|
||||
}
|
||||
|
||||
/// Returns the request-form policy, including whether a documented deprecated legacy form exists.
|
||||
#[must_use]
|
||||
pub const fn request_form_status(&self) -> crate::RpcRequestFormStatus {
|
||||
return self.request_form_status;
|
||||
}
|
||||
|
||||
/// Returns the technical operation kind.
|
||||
#[must_use]
|
||||
pub const fn operation_kind(&self) -> crate::RpcOperationKind {
|
||||
return self.operation_kind;
|
||||
}
|
||||
|
||||
/// Returns the transport retry classification.
|
||||
#[must_use]
|
||||
pub const fn transport_retry_class(&self) -> crate::TransportRetryClass {
|
||||
return self.transport_retry_class;
|
||||
}
|
||||
|
||||
/// Returns the documented replacement or migration direction when one exists.
|
||||
#[must_use]
|
||||
pub const fn replacement(&self) -> std::option::Option<&'static str> {
|
||||
return self.replacement;
|
||||
}
|
||||
|
||||
/// Returns the release assigned to typed KSP coverage.
|
||||
#[must_use]
|
||||
pub const fn coverage_release(&self) -> crate::HttpRpcCoverageRelease {
|
||||
return self.coverage_release;
|
||||
}
|
||||
|
||||
/// Returns whether calling the method itself must emit a lifecycle warning when runtime support exists.
|
||||
#[must_use]
|
||||
pub const fn requires_method_usage_warning(&self) -> bool {
|
||||
return match self.runtime_status {
|
||||
crate::RpcRuntimeStatus::Removed => false,
|
||||
crate::RpcRuntimeStatus::Supported => match self.documentation_status {
|
||||
crate::RpcDocumentationStatus::Stable => false,
|
||||
crate::RpcDocumentationStatus::Deprecated | crate::RpcDocumentationStatus::Unstable => true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Checks runtime support and centrally emits the KSP warning required for deprecated, unstable or removed methods.
|
||||
///
|
||||
/// Removed methods return [`crate::ERROR_CODE_METHOD_REMOVED`] and are never presented as callable standard RPC operations.
|
||||
pub fn ensure_runtime_supported(&self) -> ksp_core_lib::Result<()> {
|
||||
if self.runtime_status == crate::RpcRuntimeStatus::Removed {
|
||||
let replacement = match self.replacement {
|
||||
std::option::Option::Some(replacement) => replacement,
|
||||
std::option::Option::None => "none",
|
||||
};
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
rpc_method = self.method,
|
||||
documentation_status = self.documentation_status.code(),
|
||||
runtime_status = self.runtime_status.code(),
|
||||
replacement,
|
||||
"removed Solana HTTP RPC method requested"
|
||||
);
|
||||
let mut error = ksp_core_lib::Error::new(crate::ERROR_CODE_METHOD_REMOVED, "Solana HTTP RPC method is removed from the targeted runtime")
|
||||
.with_context("rpc_method", self.method)
|
||||
.with_context("documentation_status", self.documentation_status.code());
|
||||
if let std::option::Option::Some(replacement) = self.replacement {
|
||||
error = error.with_context("replacement", replacement);
|
||||
}
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if self.requires_method_usage_warning() {
|
||||
let replacement = match self.replacement {
|
||||
std::option::Option::Some(replacement) => replacement,
|
||||
std::option::Option::None => "none",
|
||||
};
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
rpc_method = self.method,
|
||||
documentation_status = self.documentation_status.code(),
|
||||
runtime_status = self.runtime_status.code(),
|
||||
replacement,
|
||||
"non-stable Solana HTTP RPC method requested"
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all 52 current Solana HTTP RPC method descriptors audited for the `0.2.1`–`0.2.4` coverage sequence.
|
||||
#[must_use]
|
||||
pub const fn current_http_rpc_methods() -> &'static [crate::HttpRpcMethodDescriptor] {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/rpc_transactions.rs
|
||||
// version: 8
|
||||
// version: 10
|
||||
|
||||
const MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS: usize = 128;
|
||||
const MAX_SIGNATURES_FOR_ADDRESS_LIMIT: usize = 1_000;
|
||||
const MAX_SIGNATURE_STATUSES: usize = 256;
|
||||
|
||||
/// Binary encoding accepted for serialized transaction input payloads.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -1083,10 +1087,6 @@ impl SolanaSimulateTransactionResult {
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_RECENT_PRIORITIZATION_FEE_ACCOUNTS: usize = 128;
|
||||
const MAX_SIGNATURES_FOR_ADDRESS_LIMIT: usize = 1_000;
|
||||
const MAX_SIGNATURE_STATUSES: usize = 256;
|
||||
|
||||
impl crate::HttpTransportPool {
|
||||
/// Executes typed `getFeeForMessage` through the common KSP HTTP transport path.
|
||||
pub async fn get_fee_for_message(
|
||||
@@ -1389,7 +1389,6 @@ impl crate::HttpTransportPool {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut params = std::vec![serde_json::Value::String(transaction.to_owned())];
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& !config.is_empty()
|
||||
@@ -1469,6 +1468,104 @@ impl crate::HttpTransportPool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireTransactionRpcResponse {
|
||||
context: serde_json::Value,
|
||||
value: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireLatestBlockhash {
|
||||
blockhash: std::string::String,
|
||||
last_valid_block_height: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WirePrioritizationFee {
|
||||
slot: u64,
|
||||
prioritization_fee: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireSignatureInfo {
|
||||
signature: std::string::String,
|
||||
slot: u64,
|
||||
#[serde(default)]
|
||||
err: std::option::Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
memo: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
block_time: std::option::Option<i64>,
|
||||
#[serde(default)]
|
||||
confirmation_status: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
transaction_index: std::option::Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireSignatureStatus {
|
||||
slot: u64,
|
||||
#[serde(default)]
|
||||
confirmations: std::option::Option<u64>,
|
||||
status: serde_json::Value,
|
||||
#[serde(default)]
|
||||
err: std::option::Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
confirmation_status: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireConfirmedTransaction {
|
||||
slot: u64,
|
||||
transaction: serde_json::Value,
|
||||
#[serde(default)]
|
||||
meta: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
block_time: std::option::Option<i64>,
|
||||
#[serde(default)]
|
||||
version: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
transaction_index: crate::SolanaWireField<u32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireSimulateTransactionResult {
|
||||
#[serde(default)]
|
||||
err: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
logs: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
|
||||
#[serde(default)]
|
||||
accounts: crate::SolanaWireField<std::vec::Vec<std::option::Option<serde_json::Value>>>,
|
||||
#[serde(default)]
|
||||
units_consumed: crate::SolanaWireField<u64>,
|
||||
#[serde(default)]
|
||||
loaded_accounts_data_size: crate::SolanaWireField<u32>,
|
||||
#[serde(default)]
|
||||
return_data: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
inner_instructions: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
replacement_blockhash: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
fee: crate::SolanaWireField<u64>,
|
||||
#[serde(default)]
|
||||
pre_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
|
||||
#[serde(default)]
|
||||
post_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
|
||||
#[serde(default)]
|
||||
pre_token_balances: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
post_token_balances: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
loaded_addresses: crate::SolanaWireField<serde_json::Value>,
|
||||
}
|
||||
|
||||
fn push_transaction_context_config(params: &mut std::vec::Vec<serde_json::Value>, config: std::option::Option<&crate::SolanaContextConfig>) {
|
||||
if let std::option::Option::Some(config) = config
|
||||
&& (config.commitment().is_some() || config.min_context_slot().is_some())
|
||||
@@ -1584,12 +1681,6 @@ fn transaction_descriptor(method: &str) -> ksp_core_lib::Result<&'static crate::
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WireTransactionRpcResponse {
|
||||
context: serde_json::Value,
|
||||
value: serde_json::Value,
|
||||
}
|
||||
|
||||
fn invalid_transaction_wire(method: &str, field: &str, message: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("rpc_method", method).with_context("field", field);
|
||||
}
|
||||
@@ -1711,98 +1802,6 @@ fn decode_replacement_blockhash_field(
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireLatestBlockhash {
|
||||
blockhash: std::string::String,
|
||||
last_valid_block_height: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WirePrioritizationFee {
|
||||
slot: u64,
|
||||
prioritization_fee: u64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireSignatureInfo {
|
||||
signature: std::string::String,
|
||||
slot: u64,
|
||||
#[serde(default)]
|
||||
err: std::option::Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
memo: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
block_time: std::option::Option<i64>,
|
||||
#[serde(default)]
|
||||
confirmation_status: std::option::Option<std::string::String>,
|
||||
#[serde(default)]
|
||||
transaction_index: std::option::Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireSignatureStatus {
|
||||
slot: u64,
|
||||
#[serde(default)]
|
||||
confirmations: std::option::Option<u64>,
|
||||
status: serde_json::Value,
|
||||
#[serde(default)]
|
||||
err: std::option::Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
confirmation_status: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireConfirmedTransaction {
|
||||
slot: u64,
|
||||
transaction: serde_json::Value,
|
||||
#[serde(default)]
|
||||
meta: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
block_time: std::option::Option<i64>,
|
||||
#[serde(default)]
|
||||
version: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
transaction_index: crate::SolanaWireField<u32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct WireSimulateTransactionResult {
|
||||
#[serde(default)]
|
||||
err: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
logs: crate::SolanaWireField<std::vec::Vec<std::string::String>>,
|
||||
#[serde(default)]
|
||||
accounts: crate::SolanaWireField<std::vec::Vec<std::option::Option<serde_json::Value>>>,
|
||||
#[serde(default)]
|
||||
units_consumed: crate::SolanaWireField<u64>,
|
||||
#[serde(default)]
|
||||
loaded_accounts_data_size: crate::SolanaWireField<u32>,
|
||||
#[serde(default)]
|
||||
return_data: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
inner_instructions: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
replacement_blockhash: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
fee: crate::SolanaWireField<u64>,
|
||||
#[serde(default)]
|
||||
pre_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
|
||||
#[serde(default)]
|
||||
post_balances: crate::SolanaWireField<std::vec::Vec<u64>>,
|
||||
#[serde(default)]
|
||||
pre_token_balances: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
post_token_balances: crate::SolanaWireField<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
loaded_addresses: crate::SolanaWireField<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/rpc_transactions.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/src/settings.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
/// Runtime HTTP endpoint URL owned by Transport.
|
||||
///
|
||||
@@ -446,6 +446,7 @@ impl HttpTransportSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates endpoint settings.
|
||||
pub(crate) fn validate_endpoint_settings(endpoint: &crate::HttpEndpointSettings) -> ksp_core_lib::Result<()> {
|
||||
return validate_endpoint(endpoint, 0);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||
// version: 23
|
||||
// version: 24
|
||||
|
||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||
|
||||
@@ -442,7 +442,6 @@ fn public_v0_2_4_pre_007_simple_economics_wrappers_are_available_from_crate_root
|
||||
let _get_inflation_rate = ksp_onchain_transport_lib::HttpTransportPool::get_inflation_rate;
|
||||
let _get_stake_minimum_delegation = ksp_onchain_transport_lib::HttpTransportPool::get_stake_minimum_delegation;
|
||||
let _get_supply = ksp_onchain_transport_lib::HttpTransportPool::get_supply;
|
||||
|
||||
let supply_config = ksp_onchain_transport_lib::SolanaSupplyConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
|
||||
std::option::Option::Some(false),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||
// version: 21
|
||||
// version: 22
|
||||
|
||||
//! Release-level completeness canaries for the staged HTTP wrapper sequence.
|
||||
|
||||
@@ -681,7 +681,6 @@ fn release_v0_2_4_pre_009_final_http_inventory_and_coverage_partition_are_exact(
|
||||
"getSnapshotSlot",
|
||||
"getStakeActivation",
|
||||
];
|
||||
|
||||
let current = ksp_onchain_transport_lib::current_http_rpc_methods();
|
||||
let historical = ksp_onchain_transport_lib::historical_http_rpc_methods();
|
||||
let mut actual_current = std::vec::Vec::with_capacity(current.len());
|
||||
@@ -691,7 +690,6 @@ fn release_v0_2_4_pre_009_final_http_inventory_and_coverage_partition_are_exact(
|
||||
let mut v0_2_3 = 0_usize;
|
||||
let mut v0_2_4 = 0_usize;
|
||||
let mut historical_current = 0_usize;
|
||||
|
||||
for descriptor in current {
|
||||
actual_current.push(descriptor.method());
|
||||
assert_eq!(descriptor.runtime_status(), ksp_onchain_transport_lib::RpcRuntimeStatus::Supported);
|
||||
@@ -719,7 +717,6 @@ fn release_v0_2_4_pre_009_final_http_inventory_and_coverage_partition_are_exact(
|
||||
assert_eq!(descriptor.coverage_release(), ksp_onchain_transport_lib::HttpRpcCoverageRelease::Historical);
|
||||
assert_eq!(descriptor.transport_retry_class(), ksp_onchain_transport_lib::TransportRetryClass::NotApplicable);
|
||||
}
|
||||
|
||||
actual_current.sort_unstable();
|
||||
actual_historical.sort_unstable();
|
||||
let mut expected_current = expected_current;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/tests/transport_devnet_smoke.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
//! Opt-in live Devnet smoke for representative pure Transport reads across the complete HTTP typed surface.
|
||||
|
||||
@@ -45,7 +45,6 @@ fn devnet_pool() -> ksp_core_lib::Result<ksp_onchain_transport_lib::HttpTranspor
|
||||
async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transactions_blocks_and_economics_reads() {
|
||||
let pool = devnet_pool().expect("programmatic Devnet Transport settings must construct a pool");
|
||||
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
|
||||
|
||||
let account_config = ksp_onchain_transport_lib::SolanaAccountInfoConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64),
|
||||
std::option::Option::None,
|
||||
@@ -58,7 +57,6 @@ async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transacti
|
||||
.expect("Devnet getAccountInfo smoke must succeed");
|
||||
assert!(account.context().slot() > 0);
|
||||
assert!(account.value().is_some());
|
||||
|
||||
// Follow the current official Devnet example shape with an ordinary owner, the canonical SPL Token program selector,
|
||||
// and an explicit finalized/jsonParsed config. The owner need not retain any token account; an empty list remains valid.
|
||||
let token_owner = "A1TMhSGzQxMr1TboBKtgixKz1sS6REASMxPo1qsyTSJd"
|
||||
@@ -79,7 +77,6 @@ async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transacti
|
||||
.await
|
||||
.expect("Devnet getTokenAccountsByOwner smoke must succeed with the documented finalized/jsonParsed request shape");
|
||||
assert!(token_accounts.context().slot() > 0);
|
||||
|
||||
let epoch = pool
|
||||
.get_epoch_info(
|
||||
&role,
|
||||
@@ -91,10 +88,8 @@ async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transacti
|
||||
.await
|
||||
.expect("Devnet getEpochInfo smoke must succeed");
|
||||
assert!(epoch.absolute_slot() > 0);
|
||||
|
||||
let vote_accounts = pool.get_vote_accounts(&role, std::option::Option::None).await.expect("Devnet getVoteAccounts smoke must succeed");
|
||||
assert!(!vote_accounts.current().is_empty() || !vote_accounts.delinquent().is_empty());
|
||||
|
||||
let transaction_context = ksp_onchain_transport_lib::SolanaContextConfig::new(
|
||||
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||
std::option::Option::None,
|
||||
@@ -105,27 +100,22 @@ async fn programmatic_devnet_transport_reaches_accounts_tokens_cluster_transacti
|
||||
.expect("Devnet getLatestBlockhash smoke must succeed");
|
||||
assert!(!latest_blockhash.value().blockhash().is_empty());
|
||||
assert!(latest_blockhash.value().last_valid_block_height() > 0);
|
||||
|
||||
let blockhash_valid = pool
|
||||
.is_blockhash_valid(&role, latest_blockhash.value().blockhash(), std::option::Option::Some(&transaction_context))
|
||||
.await
|
||||
.expect("Devnet isBlockhashValid smoke must succeed");
|
||||
assert_eq!(blockhash_valid.value(), &true);
|
||||
|
||||
let transaction_count = pool
|
||||
.get_transaction_count(&role, std::option::Option::Some(&transaction_context))
|
||||
.await
|
||||
.expect("Devnet getTransactionCount smoke must succeed");
|
||||
assert!(transaction_count > 0);
|
||||
|
||||
let block_height = pool.get_block_height(&role, std::option::Option::Some(&transaction_context)).await.expect("Devnet getBlockHeight smoke must succeed");
|
||||
assert!(block_height > 0);
|
||||
|
||||
let inflation_rate = pool.get_inflation_rate(&role).await.expect("Devnet getInflationRate smoke must succeed");
|
||||
assert!(inflation_rate.total().is_finite());
|
||||
assert!(inflation_rate.validator().is_finite());
|
||||
assert!(inflation_rate.foundation().is_finite());
|
||||
|
||||
let stake_minimum_delegation = pool
|
||||
.get_stake_minimum_delegation(&role, std::option::Option::Some(&transaction_context))
|
||||
.await
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/client.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings {
|
||||
let url = crate::HttpEndpointUrl::parse(url_text).expect("test endpoint URL must parse");
|
||||
@@ -25,7 +25,7 @@ fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings {
|
||||
|
||||
#[test]
|
||||
fn endpoint_client_snapshot_never_contains_url_or_secret_material() {
|
||||
let client = super::HttpEndpointClient::new(endpoint(true, "https://provider.invalid/rpc?api-key=SECRET-CANARY")).expect("client must build");
|
||||
let client = crate::HttpEndpointClient::new(endpoint(true, "https://provider.invalid/rpc?api-key=SECRET-CANARY")).expect("client must build");
|
||||
let snapshot = client.snapshot();
|
||||
let rendered = format!("{snapshot:?} {client:?}");
|
||||
assert_eq!(snapshot.availability(), crate::HttpEndpointAvailability::Available);
|
||||
@@ -36,21 +36,21 @@ fn endpoint_client_snapshot_never_contains_url_or_secret_material() {
|
||||
|
||||
#[test]
|
||||
fn disabled_endpoint_client_is_visible_but_not_selectable() {
|
||||
let client = super::HttpEndpointClient::new(endpoint(false, "https://api.devnet.solana.com")).expect("disabled client must still build");
|
||||
let client = crate::HttpEndpointClient::new(endpoint(false, "https://api.devnet.solana.com")).expect("disabled client must still build");
|
||||
assert_eq!(client.snapshot().availability(), crate::HttpEndpointAvailability::Disabled);
|
||||
assert!(!client.supports(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_client_matches_exact_and_wildcard_capabilities() {
|
||||
let client = super::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build");
|
||||
let client = crate::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build");
|
||||
assert!(client.supports(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("get_balance")));
|
||||
assert!(!client.supports(&crate::HttpRoleName::new("write"), &crate::HttpRequestKind::new("get_balance")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_role_snapshot_exposes_safe_resilience_state() {
|
||||
let client = super::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build");
|
||||
let client = crate::HttpEndpointClient::new(endpoint(true, "https://api.devnet.solana.com")).expect("client must build");
|
||||
let snapshot = client.snapshot();
|
||||
let role = &snapshot.roles()[0];
|
||||
assert_eq!(role.availability(), crate::HttpEndpointAvailability::Available);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/json_rpc.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn request_serialization_matches_json_rpc_2_0_shape() {
|
||||
let request = super::JsonRpcRequest::new(7, "getBalance", std::vec![serde_json::json!("Address111"), serde_json::json!({"commitment":"confirmed"})])
|
||||
let request = crate::JsonRpcRequest::new(7, "getBalance", std::vec![serde_json::json!("Address111"), serde_json::json!({"commitment":"confirmed"})])
|
||||
.expect("valid request must construct");
|
||||
let encoded = request.to_json_string().expect("serializable request must encode");
|
||||
let value: serde_json::Value = serde_json::from_str(encoded.as_str()).expect("encoded request must remain JSON");
|
||||
@@ -15,28 +15,28 @@ fn request_serialization_matches_json_rpc_2_0_shape() {
|
||||
|
||||
#[test]
|
||||
fn request_rejects_empty_or_untrimmed_method() {
|
||||
assert!(super::JsonRpcRequest::new(1, "", std::vec![]).is_err());
|
||||
assert!(super::JsonRpcRequest::new(1, " getHealth", std::vec![]).is_err());
|
||||
assert!(crate::JsonRpcRequest::new(1, "", std::vec![]).is_err());
|
||||
assert!(crate::JsonRpcRequest::new(1, " getHealth", std::vec![]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_parser_preserves_null_success_result() {
|
||||
let response = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":null,"id":9}"#, 9).expect("null result is a valid success payload");
|
||||
assert!(matches!(&response, super::JsonRpcResponse::Success(_)), "success response must not parse as error");
|
||||
if let super::JsonRpcResponse::Success(success) = response {
|
||||
let response = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":null,"id":9}"#, 9).expect("null result is a valid success payload");
|
||||
assert!(matches!(&response, crate::JsonRpcResponse::Success(_)), "success response must not parse as error");
|
||||
if let crate::JsonRpcResponse::Success(success) = response {
|
||||
assert!(success.result().is_null());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_parser_preserves_rpc_error_payload() {
|
||||
let response = super::parse_json_rpc_response_text(
|
||||
let response = crate::parse_json_rpc_response_text(
|
||||
r#"{"jsonrpc":"2.0","error":{"code":-32005,"message":"Node is unhealthy","data":{"numSlotsBehind":12}},"id":4}"#,
|
||||
4,
|
||||
)
|
||||
.expect("valid JSON-RPC error envelope must parse");
|
||||
assert!(matches!(&response, super::JsonRpcResponse::Error(_)), "RPC error response must not parse as success");
|
||||
if let super::JsonRpcResponse::Error(error_response) = response {
|
||||
assert!(matches!(&response, crate::JsonRpcResponse::Error(_)), "RPC error response must not parse as success");
|
||||
if let crate::JsonRpcResponse::Error(error_response) = response {
|
||||
assert_eq!(error_response.error().code(), -32005);
|
||||
assert_eq!(error_response.error().message(), "Node is unhealthy");
|
||||
assert_eq!(error_response.error().data(), std::option::Option::Some(&serde_json::json!({"numSlotsBehind":12})));
|
||||
@@ -45,38 +45,38 @@ fn response_parser_preserves_rpc_error_payload() {
|
||||
|
||||
#[test]
|
||||
fn response_parser_rejects_id_mismatch() {
|
||||
let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","id":2}"#, 1).expect_err("mismatched id must fail");
|
||||
let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","id":2}"#, 1).expect_err("mismatched id must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_parser_rejects_wrong_protocol_version() {
|
||||
let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"1.0","result":"ok","id":1}"#, 1).expect_err("wrong version must fail");
|
||||
let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"1.0","result":"ok","id":1}"#, 1).expect_err("wrong version must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_parser_rejects_both_result_and_error() {
|
||||
let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","error":{"code":-1,"message":"bad"},"id":1}"#, 1)
|
||||
let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":"ok","error":{"code":-1,"message":"bad"},"id":1}"#, 1)
|
||||
.expect_err("mutually exclusive fields must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_parser_rejects_missing_result_and_error() {
|
||||
let error = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","id":1}"#, 1).expect_err("missing outcome must fail");
|
||||
let error = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","id":1}"#, 1).expect_err("missing outcome must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_JSON_RPC_PROTOCOL_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_parser_distinguishes_invalid_json_from_protocol_error() {
|
||||
let error = super::parse_json_rpc_response_text("not-json", 1).expect_err("invalid JSON must fail decoding");
|
||||
let error = crate::parse_json_rpc_response_text("not-json", 1).expect_err("invalid JSON must fail decoding");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_JSON_DECODE_FAILED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_error_maps_to_shared_ksp_error_without_copying_remote_payload_into_context() {
|
||||
let response = super::parse_json_rpc_response_text(
|
||||
let response = crate::parse_json_rpc_response_text(
|
||||
r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"SECRET-CANARY","data":{"payload":"SECRET-DATA"}},"id":1}"#,
|
||||
1,
|
||||
)
|
||||
@@ -90,7 +90,7 @@ fn rpc_error_maps_to_shared_ksp_error_without_copying_remote_payload_into_contex
|
||||
|
||||
#[test]
|
||||
fn request_debug_omits_parameter_payloads() {
|
||||
let request = super::JsonRpcRequest::new(1, "sendTransaction", std::vec![serde_json::json!("SIGNED-TRANSACTION-SECRET-CANARY")])
|
||||
let request = crate::JsonRpcRequest::new(1, "sendTransaction", std::vec![serde_json::json!("SIGNED-TRANSACTION-SECRET-CANARY")])
|
||||
.expect("test request must construct");
|
||||
let rendered = format!("{request:?}");
|
||||
assert!(rendered.contains("sendTransaction"));
|
||||
@@ -100,11 +100,11 @@ fn request_debug_omits_parameter_payloads() {
|
||||
|
||||
#[test]
|
||||
fn response_debug_omits_result_and_remote_error_payloads() {
|
||||
let success = super::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":{"secret":"RESULT-SECRET-CANARY"},"id":1}"#, 1)
|
||||
let success = crate::parse_json_rpc_response_text(r#"{"jsonrpc":"2.0","result":{"secret":"RESULT-SECRET-CANARY"},"id":1}"#, 1)
|
||||
.expect("test success response must parse");
|
||||
let success_rendered = format!("{success:?}");
|
||||
assert!(!success_rendered.contains("RESULT-SECRET-CANARY"));
|
||||
let failure = super::parse_json_rpc_response_text(
|
||||
let failure = crate::parse_json_rpc_response_text(
|
||||
r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"MESSAGE-SECRET-CANARY","data":{"secret":"DATA-SECRET-CANARY"}},"id":2}"#,
|
||||
2,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/pool.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
fn role(name: &str, priority: u32, request_kinds: std::vec::Vec<crate::HttpRequestKind>) -> crate::HttpEndpointRoleSettings {
|
||||
return crate::HttpEndpointRoleSettings::new(
|
||||
@@ -66,7 +66,7 @@ fn settings(endpoints: std::vec::Vec<crate::HttpEndpointSettings>) -> crate::Htt
|
||||
|
||||
#[test]
|
||||
fn pool_prefers_lowest_priority_tier() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("secondary", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("primary", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
]))
|
||||
@@ -80,7 +80,7 @@ fn pool_prefers_lowest_priority_tier() {
|
||||
|
||||
#[test]
|
||||
fn pool_round_robins_fairly_inside_best_priority_tier() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("one", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("two", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
@@ -98,7 +98,7 @@ fn pool_round_robins_fairly_inside_best_priority_tier() {
|
||||
|
||||
#[test]
|
||||
fn disabled_best_priority_endpoint_falls_back_to_next_tier() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("disabled-primary", false, 1, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
]))
|
||||
@@ -111,7 +111,7 @@ fn disabled_best_priority_endpoint_falls_back_to_next_tier() {
|
||||
|
||||
#[test]
|
||||
fn pool_filters_role_and_capability_before_priority() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("wrong-capability", true, 1, std::vec![crate::HttpRequestKind::new("send_transaction")]),
|
||||
endpoint("matching", true, 50, std::vec![crate::HttpRequestKind::new("get_balance")]),
|
||||
]))
|
||||
@@ -124,7 +124,7 @@ fn pool_filters_role_and_capability_before_priority() {
|
||||
|
||||
#[test]
|
||||
fn pool_returns_structured_error_when_no_endpoint_matches() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("read-only", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![endpoint("read-only", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
|
||||
.expect("pool must build");
|
||||
let error = pool
|
||||
.select_for_request_kind(&crate::HttpRoleName::new("default"), &crate::HttpRequestKind::new("send_transaction"))
|
||||
@@ -135,7 +135,7 @@ fn pool_returns_structured_error_when_no_endpoint_matches() {
|
||||
|
||||
#[test]
|
||||
fn standard_method_selection_uses_registry_request_kind() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("balance", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![endpoint("balance", true, 10, std::vec![crate::HttpRequestKind::new("get_balance")],)]))
|
||||
.expect("pool must build");
|
||||
let method = crate::find_http_rpc_method("getBalance").expect("audited method must exist");
|
||||
let selection = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect("standard method must route");
|
||||
@@ -144,7 +144,7 @@ fn standard_method_selection_uses_registry_request_kind() {
|
||||
|
||||
#[test]
|
||||
fn pool_snapshot_is_safe_and_preserves_disabled_endpoints() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
endpoint("enabled", true, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
endpoint("disabled", false, 10, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
]))
|
||||
@@ -185,7 +185,7 @@ fn disabled_role_is_excluded_before_priority_selection() {
|
||||
base.max_idle_connections_per_host(),
|
||||
std::vec![disabled_role, enabled_non_matching_role],
|
||||
);
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
disabled_role_endpoint,
|
||||
endpoint("fallback", true, 20, std::vec![crate::HttpRequestKind::wildcard()]),
|
||||
]))
|
||||
@@ -198,7 +198,7 @@ fn disabled_role_is_excluded_before_priority_selection() {
|
||||
|
||||
#[test]
|
||||
fn removed_standard_method_is_rejected_before_endpoint_routing() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![endpoint("wildcard", true, 10, std::vec![crate::HttpRequestKind::wildcard()],)]))
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![endpoint("wildcard", true, 10, std::vec![crate::HttpRequestKind::wildcard()],)]))
|
||||
.expect("pool must build");
|
||||
let method = crate::find_http_rpc_method("confirmTransaction").expect("historical method must exist");
|
||||
let error = pool.select_for_method(&crate::HttpRoleName::new("default"), method).expect_err("removed standard method must be rejected before routing");
|
||||
@@ -207,7 +207,7 @@ fn removed_standard_method_is_rejected_before_endpoint_routing() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_concurrency_saturation_falls_back_to_lower_priority_tier() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
limited_endpoint("primary", 1, std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None),
|
||||
limited_endpoint("fallback", 20, std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None),
|
||||
]))
|
||||
@@ -222,7 +222,7 @@ async fn runtime_concurrency_saturation_falls_back_to_lower_priority_tier() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn runtime_token_bucket_exhaustion_falls_back_without_busy_waiting() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
limited_endpoint("primary", 1, std::option::Option::Some(1), std::option::Option::Some(1), std::option::Option::None, std::option::Option::None),
|
||||
limited_endpoint("fallback", 20, std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
]))
|
||||
@@ -238,7 +238,7 @@ async fn runtime_token_bucket_exhaustion_falls_back_without_busy_waiting() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_cooldown_excludes_rate_limited_role_and_uses_fallback() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![
|
||||
limited_endpoint(
|
||||
"primary",
|
||||
1,
|
||||
@@ -263,7 +263,7 @@ async fn provider_cooldown_excludes_rate_limited_role_and_uses_fallback() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn admission_waits_for_released_concurrency_without_holding_a_sync_mutex_across_await() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
"primary",
|
||||
1,
|
||||
std::option::Option::None,
|
||||
@@ -289,7 +289,7 @@ async fn admission_waits_for_released_concurrency_without_holding_a_sync_mutex_a
|
||||
|
||||
#[tokio::test]
|
||||
async fn admission_timeout_is_bounded_when_concurrency_never_becomes_available() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
"primary",
|
||||
1,
|
||||
std::option::Option::None,
|
||||
@@ -310,7 +310,7 @@ async fn admission_timeout_is_bounded_when_concurrency_never_becomes_available()
|
||||
|
||||
#[tokio::test]
|
||||
async fn passive_health_snapshot_moves_from_degraded_back_to_available_after_success() {
|
||||
let pool = super::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
let pool = crate::HttpTransportPool::new(settings(std::vec![limited_endpoint(
|
||||
"primary",
|
||||
1,
|
||||
std::option::Option::None,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/resilience.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
fn non_zero(value: u32) -> std::num::NonZeroU32 {
|
||||
return std::num::NonZeroU32::new(value).expect("test limit must be non-zero");
|
||||
@@ -46,46 +46,46 @@ fn retry_backoff_is_exponential_and_bounded() {
|
||||
#[test]
|
||||
fn retry_safe_timeout_is_retried_until_budget_is_exhausted() {
|
||||
let settings = retry_settings();
|
||||
let first = super::evaluate_transport_retry(
|
||||
let first = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&settings,
|
||||
super::HttpRetryCause::Timeout,
|
||||
super::HttpDispatchState::DispatchedAmbiguous,
|
||||
crate::HttpRetryCause::Timeout,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
0,
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(first, super::HttpRetryDecision::RetryAfter(std::time::Duration::from_millis(100)));
|
||||
let exhausted = super::evaluate_transport_retry(
|
||||
assert_eq!(first, crate::HttpRetryDecision::RetryAfter(std::time::Duration::from_millis(100)));
|
||||
let exhausted = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&settings,
|
||||
super::HttpRetryCause::Timeout,
|
||||
super::HttpDispatchState::DispatchedAmbiguous,
|
||||
crate::HttpRetryCause::Timeout,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
settings.max_retries(),
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(exhausted, super::HttpRetryDecision::Stop);
|
||||
assert_eq!(exhausted, crate::HttpRetryDecision::Stop);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_submission_never_retries_after_ambiguous_dispatch() {
|
||||
let decision = super::evaluate_transport_retry(
|
||||
let decision = crate::evaluate_transport_retry(
|
||||
method("sendTransaction"),
|
||||
&retry_settings(),
|
||||
super::HttpRetryCause::Connection,
|
||||
super::HttpDispatchState::DispatchedAmbiguous,
|
||||
crate::HttpRetryCause::Connection,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
0,
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(decision, super::HttpRetryDecision::Stop);
|
||||
assert_eq!(decision, crate::HttpRetryDecision::Stop);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_submission_can_retry_when_transport_proves_no_dispatch() {
|
||||
let decision = super::evaluate_transport_retry(
|
||||
let decision = crate::evaluate_transport_retry(
|
||||
method("sendTransaction"),
|
||||
&retry_settings(),
|
||||
super::HttpRetryCause::Connection,
|
||||
super::HttpDispatchState::NotDispatched,
|
||||
crate::HttpRetryCause::Connection,
|
||||
crate::HttpDispatchState::NotDispatched,
|
||||
0,
|
||||
std::option::Option::None,
|
||||
);
|
||||
@@ -94,36 +94,36 @@ fn write_submission_can_retry_when_transport_proves_no_dispatch() {
|
||||
|
||||
#[test]
|
||||
fn rpc_application_and_invalid_response_are_not_transport_retries() {
|
||||
for cause in [super::HttpRetryCause::RpcApplication, super::HttpRetryCause::InvalidResponse, super::HttpRetryCause::Request] {
|
||||
let decision = super::evaluate_transport_retry(
|
||||
for cause in [crate::HttpRetryCause::RpcApplication, crate::HttpRetryCause::InvalidResponse, crate::HttpRetryCause::Request] {
|
||||
let decision = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&retry_settings(),
|
||||
cause,
|
||||
super::HttpDispatchState::NotDispatched,
|
||||
crate::HttpDispatchState::NotDispatched,
|
||||
0,
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert_eq!(decision, super::HttpRetryDecision::Stop);
|
||||
assert_eq!(decision, crate::HttpRetryDecision::Stop);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_retry_after_can_extend_backoff_but_is_defensively_bounded() {
|
||||
let settings = retry_settings();
|
||||
let extended = super::evaluate_transport_retry(
|
||||
let extended = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&settings,
|
||||
super::HttpRetryCause::RateLimited,
|
||||
super::HttpDispatchState::DispatchedAmbiguous,
|
||||
crate::HttpRetryCause::RateLimited,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
0,
|
||||
std::option::Option::Some(std::time::Duration::from_secs(3)),
|
||||
);
|
||||
assert_eq!(extended.delay(), std::option::Option::Some(std::time::Duration::from_secs(3)));
|
||||
let bounded = super::evaluate_transport_retry(
|
||||
let bounded = crate::evaluate_transport_retry(
|
||||
method("getBalance"),
|
||||
&settings,
|
||||
super::HttpRetryCause::RateLimited,
|
||||
super::HttpDispatchState::DispatchedAmbiguous,
|
||||
crate::HttpRetryCause::RateLimited,
|
||||
crate::HttpDispatchState::DispatchedAmbiguous,
|
||||
0,
|
||||
std::option::Option::Some(std::time::Duration::from_secs(600)),
|
||||
);
|
||||
@@ -144,29 +144,29 @@ fn token_bucket_consumes_burst_then_refills_from_elapsed_time() {
|
||||
#[test]
|
||||
fn absent_burst_capacity_defaults_to_one_second_of_rps_capacity() {
|
||||
let role = role_limits(std::option::Option::Some(2), std::option::Option::None, std::option::Option::None, std::option::Option::None);
|
||||
let runtime = std::sync::Arc::new(super::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new())));
|
||||
let runtime = std::sync::Arc::new(crate::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new())));
|
||||
let now = std::time::Instant::now();
|
||||
let first = runtime.try_acquire(now);
|
||||
let second = runtime.try_acquire(now);
|
||||
let third = runtime.try_acquire(now);
|
||||
assert!(matches!(first, super::RoleAdmissionAttempt::Ready(_)));
|
||||
assert!(matches!(second, super::RoleAdmissionAttempt::Ready(_)));
|
||||
assert!(matches!(third, super::RoleAdmissionAttempt::BlockedUntil(_)));
|
||||
assert!(matches!(first, crate::RoleAdmissionAttempt::Ready(_)));
|
||||
assert!(matches!(second, crate::RoleAdmissionAttempt::Ready(_)));
|
||||
assert!(matches!(third, crate::RoleAdmissionAttempt::BlockedUntil(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrency_semaphore_releases_capacity_when_permit_is_dropped() {
|
||||
let role = role_limits(std::option::Option::None, std::option::Option::None, std::option::Option::Some(1), std::option::Option::None);
|
||||
let runtime = std::sync::Arc::new(super::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new())));
|
||||
let runtime = std::sync::Arc::new(crate::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new())));
|
||||
let now = std::time::Instant::now();
|
||||
let first = runtime.try_acquire(now);
|
||||
let held = match first {
|
||||
super::RoleAdmissionAttempt::Ready(permit) => permit,
|
||||
crate::RoleAdmissionAttempt::Ready(permit) => permit,
|
||||
_ => panic!("first concurrency permit must be available"),
|
||||
};
|
||||
assert!(matches!(runtime.try_acquire(now), super::RoleAdmissionAttempt::ConcurrencySaturated));
|
||||
assert!(matches!(runtime.try_acquire(now), crate::RoleAdmissionAttempt::ConcurrencySaturated));
|
||||
drop(held);
|
||||
assert!(matches!(runtime.try_acquire(now), super::RoleAdmissionAttempt::Ready(_)));
|
||||
assert!(matches!(runtime.try_acquire(now), crate::RoleAdmissionAttempt::Ready(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -177,7 +177,7 @@ fn rate_limit_cooldown_marks_role_and_caps_provider_delay() {
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(std::time::Duration::from_millis(10)),
|
||||
);
|
||||
let runtime = super::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new()));
|
||||
let runtime = crate::HttpRoleRuntime::new(&role, std::sync::Arc::new(tokio::sync::Notify::new()));
|
||||
let pause = runtime.record_rate_limited(std::option::Option::Some(std::time::Duration::from_secs(600)));
|
||||
assert_eq!(pause, std::time::Duration::from_secs(60));
|
||||
assert_eq!(runtime.rate_limit_count(), 1);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_accounts.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
#[test]
|
||||
fn account_config_serializes_all_common_fields() {
|
||||
@@ -64,7 +64,6 @@ fn staged_largest_and_keyed_account_helpers_match_wire_shapes() {
|
||||
std::option::Option::Some(true),
|
||||
);
|
||||
assert_eq!(config.to_json_value(), serde_json::json!({"commitment":"finalized","filter":"nonCirculating","sortResults":true}));
|
||||
|
||||
let keyed = crate::SolanaKeyedAccount::decode_wire(
|
||||
"fixture",
|
||||
serde_json::json!({
|
||||
@@ -82,7 +81,6 @@ fn staged_largest_and_keyed_account_helpers_match_wire_shapes() {
|
||||
.expect("keyed account must decode");
|
||||
assert_eq!(keyed.pubkey().to_string(), "11111111111111111111111111111111");
|
||||
assert_eq!(keyed.account().lamports(), 42);
|
||||
|
||||
let balance = crate::SolanaAccountBalance::decode_wire("fixture", serde_json::json!({"address":"11111111111111111111111111111111","lamports":99}))
|
||||
.expect("account balance must decode");
|
||||
assert_eq!(balance.address().to_string(), "11111111111111111111111111111111");
|
||||
@@ -417,7 +415,6 @@ async fn typed_get_program_accounts_rejects_filter_cardinality_and_oversized_raw
|
||||
let error = result.expect_err("more than four program-account filters must be rejected locally");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].value(), "5");
|
||||
|
||||
let oversized = crate::SolanaProgramAccountsConfig::new(
|
||||
crate::SolanaAccountInfoConfig::default(),
|
||||
std::vec![crate::SolanaProgramAccountFilter::Memcmp(crate::SolanaMemcmpFilter::new(0, crate::SolanaMemcmpBytes::Bytes(std::vec![0_u8; 129]),))],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_blocks.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
#[test]
|
||||
fn transaction_details_and_get_block_config_preserve_all_modern_options() {
|
||||
@@ -280,7 +280,6 @@ async fn typed_get_block_time_preserves_timestamp_and_null() {
|
||||
assert_eq!(timestamp, std::option::Option::Some(1_787_072_400));
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000123]));
|
||||
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_block_time.null.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let timestamp = pool.get_block_time(&crate::HttpRoleName::new("default"), 430_000_124).await.expect("null block time fixture must succeed");
|
||||
@@ -356,7 +355,6 @@ async fn typed_get_blocks_allows_reversed_and_boundary_ranges_but_rejects_oversi
|
||||
assert!(blocks.is_empty());
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000100, 430000099]));
|
||||
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
let blocks = pool
|
||||
@@ -366,7 +364,6 @@ async fn typed_get_blocks_allows_reversed_and_boundary_ranges_but_rejects_oversi
|
||||
assert_eq!(blocks, std::vec![430_000_100, 430_000_103, 430_000_109]);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([10, 500010]));
|
||||
|
||||
let pool = pool_for_url("http://127.0.0.1:9");
|
||||
let result = pool.get_blocks(&role, 10, std::option::Option::Some(500_011), std::option::Option::None).await;
|
||||
let error = result.expect_err("range above 500000 must reject before I/O");
|
||||
@@ -387,7 +384,6 @@ async fn typed_block_range_wrappers_reject_processed_commitment_before_io() {
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
assert_eq!(error.context()[1].key(), "commitment");
|
||||
assert_eq!(error.context()[1].value(), "processed");
|
||||
|
||||
let get_blocks_with_limit = pool.get_blocks_with_limit(&role, 1, 1, std::option::Option::Some(&config)).await;
|
||||
let error = get_blocks_with_limit.expect_err("getBlocksWithLimit processed commitment must reject before I/O");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
@@ -404,7 +400,6 @@ async fn typed_get_blocks_with_limit_accepts_zero_and_maximum_and_rejects_above_
|
||||
assert!(blocks.is_empty());
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000200, 0]));
|
||||
|
||||
let config = crate::SolanaContextConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized), std::option::Option::Some(430_000_000));
|
||||
let (url, handle) = serve_once(include_str!("../fixtures/http/get_blocks_with_limit.success.json"));
|
||||
let pool = pool_for_url(url.as_str());
|
||||
@@ -415,7 +410,6 @@ async fn typed_get_blocks_with_limit_accepts_zero_and_maximum_and_rejects_above_
|
||||
assert_eq!(blocks, std::vec![430_000_200, 430_000_201, 430_000_205]);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([430000200,500000,{"commitment":"finalized","minContextSlot":430000000}]));
|
||||
|
||||
let pool = pool_for_url("http://127.0.0.1:9");
|
||||
let result = pool.get_blocks_with_limit(&role, 1, 500_001, std::option::Option::None).await;
|
||||
let error = result.expect_err("getBlocksWithLimit above 500000 must reject before I/O");
|
||||
@@ -497,7 +491,6 @@ async fn typed_get_block_production_omits_empty_config_and_supports_open_range_w
|
||||
assert_eq!(response.value().range().last_slot(), 430_000_099);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([]));
|
||||
|
||||
let range = crate::SolanaBlockProductionRange::new(430_000_000, std::option::Option::None);
|
||||
let config = crate::SolanaBlockProductionConfig::new(
|
||||
std::option::Option::Some(crate::SolanaCommitment::Processed),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_cluster.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
#[test]
|
||||
fn cluster_node_fixture_preserves_v4_client_id_and_optional_fields() {
|
||||
@@ -59,19 +59,16 @@ fn epoch_snapshot_and_leader_helpers_preserve_wire_shapes() {
|
||||
)
|
||||
.expect("epoch info must decode");
|
||||
assert_eq!(epoch.transaction_count(), std::option::Option::None);
|
||||
|
||||
let schedule = crate::SolanaEpochSchedule::decode_wire(
|
||||
"getEpochSchedule",
|
||||
serde_json::json!({"firstNormalEpoch":1,"firstNormalSlot":32,"leaderScheduleSlotOffset":32,"slotsPerEpoch":64,"warmup":false}),
|
||||
)
|
||||
.expect("epoch schedule must decode");
|
||||
assert_eq!(schedule.slots_per_epoch(), 64);
|
||||
|
||||
let snapshot = crate::SolanaSnapshotSlotInfo::decode_wire("getHighestSnapshotSlot", serde_json::json!({"full":100,"incremental":null}))
|
||||
.expect("snapshot info must decode");
|
||||
assert_eq!(snapshot.full(), 100);
|
||||
assert_eq!(snapshot.incremental(), std::option::Option::None);
|
||||
|
||||
let leader = crate::SolanaLeaderSchedule::decode_wire("getLeaderSchedule", serde_json::json!({"11111111111111111111111111111111":[0,2,4]}))
|
||||
.expect("leader schedule must decode");
|
||||
assert_eq!(leader.entries().len(), 1);
|
||||
@@ -90,7 +87,6 @@ fn vote_status_and_config_helpers_preserve_wire_shapes() {
|
||||
config.to_json_value(),
|
||||
serde_json::json!({"commitment":"finalized","votePubkey":"11111111111111111111111111111111","keepUnstakedDelinquents":true,"delinquentSlotDistance":128})
|
||||
);
|
||||
|
||||
let status = crate::SolanaVoteAccountStatus::decode_wire(
|
||||
"getVoteAccounts",
|
||||
serde_json::json!({
|
||||
@@ -363,7 +359,6 @@ async fn typed_get_slot_serializes_context_config_and_omits_empty_config() {
|
||||
assert_eq!(slot, 430_000_020);
|
||||
let request = configured_handle.join().expect("fixture server must join");
|
||||
assert_eq!(request_body(request.as_str())["params"], serde_json::json!([{"commitment":"confirmed","minContextSlot":429999999}]));
|
||||
|
||||
let (empty_url, empty_handle) = serve_once(include_str!("../fixtures/http/get_slot.success.json"));
|
||||
let empty_pool = pool_for_url(empty_url.as_str());
|
||||
let empty = crate::SolanaContextConfig::default();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_economics.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
#[test]
|
||||
fn inflation_reward_config_preserves_epoch_commitment_and_min_context_slot() {
|
||||
@@ -346,7 +346,6 @@ async fn typed_get_inflation_reward_serializes_full_config_and_preserves_positio
|
||||
let fourth = rewards[3].as_ref().expect("fourth reward must be present");
|
||||
assert_eq!(fourth.commission(), std::option::Option::Some(3));
|
||||
assert!(fourth.commission_bps().is_null());
|
||||
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("getInflationReward"));
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_method.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#[test]
|
||||
fn audited_registry_has_expected_current_and_historical_counts() {
|
||||
assert_eq!(super::current_http_rpc_methods().len(), 52);
|
||||
assert_eq!(super::historical_http_rpc_methods().len(), 14);
|
||||
assert_eq!(crate::current_http_rpc_methods().len(), 52);
|
||||
assert_eq!(crate::historical_http_rpc_methods().len(), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audited_registry_method_names_are_unique() {
|
||||
let mut names = std::collections::BTreeSet::<&str>::new();
|
||||
for descriptor in super::current_http_rpc_methods() {
|
||||
for descriptor in crate::current_http_rpc_methods() {
|
||||
assert!(names.insert(descriptor.method()), "duplicate current method {}", descriptor.method());
|
||||
}
|
||||
for descriptor in super::historical_http_rpc_methods() {
|
||||
for descriptor in crate::historical_http_rpc_methods() {
|
||||
assert!(names.insert(descriptor.method()), "duplicate historical method {}", descriptor.method());
|
||||
}
|
||||
assert_eq!(names.len(), 66);
|
||||
@@ -26,13 +26,13 @@ fn coverage_release_counts_match_recalibrated_matrix() {
|
||||
let mut transactions = 0_usize;
|
||||
let mut blocks_economics = 0_usize;
|
||||
let mut historical = 0_usize;
|
||||
for descriptor in super::current_http_rpc_methods() {
|
||||
for descriptor in crate::current_http_rpc_methods() {
|
||||
match descriptor.coverage_release() {
|
||||
super::HttpRpcCoverageRelease::V0_2_1 => foundation += 1,
|
||||
super::HttpRpcCoverageRelease::V0_2_2 => accounts_tokens_cluster += 1,
|
||||
super::HttpRpcCoverageRelease::V0_2_3 => transactions += 1,
|
||||
super::HttpRpcCoverageRelease::V0_2_4 => blocks_economics += 1,
|
||||
super::HttpRpcCoverageRelease::Historical => historical += 1,
|
||||
crate::HttpRpcCoverageRelease::V0_2_1 => foundation += 1,
|
||||
crate::HttpRpcCoverageRelease::V0_2_2 => accounts_tokens_cluster += 1,
|
||||
crate::HttpRpcCoverageRelease::V0_2_3 => transactions += 1,
|
||||
crate::HttpRpcCoverageRelease::V0_2_4 => blocks_economics += 1,
|
||||
crate::HttpRpcCoverageRelease::Historical => historical += 1,
|
||||
}
|
||||
}
|
||||
assert_eq!(historical, 0);
|
||||
@@ -45,8 +45,8 @@ fn coverage_release_counts_match_recalibrated_matrix() {
|
||||
#[test]
|
||||
fn foundation_canary_assignment_is_exact() {
|
||||
let mut names = std::vec::Vec::<&str>::new();
|
||||
for descriptor in super::current_http_rpc_methods() {
|
||||
if descriptor.coverage_release() == super::HttpRpcCoverageRelease::V0_2_1 {
|
||||
for descriptor in crate::current_http_rpc_methods() {
|
||||
if descriptor.coverage_release() == crate::HttpRpcCoverageRelease::V0_2_1 {
|
||||
names.push(descriptor.method());
|
||||
}
|
||||
}
|
||||
@@ -56,62 +56,62 @@ fn foundation_canary_assignment_is_exact() {
|
||||
|
||||
#[test]
|
||||
fn historical_methods_are_deprecated_removed_and_not_retryable() {
|
||||
for descriptor in super::historical_http_rpc_methods() {
|
||||
assert_eq!(descriptor.documentation_status(), super::RpcDocumentationStatus::Deprecated);
|
||||
assert_eq!(descriptor.runtime_status(), super::RpcRuntimeStatus::Removed);
|
||||
assert_eq!(descriptor.transport_retry_class(), super::TransportRetryClass::NotApplicable);
|
||||
assert_eq!(descriptor.coverage_release(), super::HttpRpcCoverageRelease::Historical);
|
||||
for descriptor in crate::historical_http_rpc_methods() {
|
||||
assert_eq!(descriptor.documentation_status(), crate::RpcDocumentationStatus::Deprecated);
|
||||
assert_eq!(descriptor.runtime_status(), crate::RpcRuntimeStatus::Removed);
|
||||
assert_eq!(descriptor.transport_retry_class(), crate::TransportRetryClass::NotApplicable);
|
||||
assert_eq!(descriptor.coverage_release(), crate::HttpRpcCoverageRelease::Historical);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_transaction_and_get_block_track_deprecated_legacy_request_form() {
|
||||
let get_transaction = super::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist");
|
||||
let get_block = super::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist");
|
||||
let get_transaction = crate::find_http_rpc_method("getTransaction").expect("getTransaction descriptor must exist");
|
||||
let get_block = crate::find_http_rpc_method("getBlock").expect("getBlock descriptor must exist");
|
||||
assert!(get_transaction.request_form_status().has_deprecated_legacy());
|
||||
assert!(get_block.request_form_status().has_deprecated_legacy());
|
||||
let get_balance = super::find_http_rpc_method("getBalance").expect("getBalance descriptor must exist");
|
||||
let get_balance = crate::find_http_rpc_method("getBalance").expect("getBalance descriptor must exist");
|
||||
assert!(!get_balance.request_form_status().has_deprecated_legacy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_submission_methods_are_never_retry_after_ambiguous_dispatch() {
|
||||
for method in ["sendTransaction", "requestAirdrop"] {
|
||||
let descriptor = super::find_http_rpc_method(method).expect("write descriptor must exist");
|
||||
assert_eq!(descriptor.operation_kind(), super::RpcOperationKind::WriteSubmission);
|
||||
assert_eq!(descriptor.transport_retry_class(), super::TransportRetryClass::NeverAfterDispatch);
|
||||
let descriptor = crate::find_http_rpc_method(method).expect("write descriptor must exist");
|
||||
assert_eq!(descriptor.operation_kind(), crate::RpcOperationKind::WriteSubmission);
|
||||
assert_eq!(descriptor.transport_retry_class(), crate::TransportRetryClass::NeverAfterDispatch);
|
||||
}
|
||||
let simulation = super::find_http_rpc_method("simulateTransaction").expect("simulation descriptor must exist");
|
||||
assert_eq!(simulation.operation_kind(), super::RpcOperationKind::Simulation);
|
||||
assert_eq!(simulation.transport_retry_class(), super::TransportRetryClass::RetrySafe);
|
||||
let simulation = crate::find_http_rpc_method("simulateTransaction").expect("simulation descriptor must exist");
|
||||
assert_eq!(simulation.operation_kind(), crate::RpcOperationKind::Simulation);
|
||||
assert_eq!(simulation.transport_retry_class(), crate::TransportRetryClass::RetrySafe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_method_support_check_returns_method_removed_error() {
|
||||
let descriptor = super::find_http_rpc_method("confirmTransaction").expect("historical descriptor must exist");
|
||||
let descriptor = crate::find_http_rpc_method("confirmTransaction").expect("historical descriptor must exist");
|
||||
let error = descriptor.ensure_runtime_supported().expect_err("removed method must not be callable");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_METHOD_REMOVED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stable_supported_method_passes_runtime_support_check() {
|
||||
let descriptor = super::find_http_rpc_method("getHealth").expect("current descriptor must exist");
|
||||
let descriptor = crate::find_http_rpc_method("getHealth").expect("current descriptor must exist");
|
||||
assert!(descriptor.ensure_runtime_supported().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supported_unstable_descriptor_executes_central_warning_path() {
|
||||
let descriptor = super::HttpRpcMethodDescriptor::new(
|
||||
let descriptor = crate::HttpRpcMethodDescriptor::new(
|
||||
"experimentalMethod",
|
||||
super::HttpRpcCategory::Cluster,
|
||||
crate::HttpRpcCategory::Cluster,
|
||||
"experimental_method",
|
||||
super::RpcDocumentationStatus::Unstable,
|
||||
super::RpcRuntimeStatus::Supported,
|
||||
super::RpcRequestFormStatus::Stable,
|
||||
super::RpcOperationKind::Read,
|
||||
super::TransportRetryClass::RetrySafe,
|
||||
crate::RpcDocumentationStatus::Unstable,
|
||||
crate::RpcRuntimeStatus::Supported,
|
||||
crate::RpcRequestFormStatus::Stable,
|
||||
crate::RpcOperationKind::Read,
|
||||
crate::TransportRetryClass::RetrySafe,
|
||||
std::option::Option::None,
|
||||
super::HttpRpcCoverageRelease::V0_2_1,
|
||||
crate::HttpRpcCoverageRelease::V0_2_1,
|
||||
);
|
||||
assert!(descriptor.requires_method_usage_warning());
|
||||
assert!(descriptor.ensure_runtime_supported().is_ok());
|
||||
@@ -119,5 +119,5 @@ fn supported_unstable_descriptor_executes_central_warning_path() {
|
||||
|
||||
#[test]
|
||||
fn lookup_rejects_unknown_method_without_affecting_raw_provider_extensions() {
|
||||
assert!(super::find_http_rpc_method("providerCustomMethod").is_none());
|
||||
assert!(crate::find_http_rpc_method("providerCustomMethod").is_none());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/rpc_transactions.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
#[test]
|
||||
fn transaction_encoding_strings_match_current_and_legacy_wire_labels() {
|
||||
@@ -19,7 +19,6 @@ fn wire_field_preserves_omitted_null_and_value_states() {
|
||||
#[serde(default)]
|
||||
field: crate::SolanaWireField<u64>,
|
||||
}
|
||||
|
||||
let omitted = serde_json::from_value::<Holder>(serde_json::json!({})).expect("omitted fixture must decode");
|
||||
let null = serde_json::from_value::<Holder>(serde_json::json!({"field": null})).expect("null fixture must decode");
|
||||
let value = serde_json::from_value::<Holder>(serde_json::json!({"field": 7})).expect("value fixture must decode");
|
||||
@@ -402,7 +401,6 @@ fn serve_transaction_status_then_body(
|
||||
let first_request = read_transaction_request(&mut first_stream);
|
||||
let first_response = format!("HTTP/1.1 {status_line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
||||
std::io::Write::write_all(&mut first_stream, first_response.as_bytes()).expect("fixture first response must write");
|
||||
|
||||
listener.set_nonblocking(true).expect("fixture listener must become nonblocking");
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
|
||||
return loop {
|
||||
@@ -612,7 +610,6 @@ async fn typed_get_recent_prioritization_fees_omits_optional_accounts_and_reject
|
||||
assert_eq!(fees.len(), 2);
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!([]));
|
||||
|
||||
let account = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
let max_accounts = std::vec![account; 128];
|
||||
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_recent_prioritization_fees.success.json"));
|
||||
@@ -625,7 +622,6 @@ async fn typed_get_recent_prioritization_fees_omits_optional_accounts_and_reject
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = transaction_request_body(request.as_str());
|
||||
assert_eq!(body["params"][0].as_array().expect("address parameter must be an array").len(), 128);
|
||||
|
||||
let invalid_pool = transaction_pool_for_url("http://127.0.0.1:9");
|
||||
let accounts = std::vec![account; 129];
|
||||
let result = invalid_pool.get_recent_prioritization_fees(&crate::HttpRoleName::new("default"), std::option::Option::Some(accounts.as_slice())).await;
|
||||
@@ -768,7 +764,6 @@ async fn typed_get_signature_statuses_accepts_256_and_rejects_more_than_256_befo
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = transaction_request_body(request.as_str());
|
||||
assert_eq!(body["params"][0].as_array().expect("signature parameter must be an array").len(), 256);
|
||||
|
||||
let invalid_pool = transaction_pool_for_url("http://127.0.0.1:9");
|
||||
let signatures = std::vec!["signature".to_owned(); 257];
|
||||
let result = invalid_pool.get_signature_statuses(&crate::HttpRoleName::new("default"), signatures.as_slice(), std::option::Option::None).await;
|
||||
@@ -804,7 +799,6 @@ async fn typed_get_transaction_modern_supports_omitted_and_explicit_empty_config
|
||||
assert!(result.is_none());
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["fixture-signature"]));
|
||||
|
||||
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/get_transaction.null.json"));
|
||||
let pool = transaction_pool_for_url(url.as_str());
|
||||
let config = crate::SolanaGetTransactionConfig::default();
|
||||
@@ -1006,7 +1000,6 @@ async fn typed_request_airdrop_exposes_runtime_config_and_canonicalizes_empty_co
|
||||
{"recentBlockhash":"recent-blockhash-fixture","commitment":"finalized"}
|
||||
])
|
||||
);
|
||||
|
||||
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/request_airdrop.success.json"));
|
||||
let pool = transaction_pool_for_url(url.as_str());
|
||||
let empty = crate::SolanaRequestAirdropConfig::default();
|
||||
@@ -1050,7 +1043,6 @@ async fn typed_send_transaction_exposes_all_current_options_and_both_binary_enco
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
let base58 = crate::SolanaSendTransactionConfig::new(
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
@@ -1066,7 +1058,6 @@ async fn typed_send_transaction_exposes_all_current_options_and_both_binary_enco
|
||||
.expect("sendTransaction base58 fixture must succeed");
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["opaque-base58-transaction", {"encoding":"base58"}]));
|
||||
|
||||
let empty = crate::SolanaSendTransactionConfig::default();
|
||||
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.success.json"));
|
||||
let pool = transaction_pool_for_url(url.as_str());
|
||||
@@ -1086,7 +1077,6 @@ async fn typed_write_wrappers_preserve_rpc_application_errors_without_transport_
|
||||
let result = pool.request_airdrop(&crate::HttpRoleName::new("default"), &recipient, 1, std::option::Option::None).await;
|
||||
assert_eq!(result.expect_err("requestAirdrop RPC error must propagate").code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
|
||||
handle.join().expect("fixture server must join");
|
||||
|
||||
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.preflight_error.json"));
|
||||
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3);
|
||||
let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await;
|
||||
@@ -1104,7 +1094,6 @@ async fn typed_write_wrappers_never_resend_after_http_429_dispatch() {
|
||||
let (count, request) = handle.join().expect("fixture server must join");
|
||||
assert_eq!(count, 1);
|
||||
assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("requestAirdrop"));
|
||||
|
||||
let (url, handle) = serve_transaction_status_and_count("429 Too Many Requests");
|
||||
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3);
|
||||
let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await;
|
||||
@@ -1123,7 +1112,6 @@ async fn typed_write_wrappers_never_resend_after_temporary_http_dispatch() {
|
||||
assert_eq!(result.expect_err("airdrop 503 must stop after dispatch").code(), crate::ERROR_CODE_HTTP_REQUEST_FAILED);
|
||||
let (count, _) = handle.join().expect("fixture server must join");
|
||||
assert_eq!(count, 1);
|
||||
|
||||
let (url, handle) = serve_transaction_status_and_count("503 Service Unavailable");
|
||||
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(500), 3);
|
||||
let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await;
|
||||
@@ -1141,7 +1129,6 @@ async fn typed_write_wrappers_never_resend_after_ambiguous_timeout_dispatch() {
|
||||
assert_eq!(result.expect_err("airdrop timeout must stop after ambiguous dispatch").code(), crate::ERROR_CODE_TIMEOUT);
|
||||
let (count, _) = handle.join().expect("fixture server must join");
|
||||
assert_eq!(count, 1);
|
||||
|
||||
let (url, handle) = serve_transaction_timeout_and_count(std::time::Duration::from_millis(80));
|
||||
let pool = transaction_pool_for_urls(&[(url.as_str(), 10)], std::time::Duration::from_millis(20), 3);
|
||||
let result = pool.send_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::None).await;
|
||||
@@ -1163,7 +1150,6 @@ async fn typed_write_wrappers_can_retry_when_connection_failure_proves_not_dispa
|
||||
assert_eq!(result, "airdrop-fixture-signature-111111111111111111111111111111111111111111111111");
|
||||
let request = handle.join().expect("secondary fixture server must join");
|
||||
assert_eq!(transaction_request_body(request.as_str())["method"], serde_json::json!("requestAirdrop"));
|
||||
|
||||
let closed = unused_local_url();
|
||||
let (secondary, handle) = serve_transaction_once(include_str!("../fixtures/http/send_transaction.success.json"));
|
||||
let pool = transaction_pool_for_urls(&[(closed.as_str(), 10), (secondary.as_str(), 10)], std::time::Duration::from_millis(500), 1);
|
||||
@@ -1210,7 +1196,6 @@ async fn typed_simulate_transaction_serializes_complete_config_and_preserves_ric
|
||||
let replacement = response.value().replacement_blockhash().value().expect("replacement blockhash must be present");
|
||||
assert_eq!(replacement.blockhash(), "ComputeBudget111111111111111111111111111111");
|
||||
assert_eq!(replacement.last_valid_block_height(), 430_123_999);
|
||||
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
let body = transaction_request_body(request.as_str());
|
||||
assert_eq!(body["method"], serde_json::json!("simulateTransaction"));
|
||||
@@ -1250,7 +1235,6 @@ async fn typed_simulate_transaction_supports_base58_and_omits_empty_config() {
|
||||
.expect("simulateTransaction base58 fixture must succeed");
|
||||
let request = handle.join().expect("fixture server must join");
|
||||
assert_eq!(transaction_request_body(request.as_str())["params"], serde_json::json!(["opaque-base58-transaction", {"encoding":"base58"}]));
|
||||
|
||||
let empty = crate::SolanaSimulateTransactionConfig::default();
|
||||
let (url, handle) = serve_transaction_once(include_str!("../fixtures/http/simulate_transaction.partial.json"));
|
||||
let pool = transaction_pool_for_url(url.as_str());
|
||||
@@ -1303,7 +1287,6 @@ async fn typed_simulate_transaction_rejects_deterministic_invalid_configs_before
|
||||
);
|
||||
let result = pool.simulate_transaction(&crate::HttpRoleName::new("default"), "opaque-transaction", std::option::Option::Some(&conflicting)).await;
|
||||
assert_eq!(result.expect_err("conflicting simulation flags must fail before I/O").code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||
|
||||
let address = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||
let rejected_encodings = [crate::SolanaAccountEncoding::Binary, crate::SolanaAccountEncoding::Base58];
|
||||
for encoding in rejected_encodings {
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
// file: crates/ksp-onchain-transport-lib/unit_tests/settings.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
fn non_zero(value: u32) -> std::num::NonZeroU32 {
|
||||
return std::num::NonZeroU32::new(value).expect("test non-zero value must remain non-zero");
|
||||
}
|
||||
|
||||
fn valid_settings(url_text: &str) -> super::HttpTransportSettings {
|
||||
let url = super::HttpEndpointUrl::parse(url_text).expect("test URL must be valid");
|
||||
let limits = super::HttpRoleLimits::new(
|
||||
fn valid_settings(url_text: &str) -> crate::HttpTransportSettings {
|
||||
let url = crate::HttpEndpointUrl::parse(url_text).expect("test URL must be valid");
|
||||
let limits = crate::HttpRoleLimits::new(
|
||||
std::option::Option::Some(non_zero(10)),
|
||||
std::option::Option::Some(non_zero(20)),
|
||||
std::option::Option::Some(non_zero(4)),
|
||||
std::option::Option::Some(std::time::Duration::from_millis(500)),
|
||||
);
|
||||
let role = super::HttpEndpointRoleSettings::new(super::HttpRoleName::new("default"), true, std::vec![super::HttpRequestKind::wildcard()], 100, limits);
|
||||
let endpoint = super::HttpEndpointSettings::new(
|
||||
let role = crate::HttpEndpointRoleSettings::new(crate::HttpRoleName::new("default"), true, std::vec![crate::HttpRequestKind::wildcard()], 100, limits);
|
||||
let endpoint = crate::HttpEndpointSettings::new(
|
||||
"devnet_public",
|
||||
true,
|
||||
super::HttpProviderName::new("solana-public"),
|
||||
super::HttpClusterName::new("devnet"),
|
||||
crate::HttpProviderName::new("solana-public"),
|
||||
crate::HttpClusterName::new("devnet"),
|
||||
url,
|
||||
std::time::Duration::from_secs(5),
|
||||
std::time::Duration::from_secs(15),
|
||||
std::option::Option::Some(8),
|
||||
std::vec![role],
|
||||
);
|
||||
return super::HttpTransportSettings::new(
|
||||
return crate::HttpTransportSettings::new(
|
||||
std::vec![endpoint],
|
||||
super::HttpRetrySettings::new(2, std::time::Duration::from_millis(100), std::time::Duration::from_secs(2)),
|
||||
crate::HttpRetrySettings::new(2, std::time::Duration::from_millis(100), std::time::Duration::from_secs(2)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_url_accepts_http_and_https() {
|
||||
assert!(super::HttpEndpointUrl::parse("https://api.devnet.solana.com").is_ok());
|
||||
assert!(super::HttpEndpointUrl::parse("http://127.0.0.1:8899").is_ok());
|
||||
assert!(crate::HttpEndpointUrl::parse("https://api.devnet.solana.com").is_ok());
|
||||
assert!(crate::HttpEndpointUrl::parse("http://127.0.0.1:8899").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_url_rejects_non_http_schemes() {
|
||||
let result = super::HttpEndpointUrl::parse("ws://api.devnet.solana.com");
|
||||
let result = crate::HttpEndpointUrl::parse("ws://api.devnet.solana.com");
|
||||
let error = result.expect_err("WebSocket URL must not be accepted by HTTP settings");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_url_debug_redacts_secret_material() {
|
||||
let url = super::HttpEndpointUrl::parse("https://provider.invalid/rpc?api-key=SECRET-CANARY").expect("test URL must parse");
|
||||
let url = crate::HttpEndpointUrl::parse("https://provider.invalid/rpc?api-key=SECRET-CANARY").expect("test URL must parse");
|
||||
let rendered = format!("{url:?}");
|
||||
assert!(rendered.contains("<redacted>"));
|
||||
assert!(!rendered.contains("SECRET-CANARY"));
|
||||
@@ -70,28 +70,28 @@ fn transport_settings_debug_does_not_leak_endpoint_url() {
|
||||
|
||||
#[test]
|
||||
fn transport_settings_require_one_enabled_endpoint() {
|
||||
let url = super::HttpEndpointUrl::parse("https://api.devnet.solana.com").expect("test URL must parse");
|
||||
let role = super::HttpEndpointRoleSettings::new(
|
||||
super::HttpRoleName::new("default"),
|
||||
let url = crate::HttpEndpointUrl::parse("https://api.devnet.solana.com").expect("test URL must parse");
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![super::HttpRequestKind::wildcard()],
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
100,
|
||||
super::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::None, std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
let endpoint = super::HttpEndpointSettings::new(
|
||||
let endpoint = crate::HttpEndpointSettings::new(
|
||||
"disabled",
|
||||
false,
|
||||
super::HttpProviderName::new("provider"),
|
||||
super::HttpClusterName::new("devnet"),
|
||||
crate::HttpProviderName::new("provider"),
|
||||
crate::HttpClusterName::new("devnet"),
|
||||
url,
|
||||
std::time::Duration::from_secs(1),
|
||||
std::time::Duration::from_secs(1),
|
||||
std::option::Option::None,
|
||||
std::vec![role],
|
||||
);
|
||||
let settings = super::HttpTransportSettings::new(
|
||||
let settings = crate::HttpTransportSettings::new(
|
||||
std::vec![endpoint],
|
||||
super::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)),
|
||||
crate::HttpRetrySettings::new(1, std::time::Duration::from_millis(1), std::time::Duration::from_millis(2)),
|
||||
);
|
||||
let error = settings.validate().expect_err("all-disabled settings must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||
@@ -101,7 +101,7 @@ fn transport_settings_require_one_enabled_endpoint() {
|
||||
fn transport_settings_reject_duplicate_endpoint_names() {
|
||||
let first = valid_settings("https://one.invalid");
|
||||
let second = valid_settings("https://two.invalid");
|
||||
let settings = super::HttpTransportSettings::new(std::vec![first.endpoints()[0].clone(), second.endpoints()[0].clone()], first.retry().clone());
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![first.endpoints()[0].clone(), second.endpoints()[0].clone()], first.retry().clone());
|
||||
let error = settings.validate().expect_err("duplicate endpoint names must fail");
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||
}
|
||||
@@ -110,7 +110,7 @@ fn transport_settings_reject_duplicate_endpoint_names() {
|
||||
fn transport_settings_reject_duplicate_roles() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let endpoint = &base.endpoints()[0];
|
||||
let duplicated_endpoint = super::HttpEndpointSettings::new(
|
||||
let duplicated_endpoint = crate::HttpEndpointSettings::new(
|
||||
endpoint.name(),
|
||||
true,
|
||||
endpoint.provider().clone(),
|
||||
@@ -121,7 +121,7 @@ fn transport_settings_reject_duplicate_roles() {
|
||||
endpoint.max_idle_connections_per_host(),
|
||||
std::vec![endpoint.roles()[0].clone(), endpoint.roles()[0].clone()],
|
||||
);
|
||||
let settings = super::HttpTransportSettings::new(std::vec![duplicated_endpoint], base.retry().clone());
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![duplicated_endpoint], base.retry().clone());
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
@@ -129,14 +129,14 @@ fn transport_settings_reject_duplicate_roles() {
|
||||
fn transport_settings_reject_wildcard_mixed_with_specific_kind() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let endpoint = &base.endpoints()[0];
|
||||
let role = super::HttpEndpointRoleSettings::new(
|
||||
super::HttpRoleName::new("default"),
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![super::HttpRequestKind::wildcard(), super::HttpRequestKind::new("get_balance")],
|
||||
std::vec![crate::HttpRequestKind::wildcard(), crate::HttpRequestKind::new("get_balance")],
|
||||
100,
|
||||
endpoint.roles()[0].limits().clone(),
|
||||
);
|
||||
let modified_endpoint = super::HttpEndpointSettings::new(
|
||||
let modified_endpoint = crate::HttpEndpointSettings::new(
|
||||
endpoint.name(),
|
||||
true,
|
||||
endpoint.provider().clone(),
|
||||
@@ -147,7 +147,7 @@ fn transport_settings_reject_wildcard_mixed_with_specific_kind() {
|
||||
endpoint.max_idle_connections_per_host(),
|
||||
std::vec![role],
|
||||
);
|
||||
let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
@@ -155,14 +155,14 @@ fn transport_settings_reject_wildcard_mixed_with_specific_kind() {
|
||||
fn transport_settings_reject_burst_without_rps() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let endpoint = &base.endpoints()[0];
|
||||
let role = super::HttpEndpointRoleSettings::new(
|
||||
super::HttpRoleName::new("default"),
|
||||
let role = crate::HttpEndpointRoleSettings::new(
|
||||
crate::HttpRoleName::new("default"),
|
||||
true,
|
||||
std::vec![super::HttpRequestKind::wildcard()],
|
||||
std::vec![crate::HttpRequestKind::wildcard()],
|
||||
100,
|
||||
super::HttpRoleLimits::new(std::option::Option::None, std::option::Option::Some(non_zero(2)), std::option::Option::None, std::option::Option::None),
|
||||
crate::HttpRoleLimits::new(std::option::Option::None, std::option::Option::Some(non_zero(2)), std::option::Option::None, std::option::Option::None),
|
||||
);
|
||||
let modified_endpoint = super::HttpEndpointSettings::new(
|
||||
let modified_endpoint = crate::HttpEndpointSettings::new(
|
||||
endpoint.name(),
|
||||
true,
|
||||
endpoint.provider().clone(),
|
||||
@@ -173,16 +173,16 @@ fn transport_settings_reject_burst_without_rps() {
|
||||
endpoint.max_idle_connections_per_host(),
|
||||
std::vec![role],
|
||||
);
|
||||
let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_settings_reject_reversed_retry_backoff() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let settings = super::HttpTransportSettings::new(
|
||||
let settings = crate::HttpTransportSettings::new(
|
||||
base.endpoints().to_vec(),
|
||||
super::HttpRetrySettings::new(2, std::time::Duration::from_secs(2), std::time::Duration::from_secs(1)),
|
||||
crate::HttpRetrySettings::new(2, std::time::Duration::from_secs(2), std::time::Duration::from_secs(1)),
|
||||
);
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
@@ -191,7 +191,7 @@ fn transport_settings_reject_reversed_retry_backoff() {
|
||||
fn transport_settings_reject_zero_request_timeout() {
|
||||
let base = valid_settings("https://api.devnet.solana.com");
|
||||
let endpoint = &base.endpoints()[0];
|
||||
let modified_endpoint = super::HttpEndpointSettings::new(
|
||||
let modified_endpoint = crate::HttpEndpointSettings::new(
|
||||
endpoint.name(),
|
||||
true,
|
||||
endpoint.provider().clone(),
|
||||
@@ -202,6 +202,6 @@ fn transport_settings_reject_zero_request_timeout() {
|
||||
endpoint.max_idle_connections_per_host(),
|
||||
endpoint.roles().to_vec(),
|
||||
);
|
||||
let settings = super::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
let settings = crate::HttpTransportSettings::new(std::vec![modified_endpoint], base.retry().clone());
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
26
crates/ksp-wallet-lib/Cargo.toml
Normal file
26
crates/ksp-wallet-lib/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
# file: crates/ksp-wallet-lib/Cargo.toml
|
||||
# version: 6
|
||||
|
||||
[package]
|
||||
name = "ksp-wallet-lib"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
argon2 = { workspace = true, features = ["alloc", "zeroize"] }
|
||||
chacha20poly1305 = { workspace = true, features = ["alloc", "zeroize"] }
|
||||
ed25519-dalek = { workspace = true, features = ["signature", "zeroize"] }
|
||||
getrandom.workspace = true
|
||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||
base64.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
solana-keypair.workspace = true
|
||||
tokio = { workspace = true, features = ["rt"] }
|
||||
tempfile.workspace = true
|
||||
zeroize.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
194
crates/ksp-wallet-lib/README.md
Normal file
194
crates/ksp-wallet-lib/README.md
Normal file
@@ -0,0 +1,194 @@
|
||||
<!-- file: crates/ksp-wallet-lib/README.md -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# `ksp-wallet-lib`
|
||||
|
||||
Statut : **stable depuis KSP `0.2.5`**.
|
||||
|
||||
`ksp-wallet-lib` est la bibliothèque KSP propriétaire du Wallet Solana natif. Elle possède le format autonome `.kspwallet` V1, les capacités indépendantes VIEW/OWNER, la protection du secret Solana, la signature, l'administration des metadata, les rotations de credentials, la persistence native et les adapters d'import/export explicitement supportés.
|
||||
|
||||
La crate est volontairement indépendante de Config, du réseau et de Tauri. Un consumer fournit les chemins, passwords et metadata ; Wallet ouvre, protège, signe et persiste sans décider d'une policy de dépense ni contacter un RPC.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
La crate possède :
|
||||
|
||||
- le format natif `.kspwallet` V1 et son parser JSON strict ;
|
||||
- les key slots OWNER/VIEW indépendants ;
|
||||
- Argon2id pour les KDF de passwords ;
|
||||
- XChaCha20-Poly1305 pour le wrapping et les compartiments ;
|
||||
- une autorité Ed25519 d'administration distincte de la keypair Solana ;
|
||||
- les compartiments `owner_control`, `metadata` et `secret` ;
|
||||
- la création et l'ouverture in-memory ;
|
||||
- la création et l'ouverture fichier async-first ;
|
||||
- la projection verrouillée minimale ;
|
||||
- la signature Solana via OWNER sans getter secret ;
|
||||
- l'alias et les notes protégés ;
|
||||
- les rotations OWNER/VIEW ;
|
||||
- la révocation forte VIEW par OWNER ;
|
||||
- la persistence create/import no-clobber ;
|
||||
- le remplacement administratif capability-bound avec détection de handle stale ;
|
||||
- l'inspection/import/export Solana CLI JSON et Base58 de keypair complète ;
|
||||
- les diagnostics et erreurs Wallet sans exposition de secrets.
|
||||
|
||||
La crate ne possède pas :
|
||||
|
||||
```text
|
||||
Config
|
||||
Transport HTTP/WebSocket/gRPC
|
||||
balance/réseau
|
||||
WalletPolicy / execution policy
|
||||
construction ou envoi de transaction
|
||||
Store
|
||||
Tauri/UI
|
||||
hardware wallet / remote signer
|
||||
anti-rollback externe
|
||||
```
|
||||
|
||||
## Frontières de dépendances
|
||||
|
||||
La direction de production est :
|
||||
|
||||
```text
|
||||
ksp-wallet-lib
|
||||
-> ksp-core-lib
|
||||
-> ksp-logging-lib
|
||||
-> argon2 / chacha20poly1305 / getrandom
|
||||
-> ed25519-dalek / solana-keypair
|
||||
-> serde / serde_json
|
||||
-> tempfile / tokio
|
||||
-> zeroize
|
||||
```
|
||||
|
||||
Les dépendances suivantes sont interdites :
|
||||
|
||||
```text
|
||||
ksp-wallet-lib -X-> ksp-config-lib
|
||||
ksp-wallet-lib -X-> ksp-onchain-transport-lib
|
||||
ksp-wallet-lib -X-> execution policy
|
||||
ksp-wallet-lib -X-> Store
|
||||
ksp-wallet-lib -X-> Tauri
|
||||
ksp-wallet-lib -X-> tracing direct
|
||||
ksp-wallet-lib -X-> std::env
|
||||
ksp-wallet-lib -X-> solana-pubkey direct
|
||||
ksp-wallet-lib -X-> solana-signer direct
|
||||
ksp-wallet-lib -X-> solana-signature direct
|
||||
```
|
||||
|
||||
La Pubkey publique est toujours `ksp_core_lib::Pubkey`. `solana-keypair` reste un détail secret/signing interne à Wallet et n'est pas réexportée.
|
||||
|
||||
## Modèle de capacités
|
||||
|
||||
### Verrouillé
|
||||
|
||||
Sans password, `inspect_locked_wallet_v1` et `inspect_locked_wallet_file_v1` exposent uniquement :
|
||||
|
||||
```text
|
||||
format_version
|
||||
view_enabled
|
||||
```
|
||||
|
||||
La Pubkey, l'alias et les notes restent chiffrés.
|
||||
|
||||
### VIEW
|
||||
|
||||
`WalletView` expose :
|
||||
|
||||
```text
|
||||
Pubkey
|
||||
alias
|
||||
notes
|
||||
rotation de son propre password VIEW
|
||||
```
|
||||
|
||||
VIEW ne possède aucune API de signature, d'export secret, de mutation metadata, de rotation OWNER ou de désactivation/recréation VIEW.
|
||||
|
||||
### OWNER
|
||||
|
||||
`WalletOwner` expose toutes les metadata autorisées et ajoute :
|
||||
|
||||
```text
|
||||
signature Solana
|
||||
export du secret via adapters explicites
|
||||
alias/notes administration
|
||||
rotation OWNER
|
||||
rotation VIEW sans ancien password VIEW
|
||||
disable VIEW avec rekey metadata fort
|
||||
recreate VIEW avec nouveau slot et nouveau K_metadata
|
||||
```
|
||||
|
||||
OWNER ne dépend jamais du password VIEW.
|
||||
|
||||
## Format `.kspwallet` V1
|
||||
|
||||
Le format est publiquement spécifié dans [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md). Il est autonome : un fichier valide et le password de la capacité concernée suffisent à l'ouverture ; aucun pepper KSP, keychain, OTP, service distant, réseau ou secret externe n'est requis.
|
||||
|
||||
Les principales primitives sont :
|
||||
|
||||
```text
|
||||
KDF Argon2id v19
|
||||
profil création 65 536 KiB / 3 iterations / 1 lane
|
||||
AEAD XChaCha20-Poly1305
|
||||
CSPRNG OS via getrandom
|
||||
state signature Ed25519
|
||||
wire binary Base64url sans padding
|
||||
format JSON UTF-8 strict
|
||||
```
|
||||
|
||||
Les paramètres KDF sont sérialisés dans chaque slot afin que de futurs defaults puissent évoluer sans rendre les wallets existants illisibles.
|
||||
|
||||
## Persistence
|
||||
|
||||
Toute création/import reçoit un chemin explicite du caller et applique le no-clobber. Wallet ne découvre ni ne crée un répertoire configuré par lui-même.
|
||||
|
||||
Les mutations OWNER/VIEW persistées utilisent un remplacement capability-bound : le fichier courant doit encore correspondre à l'enveloppe authentifiée attendue par le handle. Un handle stale ou une mauvaise cible retourne `wallet.state_conflict`.
|
||||
|
||||
Cette protection ne constitue pas un CAS filesystem linéarisable et ne fournit pas d'anti-rollback externe. Les garanties d'atomicité/crash-durability dépendent également de l'OS et du filesystem.
|
||||
|
||||
## Import/export
|
||||
|
||||
Les formats built-in V1 sont :
|
||||
|
||||
```text
|
||||
WalletTransferFormat::SolanaCliJson
|
||||
WalletTransferFormat::SolanaKeypairBase58
|
||||
```
|
||||
|
||||
L'inspection d'un transfert retourne seulement sa Pubkey et son format. L'import crée toujours un nouveau `.kspwallet` no-clobber autour de la keypair validée. L'export secret appartient uniquement à `WalletOwner`.
|
||||
|
||||
`WalletTransferFormat` est `#[non_exhaustive]` afin de permettre des formats built-in supplémentaires sans prétendre fournir un plugin public arbitraire de codec secret.
|
||||
|
||||
## Sécurité et limites
|
||||
|
||||
Le threat model V1 considère notamment un attaquant possédant une copie complète du fichier et capable d'essais de password offline sans limite serveur. Argon2id augmente le coût de chaque essai ; il ne compense pas un password faible.
|
||||
|
||||
Limites explicitement assumées :
|
||||
|
||||
- remplacement total par un autre wallet valide non détectable depuis le nouveau fichier seul ;
|
||||
- rollback vers une ancienne copie valide non détectable sans état/ancre externe ;
|
||||
- absence de second facteur, keychain, hardware wallet ou remote signer en V1 ;
|
||||
- `zeroize` réduit les copies possédées mais ne prouve pas l'effacement physique de toute copie potentielle ;
|
||||
- les permissions filesystem sont une hygiène externe, pas une garantie cryptographique.
|
||||
|
||||
La matrice durable [`../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) enregistre les canaris adversariaux, l'interop indépendante et l'audit Cargo.
|
||||
|
||||
## Tests et vecteurs
|
||||
|
||||
Les fixtures publiques test-only sont sous `tests/fixtures/` :
|
||||
|
||||
```text
|
||||
kspwallet_v1_wire_only.json
|
||||
kspwallet_v1_crypto_vectors.json
|
||||
kspwallet_v1_full_vector.json
|
||||
kspwallet_v1_full_vector_meta.json
|
||||
```
|
||||
|
||||
Elles couvrent le wire, Argon2id/XChaCha20-Poly1305, l'ouverture VIEW/OWNER, la signature d'état et l'interop transfer. Les tests adversariaux couvrent tampering, wrong passwords, frontières VIEW/OWNER, persistence, concurrence et diagnostics secrets.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [`USAGE.md`](USAGE.md) — exemples des principales surfaces publiques ;
|
||||
- [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md) — spécification normative indépendante de Rust ;
|
||||
- [`../../docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](../../docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md) — plan historique et threat model de `0.2.5` ;
|
||||
- [`../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](../../docs/validation/008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) — matrice de sécurité/interoperabilité/compliance ;
|
||||
- [`../../prompts/011-V0_2_6_START_PROMPT.md`](../../prompts/011-V0_2_6_START_PROMPT.md) — reprise vers Wallet Desk après publication stable de `0.2.5`.
|
||||
279
crates/ksp-wallet-lib/USAGE.md
Normal file
279
crates/ksp-wallet-lib/USAGE.md
Normal file
@@ -0,0 +1,279 @@
|
||||
<!-- file: crates/ksp-wallet-lib/USAGE.md -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# Utilisation de `ksp-wallet-lib`
|
||||
|
||||
Ce guide présente les principales surfaces publiques de Wallet V1. La spécification cryptographique du fichier reste [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md).
|
||||
|
||||
Les exemples utilisent des chemins explicites : Wallet ne lit ni Config ni environnement pour découvrir un répertoire.
|
||||
|
||||
## 1. Créer un nouveau `.kspwallet`
|
||||
|
||||
```rust
|
||||
async fn create_example() -> ksp_core_lib::Result<()> {
|
||||
let metadata = ksp_wallet_lib::WalletCreateMetadataV1::new(
|
||||
Some(std::string::String::from("devnet-main")),
|
||||
vec![std::string::String::from("wallet de test")],
|
||||
);
|
||||
let owner_password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD"));
|
||||
let view_password = ksp_wallet_lib::ViewPassword::new(std::string::String::from("VIEW-PASSWORD"));
|
||||
let created = ksp_wallet_lib::create_wallet_file_v1(
|
||||
"wallets/devnet-main.kspwallet",
|
||||
owner_password,
|
||||
Some(view_password),
|
||||
metadata,
|
||||
)
|
||||
.await;
|
||||
let owner = match created {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
println!("{}", owner.pubkey());
|
||||
return Ok(());
|
||||
}
|
||||
```
|
||||
|
||||
La destination doit avoir un parent existant. Une destination existante n'est jamais remplacée par une création.
|
||||
|
||||
Pour créer uniquement en mémoire, utiliser `create_wallet_v1` puis `WalletOwner::to_json_bytes()` si le caller possède lui-même une autre boundary de stockage.
|
||||
|
||||
## 2. Inspecter un wallet verrouillé
|
||||
|
||||
```rust
|
||||
async fn inspect_example() -> ksp_core_lib::Result<()> {
|
||||
let inspected = ksp_wallet_lib::inspect_locked_wallet_file_v1("wallets/devnet-main.kspwallet").await;
|
||||
let locked = match inspected {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
println!("format={}", locked.format_version());
|
||||
println!("view_enabled={}", locked.view_enabled());
|
||||
return Ok(());
|
||||
}
|
||||
```
|
||||
|
||||
Cette projection ne contient volontairement ni Pubkey, ni alias, ni notes.
|
||||
|
||||
## 3. Ouvrir avec VIEW
|
||||
|
||||
```rust
|
||||
async fn open_view_example() -> ksp_core_lib::Result<()> {
|
||||
let password = ksp_wallet_lib::ViewPassword::new(std::string::String::from("VIEW-PASSWORD"));
|
||||
let opened = ksp_wallet_lib::open_wallet_view_file_v1("wallets/devnet-main.kspwallet", password).await;
|
||||
let mut view = match opened {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
println!("pubkey={}", view.pubkey());
|
||||
println!("alias={:?}", view.alias());
|
||||
for note in view.notes() {
|
||||
println!("note={} text={}", note.id(), note.text());
|
||||
}
|
||||
let rotated = view
|
||||
.rotate_view_password(
|
||||
"wallets/devnet-main.kspwallet",
|
||||
ksp_wallet_lib::ViewPassword::new(std::string::String::from("NEW-VIEW-PASSWORD")),
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = rotated {
|
||||
return Err(error);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
```
|
||||
|
||||
VIEW ne possède aucune API `sign`, `export_transfer`, `update_alias`, `add_note`, `rotate_owner_password`, `disable_view` ou `recreate_view`.
|
||||
|
||||
## 4. Ouvrir avec OWNER et signer
|
||||
|
||||
```rust
|
||||
async fn sign_example(message: &[u8]) -> ksp_core_lib::Result<[u8; ksp_wallet_lib::KSPWALLET_SOLANA_SIGNATURE_BYTES]> {
|
||||
let password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD"));
|
||||
let opened = ksp_wallet_lib::open_wallet_owner_file_v1("wallets/devnet-main.kspwallet", password).await;
|
||||
let owner = match opened {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
return owner.sign(message);
|
||||
}
|
||||
```
|
||||
|
||||
La signature retournée contient 64 octets Ed25519. Aucun getter public ne retourne la keypair Solana.
|
||||
|
||||
## 5. Administrer alias et notes
|
||||
|
||||
```rust
|
||||
async fn metadata_example() -> ksp_core_lib::Result<()> {
|
||||
let password = ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD"));
|
||||
let opened = ksp_wallet_lib::open_wallet_owner_file_v1("wallets/devnet-main.kspwallet", password).await;
|
||||
let mut owner = match opened {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let alias_result = owner
|
||||
.update_alias("wallets/devnet-main.kspwallet", Some(std::string::String::from("primary-devnet")))
|
||||
.await;
|
||||
if let Err(error) = alias_result {
|
||||
return Err(error);
|
||||
}
|
||||
let added = owner
|
||||
.add_note("wallets/devnet-main.kspwallet", std::string::String::from("rotation trimestrielle"))
|
||||
.await;
|
||||
let note_id = match added {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let updated = owner
|
||||
.update_note(
|
||||
"wallets/devnet-main.kspwallet",
|
||||
note_id.as_str(),
|
||||
std::string::String::from("rotation contrôlée"),
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = updated {
|
||||
return Err(error);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
```
|
||||
|
||||
Chaque mutation vérifie que le fichier courant correspond encore à l'état authentifié attendu par le handle. Un handle stale reçoit `wallet.state_conflict`.
|
||||
|
||||
## 6. Rotations de credentials
|
||||
|
||||
OWNER peut changer son password :
|
||||
|
||||
```rust
|
||||
let result = owner
|
||||
.rotate_owner_password(
|
||||
"wallets/devnet-main.kspwallet",
|
||||
ksp_wallet_lib::OwnerPassword::new(std::string::String::from("NEW-OWNER-PASSWORD")),
|
||||
)
|
||||
.await;
|
||||
```
|
||||
|
||||
OWNER peut aussi changer le password VIEW sans connaître l'ancien :
|
||||
|
||||
```rust
|
||||
let result = owner
|
||||
.rotate_view_password(
|
||||
"wallets/devnet-main.kspwallet",
|
||||
ksp_wallet_lib::ViewPassword::new(std::string::String::from("NEW-VIEW-PASSWORD")),
|
||||
)
|
||||
.await;
|
||||
```
|
||||
|
||||
Ces rotations de credentials ne changent pas la keypair Solana.
|
||||
|
||||
## 7. Révocation forte VIEW
|
||||
|
||||
La simple rotation VIEW remplace le credential courant mais ne peut pas retirer des metadata déjà connues d'un ancien détenteur VIEW. OWNER peut effectuer une révocation forte pour les metadata futures :
|
||||
|
||||
```rust
|
||||
let disabled = owner.disable_view("wallets/devnet-main.kspwallet").await;
|
||||
if let Err(error) = disabled {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let recreated = owner
|
||||
.recreate_view(
|
||||
"wallets/devnet-main.kspwallet",
|
||||
ksp_wallet_lib::ViewPassword::new(std::string::String::from("FRESH-VIEW-PASSWORD")),
|
||||
)
|
||||
.await;
|
||||
```
|
||||
|
||||
`disable_view` rekey les metadata. `recreate_view` crée ensuite un nouveau slot VIEW autour du nouveau `K_metadata`. La keypair Solana reste inchangée.
|
||||
|
||||
## 8. Inspecter et importer une keypair externe
|
||||
|
||||
Le format doit être choisi explicitement ; Wallet ne fait pas d'auto-détection heuristique.
|
||||
|
||||
```rust
|
||||
async fn inspect_transfer_example(source: &[u8]) -> ksp_core_lib::Result<()> {
|
||||
let inspected = ksp_wallet_lib::inspect_wallet_transfer(
|
||||
source,
|
||||
ksp_wallet_lib::WalletTransferFormat::SolanaCliJson,
|
||||
);
|
||||
let info = match inspected {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
println!("{}", info.pubkey());
|
||||
return Ok(());
|
||||
}
|
||||
```
|
||||
|
||||
Import fichier vers un nouveau `.kspwallet` :
|
||||
|
||||
```rust
|
||||
let imported = ksp_wallet_lib::import_wallet_transfer_file_v1(
|
||||
"wallets/imported.kspwallet",
|
||||
"wallets/legacy-id.json",
|
||||
ksp_wallet_lib::WalletTransferFormat::SolanaCliJson,
|
||||
ksp_wallet_lib::OwnerPassword::new(std::string::String::from("OWNER-PASSWORD")),
|
||||
None,
|
||||
ksp_wallet_lib::WalletCreateMetadataV1::new(Some(std::string::String::from("imported")), vec![]),
|
||||
)
|
||||
.await;
|
||||
```
|
||||
|
||||
L'import valide la cohérence secret/public, conserve exactement la keypair Solana et génère un nouvel environnement cryptographique KSP. Une destination native existante n'est jamais écrasée.
|
||||
|
||||
## 9. Exporter avec OWNER
|
||||
|
||||
En mémoire :
|
||||
|
||||
```rust
|
||||
let exported = owner.export_transfer(ksp_wallet_lib::WalletTransferFormat::SolanaKeypairBase58);
|
||||
```
|
||||
|
||||
Vers un fichier no-clobber :
|
||||
|
||||
```rust
|
||||
let exported = owner
|
||||
.export_transfer_file(
|
||||
"exports/devnet-main.keypair",
|
||||
ksp_wallet_lib::WalletTransferFormat::SolanaCliJson,
|
||||
)
|
||||
.await;
|
||||
```
|
||||
|
||||
Les octets retournés par `export_transfer` contiennent volontairement le secret. Le caller doit limiter leur durée de vie et les zeroize lorsqu'approprié. Sur Unix, l'export fichier tente `0600`, qui reste une hygiène filesystem et non une garantie cryptographique.
|
||||
|
||||
## 10. Surfaces in-memory
|
||||
|
||||
Les équivalents sans I/O filesystem sont :
|
||||
|
||||
```text
|
||||
create_wallet_v1
|
||||
open_wallet_view_v1
|
||||
open_wallet_owner_v1
|
||||
inspect_locked_wallet_v1
|
||||
inspect_wallet_transfer
|
||||
```
|
||||
|
||||
Les handles `WalletOwner` et `WalletView` peuvent être sérialisés vers le document natif courant avec `to_json_bytes()`. Ces bytes restent un `.kspwallet` chiffré, pas un export de la keypair.
|
||||
|
||||
## 11. Erreurs et diagnostics
|
||||
|
||||
Les erreurs Wallet sont des `ksp_core_lib::Error` avec codes `wallet.*`. Les erreurs de password restent génériques (`owner_unlock_failed`, `view_unlock_failed`) et ne doivent jamais être transformées en oracle détaillant KDF/wrap/ciphertext.
|
||||
|
||||
Les `Debug` des passwords et metadata protégées sont redacted. Les logs Wallet ne doivent contenir ni password, ni keypair, ni payload exporté.
|
||||
|
||||
## 12. Intégration avec Config et Transport
|
||||
|
||||
Wallet lui-même ne dépend ni de Config ni de Transport. Une application compose explicitement les couches :
|
||||
|
||||
```text
|
||||
ksp-config-lib
|
||||
-> fournit chemins/profils applicatifs
|
||||
|
||||
ksp-wallet-lib
|
||||
-> ouvre le wallet et fournit la Pubkey autorisée
|
||||
|
||||
ksp-onchain-transport-lib
|
||||
-> utilise cette Pubkey pour getBalance et autres lectures réseau
|
||||
```
|
||||
|
||||
Cette composition est le rôle de `0.2.6 — ksp-app-wallet-desk`, pas de `ksp-wallet-lib`.
|
||||
15
crates/ksp-wallet-lib/src/capability.rs
Normal file
15
crates/ksp-wallet-lib/src/capability.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
// file: crates/ksp-wallet-lib/src/capability.rs
|
||||
// version: 1
|
||||
|
||||
/// Authorized capability represented by an unlocked Wallet handle.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum WalletCapability {
|
||||
/// Metadata access plus rotation of the current VIEW password only.
|
||||
View,
|
||||
/// Full Wallet ownership including signing and administration.
|
||||
Owner,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/capability.rs"]
|
||||
mod tests;
|
||||
90
crates/ksp-wallet-lib/src/constants.rs
Normal file
90
crates/ksp-wallet-lib/src/constants.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
// file: crates/ksp-wallet-lib/src/constants.rs
|
||||
// version: 7
|
||||
|
||||
//! Wallet-owned constants.
|
||||
|
||||
/// Native Wallet format version implemented by the V1 codec.
|
||||
pub const KSPWALLET_FORMAT_VERSION_V1: u32 = 1;
|
||||
/// Exact magic string required by every native `.kspwallet` document.
|
||||
pub const KSPWALLET_MAGIC: &str = "KSPWALLET";
|
||||
/// Maximum accepted `.kspwallet` document size before JSON parsing.
|
||||
pub const KSPWALLET_MAX_FILE_BYTES: usize = 1024 * 1024;
|
||||
/// Byte length of every Solana Ed25519 message signature returned by Wallet OWNER.
|
||||
pub const KSPWALLET_SOLANA_SIGNATURE_BYTES: usize = 64;
|
||||
/// Maximum accepted canonical Base58 keypair transfer size before decoding.
|
||||
pub const KSPWALLET_TRANSFER_MAX_BASE58_BYTES: usize = 128;
|
||||
/// Maximum accepted Solana CLI keypair JSON transfer size before parsing.
|
||||
pub const KSPWALLET_TRANSFER_MAX_SOLANA_CLI_JSON_BYTES: usize = 1024;
|
||||
/// Byte length of the Poly1305 authentication tag appended to each ciphertext.
|
||||
pub const KSPWALLET_V1_AEAD_TAG_BYTES: usize = 16;
|
||||
/// Argon2 version serialized by V1 key slots.
|
||||
pub const KSPWALLET_V1_ARGON2_VERSION: u32 = 19;
|
||||
/// Default Argon2id iteration count for newly created V1 key slots.
|
||||
pub const KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS: u32 = 3;
|
||||
/// Default Argon2id memory cost for newly created V1 key slots, calibrated on the 2026-08-19 operator benchmark.
|
||||
pub const KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB: u32 = 65_536;
|
||||
/// Default Argon2id parallelism for newly created V1 key slots.
|
||||
pub const KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM: u32 = 1;
|
||||
/// Salt size generated independently for every newly created V1 key slot.
|
||||
pub const KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES: usize = 32;
|
||||
/// Byte length of an Ed25519 public key used as Wallet format authority.
|
||||
pub const KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES: usize = 32;
|
||||
/// Byte length of an Ed25519 detached state signature.
|
||||
pub const KSPWALLET_V1_ED25519_SIGNATURE_BYTES: usize = 64;
|
||||
/// Initial protected payload version used independently by control, metadata and secret compartments.
|
||||
pub const KSPWALLET_V1_INITIAL_PAYLOAD_VERSION: u32 = 1;
|
||||
/// Maximum protected alias size in UTF-8 bytes for metadata V1.
|
||||
pub const KSPWALLET_V1_MAX_ALIAS_BYTES: usize = 256;
|
||||
/// Structural V1 ceiling for serialized Argon2 iteration cost; creation defaults are benchmarked separately.
|
||||
pub const KSPWALLET_V1_MAX_ARGON2_ITERATIONS: u32 = 64;
|
||||
/// Structural V1 ceiling for serialized Argon2 memory cost; creation defaults are benchmarked separately.
|
||||
pub const KSPWALLET_V1_MAX_ARGON2_MEMORY_KIB: u32 = 1024 * 1024;
|
||||
/// Structural V1 ceiling for serialized Argon2 parallelism; creation defaults are benchmarked separately.
|
||||
pub const KSPWALLET_V1_MAX_ARGON2_PARALLELISM: u32 = 64;
|
||||
/// Maximum accepted Argon2 salt size in bytes.
|
||||
pub const KSPWALLET_V1_MAX_KDF_SALT_BYTES: usize = 64;
|
||||
/// Maximum number of key slots understood by format V1: exactly one OWNER plus optional VIEW.
|
||||
pub const KSPWALLET_V1_MAX_KEY_SLOTS: usize = 2;
|
||||
/// Maximum ciphertext size of one wrapped capability payload.
|
||||
pub const KSPWALLET_V1_MAX_KEY_WRAP_CIPHERTEXT_BYTES: usize = 4096;
|
||||
/// Maximum metadata ciphertext size, including the AEAD tag over the bounded 64-KiB plaintext.
|
||||
pub const KSPWALLET_V1_MAX_METADATA_CIPHERTEXT_BYTES: usize = KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES + KSPWALLET_V1_AEAD_TAG_BYTES;
|
||||
/// Maximum metadata plaintext size before V1 encryption.
|
||||
pub const KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES: usize = 64 * 1024;
|
||||
/// Maximum number of protected notes in metadata V1.
|
||||
pub const KSPWALLET_V1_MAX_NOTES: usize = 64;
|
||||
/// Maximum protected note text size in UTF-8 bytes for metadata V1.
|
||||
pub const KSPWALLET_V1_MAX_NOTE_TEXT_BYTES: usize = 8 * 1024;
|
||||
/// Maximum OWNER-control ciphertext size accepted by envelope V1.
|
||||
pub const KSPWALLET_V1_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES: usize = 4096;
|
||||
/// Maximum password size in exact UTF-8 input bytes.
|
||||
pub const KSPWALLET_V1_MAX_PASSWORD_BYTES: usize = 1024;
|
||||
/// Maximum OWNER-only secret ciphertext size accepted by envelope V1.
|
||||
pub const KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES: usize = 4096;
|
||||
/// Domain separator for metadata compartment AAD.
|
||||
pub const KSPWALLET_V1_METADATA_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-METADATA";
|
||||
/// Minimum accepted Argon2 salt size in bytes.
|
||||
pub const KSPWALLET_V1_MIN_KDF_SALT_BYTES: usize = 16;
|
||||
/// Byte length of every protected metadata note identifier.
|
||||
pub const KSPWALLET_V1_NOTE_ID_BYTES: usize = 16;
|
||||
/// Domain separator for OWNER-control compartment AAD.
|
||||
pub const KSPWALLET_V1_OWNER_CONTROL_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-OWNER-CONTROL";
|
||||
/// Exact plaintext size of the V1 OWNER-control compartment.
|
||||
pub const KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES: usize = 96;
|
||||
/// Domain separator for OWNER key-slot wrapping AAD.
|
||||
pub const KSPWALLET_V1_OWNER_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-OWNER-SLOT";
|
||||
/// Domain separator for OWNER-only secret compartment AAD.
|
||||
pub const KSPWALLET_V1_SECRET_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-SECRET";
|
||||
/// Exact plaintext size of the V1 Solana secret compartment.
|
||||
pub const KSPWALLET_V1_SECRET_PLAINTEXT_BYTES: usize = 64;
|
||||
/// Byte length of every V1 key-slot identifier.
|
||||
pub const KSPWALLET_V1_SLOT_ID_BYTES: usize = 16;
|
||||
/// Domain separator for the OWNER state-signature transcript.
|
||||
pub const KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN: &[u8] = b"KSPWALLET-V1-STATE";
|
||||
/// Domain separator for VIEW key-slot wrapping AAD.
|
||||
pub const KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V1-AAD-VIEW-SLOT";
|
||||
/// Byte length of an XChaCha20-Poly1305 nonce.
|
||||
pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24;
|
||||
|
||||
/// Owning tracing target for events emitted by the Wallet crate.
|
||||
pub(crate) const TRACING_TARGET: &str = "ksp-wallet-lib";
|
||||
197
crates/ksp-wallet-lib/src/crypto.rs
Normal file
197
crates/ksp-wallet-lib/src/crypto.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
// file: crates/ksp-wallet-lib/src/crypto.rs
|
||||
// version: 4
|
||||
|
||||
//! In-memory cryptographic primitives for native `.kspwallet` V1.
|
||||
|
||||
use chacha20poly1305::KeyInit; // rust-rules: trait-import
|
||||
use chacha20poly1305::aead::Aead; // rust-rules: trait-import
|
||||
|
||||
/// Exact V1 content-key and password-derived-key size in bytes.
|
||||
pub(crate) const SECRET_KEY_BYTES: usize = 32;
|
||||
|
||||
/// Owned 32-byte secret key with redacted diagnostics and drop-time zeroization.
|
||||
pub(crate) struct SecretKeyV1 {
|
||||
bytes: [u8; SECRET_KEY_BYTES],
|
||||
}
|
||||
|
||||
impl crate::SecretKeyV1 {
|
||||
/// Takes ownership of exact 32-byte secret material.
|
||||
pub(crate) const fn from_bytes(bytes: [u8; SECRET_KEY_BYTES]) -> Self {
|
||||
return Self { bytes };
|
||||
}
|
||||
|
||||
/// Generates a fresh secret key from the operating-system CSPRNG.
|
||||
pub(crate) fn random() -> ksp_core_lib::Result<Self> {
|
||||
let bytes = match random_bytes::<SECRET_KEY_BYTES>() {
|
||||
std::result::Result::Ok(bytes) => bytes,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(Self { bytes });
|
||||
}
|
||||
|
||||
/// Borrows secret bytes internally without allocating or cloning.
|
||||
pub(crate) const fn as_bytes(&self) -> &[u8; SECRET_KEY_BYTES] {
|
||||
return &self.bytes;
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::SecretKeyV1 {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.write_str("SecretKeyV1(<redacted>)");
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Drop for crate::SecretKeyV1 {
|
||||
fn drop(&mut self) {
|
||||
zeroize::Zeroize::zeroize(&mut self.bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a fresh fixed-size byte array from the operating-system CSPRNG.
|
||||
pub(crate) fn random_bytes<const LENGTH: usize>() -> ksp_core_lib::Result<[u8; LENGTH]> {
|
||||
let mut bytes = [0_u8; LENGTH];
|
||||
let fill_result = getrandom::fill(bytes.as_mut_slice());
|
||||
if fill_result.is_err() {
|
||||
zeroize::Zeroize::zeroize(&mut bytes);
|
||||
return std::result::Result::Err(randomness_error());
|
||||
}
|
||||
return std::result::Result::Ok(bytes);
|
||||
}
|
||||
|
||||
/// Generates a fresh XChaCha20-Poly1305 nonce from the operating-system CSPRNG.
|
||||
pub(crate) fn random_nonce() -> ksp_core_lib::Result<[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES]> {
|
||||
return random_bytes::<{ crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES }>();
|
||||
}
|
||||
|
||||
/// Derives one V1 password wrapping key from serialized Argon2id parameters.
|
||||
pub(crate) fn derive_password_key(password: &[u8], kdf: &crate::WalletKdfParametersV1) -> ksp_core_lib::Result<crate::SecretKeyV1> {
|
||||
return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism());
|
||||
}
|
||||
|
||||
/// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
|
||||
pub(crate) fn wrap_key(
|
||||
wrapping_key: &crate::SecretKeyV1,
|
||||
key_to_wrap: &crate::SecretKeyV1,
|
||||
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
aad: &[u8],
|
||||
) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
return crate::encrypt_bytes(wrapping_key, nonce, aad, key_to_wrap.as_bytes());
|
||||
}
|
||||
|
||||
/// Unwraps one 32-byte content key and maps every AEAD authentication failure to the generic Wallet authentication error.
|
||||
pub(crate) fn unwrap_key(
|
||||
wrapping_key: &crate::SecretKeyV1,
|
||||
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
aad: &[u8],
|
||||
ciphertext: &[u8],
|
||||
) -> ksp_core_lib::Result<crate::SecretKeyV1> {
|
||||
let plaintext_result = crate::decrypt_bytes(wrapping_key, nonce, aad, ciphertext);
|
||||
let mut plaintext = match plaintext_result {
|
||||
std::result::Result::Ok(plaintext) => plaintext,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if plaintext.len() != SECRET_KEY_BYTES {
|
||||
zeroize::Zeroize::zeroize(plaintext.as_mut_slice());
|
||||
return std::result::Result::Err(authentication_error());
|
||||
}
|
||||
let converted = <[u8; SECRET_KEY_BYTES]>::try_from(plaintext.as_slice());
|
||||
let key = match converted {
|
||||
std::result::Result::Ok(key) => crate::SecretKeyV1::from_bytes(key),
|
||||
std::result::Result::Err(_) => {
|
||||
zeroize::Zeroize::zeroize(plaintext.as_mut_slice());
|
||||
return std::result::Result::Err(authentication_error());
|
||||
},
|
||||
};
|
||||
zeroize::Zeroize::zeroize(plaintext.as_mut_slice());
|
||||
return std::result::Result::Ok(key);
|
||||
}
|
||||
|
||||
/// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
|
||||
pub(crate) fn encrypt_bytes(
|
||||
key: &crate::SecretKeyV1,
|
||||
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
aad: &[u8],
|
||||
plaintext: &[u8],
|
||||
) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
let cipher_result = chacha20poly1305::XChaCha20Poly1305::new_from_slice(key.as_bytes());
|
||||
let cipher = match cipher_result {
|
||||
std::result::Result::Ok(cipher) => cipher,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(crypto_parameter_error()),
|
||||
};
|
||||
let nonce_value = chacha20poly1305::XNonce::from(*nonce);
|
||||
let payload = chacha20poly1305::aead::Payload { msg: plaintext, aad };
|
||||
let encrypted = cipher.encrypt(&nonce_value, payload);
|
||||
return match encrypted {
|
||||
std::result::Result::Ok(ciphertext) => std::result::Result::Ok(ciphertext),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(crypto_parameter_error()),
|
||||
};
|
||||
}
|
||||
|
||||
/// Decrypts authenticated ciphertext bytes and returns a generic authentication error on tag failure.
|
||||
pub(crate) fn decrypt_bytes(
|
||||
key: &crate::SecretKeyV1,
|
||||
nonce: &[u8; crate::KSPWALLET_V1_XCHACHA_NONCE_BYTES],
|
||||
aad: &[u8],
|
||||
ciphertext: &[u8],
|
||||
) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
let cipher_result = chacha20poly1305::XChaCha20Poly1305::new_from_slice(key.as_bytes());
|
||||
let cipher = match cipher_result {
|
||||
std::result::Result::Ok(cipher) => cipher,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(crypto_parameter_error()),
|
||||
};
|
||||
let nonce_value = chacha20poly1305::XNonce::from(*nonce);
|
||||
let payload = chacha20poly1305::aead::Payload { msg: ciphertext, aad };
|
||||
let decrypted = cipher.decrypt(&nonce_value, payload);
|
||||
return match decrypted {
|
||||
std::result::Result::Ok(plaintext) => std::result::Result::Ok(plaintext),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(authentication_error()),
|
||||
};
|
||||
}
|
||||
|
||||
fn derive_argon2id(password: &[u8], salt: &[u8], memory_kib: u32, iterations: u32, parallelism: u32) -> ksp_core_lib::Result<crate::SecretKeyV1> {
|
||||
if password.is_empty() || password.len() > crate::KSPWALLET_V1_MAX_PASSWORD_BYTES {
|
||||
return std::result::Result::Err(crypto_parameter_error());
|
||||
}
|
||||
if salt.len() < crate::KSPWALLET_V1_MIN_KDF_SALT_BYTES || salt.len() > crate::KSPWALLET_V1_MAX_KDF_SALT_BYTES {
|
||||
return std::result::Result::Err(crypto_parameter_error());
|
||||
}
|
||||
if memory_kib == 0
|
||||
|| memory_kib > crate::KSPWALLET_V1_MAX_ARGON2_MEMORY_KIB
|
||||
|| iterations == 0
|
||||
|| iterations > crate::KSPWALLET_V1_MAX_ARGON2_ITERATIONS
|
||||
|| parallelism == 0
|
||||
|| parallelism > crate::KSPWALLET_V1_MAX_ARGON2_PARALLELISM
|
||||
|| memory_kib < parallelism * 8
|
||||
{
|
||||
return std::result::Result::Err(crypto_parameter_error());
|
||||
}
|
||||
let params_result = argon2::Params::new(memory_kib, iterations, parallelism, std::option::Option::Some(SECRET_KEY_BYTES));
|
||||
let params = match params_result {
|
||||
std::result::Result::Ok(params) => params,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(crypto_parameter_error()),
|
||||
};
|
||||
let argon2 = argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
|
||||
let mut output = [0_u8; SECRET_KEY_BYTES];
|
||||
let derive_result = argon2.hash_password_into(password, salt, output.as_mut_slice());
|
||||
if derive_result.is_err() {
|
||||
zeroize::Zeroize::zeroize(&mut output);
|
||||
return std::result::Result::Err(crypto_parameter_error());
|
||||
}
|
||||
return std::result::Result::Ok(crate::SecretKeyV1::from_bytes(output));
|
||||
}
|
||||
|
||||
fn randomness_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_RANDOMNESS_FAILED, "Wallet cryptographic randomness could not be obtained");
|
||||
}
|
||||
|
||||
fn crypto_parameter_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID, "Wallet cryptographic parameters are invalid or unsupported");
|
||||
}
|
||||
|
||||
fn authentication_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_AUTHENTICATION_FAILED, "Wallet cryptographic authentication failed");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/crypto.rs"]
|
||||
mod tests;
|
||||
37
crates/ksp-wallet-lib/src/error.rs
Normal file
37
crates/ksp-wallet-lib/src/error.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
// file: crates/ksp-wallet-lib/src/error.rs
|
||||
// version: 5
|
||||
|
||||
/// Error code used when an atomic Wallet persistence operation cannot publish a valid replacement.
|
||||
pub const ERROR_CODE_ATOMIC_PERSISTENCE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "atomic_persistence_failed");
|
||||
/// Error code used when an authenticated Wallet structure cannot be verified.
|
||||
pub const ERROR_CODE_AUTHENTICATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "authentication_failed");
|
||||
/// Error code used when an operation requires a capability that the caller does not own.
|
||||
pub const ERROR_CODE_CAPABILITY_INSUFFICIENT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "capability_insufficient");
|
||||
/// Error code used when an internal blocking cryptographic operation cannot complete.
|
||||
pub const ERROR_CODE_CRYPTO_OPERATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "crypto_operation_failed");
|
||||
/// Error code used when serialized cryptographic parameters are invalid or unsupported.
|
||||
pub const ERROR_CODE_CRYPTO_PARAMETERS_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "crypto_parameters_invalid");
|
||||
/// Error code used when a no-clobber create or import destination already exists.
|
||||
pub const ERROR_CODE_DESTINATION_EXISTS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "destination_exists");
|
||||
/// Error code used when a native Wallet structure is invalid.
|
||||
pub const ERROR_CODE_FORMAT_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "format_invalid");
|
||||
/// Error code used when a native Wallet format version is unsupported.
|
||||
pub const ERROR_CODE_FORMAT_VERSION_UNSUPPORTED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "format_version_unsupported");
|
||||
/// Error code used when Wallet filesystem I/O fails.
|
||||
pub const ERROR_CODE_IO_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "io_failed");
|
||||
/// Error code used when imported or decoded key material is invalid.
|
||||
pub const ERROR_CODE_KEY_MATERIAL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "key_material_invalid");
|
||||
/// Error code used when a protected Wallet note identifier is not present.
|
||||
pub const ERROR_CODE_NOTE_NOT_FOUND: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "note_not_found");
|
||||
/// Error code used when an OWNER unlock attempt fails without exposing a finer cryptographic oracle.
|
||||
pub const ERROR_CODE_OWNER_UNLOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "owner_unlock_failed");
|
||||
/// Error code used when the operating-system cryptographic random source cannot provide bytes.
|
||||
pub const ERROR_CODE_RANDOMNESS_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "randomness_failed");
|
||||
/// Error code used when a Wallet signing operation fails.
|
||||
pub const ERROR_CODE_SIGNATURE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "signature_failed");
|
||||
/// Error code used when an administrative replacement targets a different or stale Wallet state.
|
||||
pub const ERROR_CODE_STATE_CONFLICT: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "state_conflict");
|
||||
/// Error code used when an import/export transfer format is unsupported.
|
||||
pub const ERROR_CODE_TRANSFER_FORMAT_UNSUPPORTED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "transfer_format_unsupported");
|
||||
/// Error code used when a VIEW unlock attempt fails without exposing a finer cryptographic oracle.
|
||||
pub const ERROR_CODE_VIEW_UNLOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet", "view_unlock_failed");
|
||||
289
crates/ksp-wallet-lib/src/lib.rs
Normal file
289
crates/ksp-wallet-lib/src/lib.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
// file: crates/ksp-wallet-lib/src/lib.rs
|
||||
// version: 11
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Autonomous KSP Wallet foundation.
|
||||
//!
|
||||
//! `ksp-wallet-lib` owns the native `.kspwallet` domain, VIEW/OWNER capability model, protected metadata projection, password-secret wrappers and Wallet
|
||||
//! error contract. `0.2.5-pre.003` freezes the strict V1 JSON envelope, canonical Base64url decoding, structural limits and deterministic
|
||||
//! state-transcript/AEAD-AAD byte codecs. `0.2.5-pre.004` adds the in-memory Argon2id/XChaCha20-Poly1305/CSPRNG primitives and deterministic crypto
|
||||
//! vectors. `0.2.5-pre.005` adds exact protected payloads, OWNER Ed25519 state authentication and async in-memory create/open flows for VIEW and OWNER.
|
||||
//! `0.2.5-pre.006` adds bounded async-first filesystem reads plus same-directory synchronized no-clobber publication for new native files. `0.2.5-pre.007`
|
||||
//! adds Solana message signing, protected metadata administration, password rotation and strong VIEW disable/recreate with capability-bound atomic
|
||||
//! replacement. `0.2.5-pre.008` adds bounded Solana CLI JSON and canonical full-keypair Base58 import/export adapters with safe inspection and no-clobber
|
||||
//! native import/export publication. `0.2.5-pre.009` adds adversarial security/compliance canaries and records the final dependency/interop audit before
|
||||
//! documentation closure. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral
|
||||
//! observability uses only
|
||||
//! `ksp-logging-lib` with the explicit crate target defined in `src/constants.rs`.
|
||||
|
||||
mod capability;
|
||||
mod constants;
|
||||
mod crypto;
|
||||
mod error;
|
||||
mod metadata;
|
||||
mod owner;
|
||||
mod password;
|
||||
mod payload;
|
||||
mod persistence;
|
||||
mod transcript;
|
||||
mod transfer;
|
||||
mod view;
|
||||
mod wallet;
|
||||
mod wire;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/security.rs"]
|
||||
mod security_tests;
|
||||
|
||||
/// Authorized capability represented by an unlocked Wallet handle.
|
||||
pub use self::capability::WalletCapability;
|
||||
/// Native `.kspwallet` V1 format version.
|
||||
pub use self::constants::KSPWALLET_FORMAT_VERSION_V1;
|
||||
/// Exact magic string required by every native `.kspwallet` document.
|
||||
pub use self::constants::KSPWALLET_MAGIC;
|
||||
/// Maximum accepted `.kspwallet` document size before parsing.
|
||||
pub use self::constants::KSPWALLET_MAX_FILE_BYTES;
|
||||
/// Byte length of every Solana Ed25519 signature returned by OWNER.
|
||||
pub use self::constants::KSPWALLET_SOLANA_SIGNATURE_BYTES;
|
||||
/// Maximum accepted Base58 keypair transfer size before decoding.
|
||||
pub use self::constants::KSPWALLET_TRANSFER_MAX_BASE58_BYTES;
|
||||
/// Maximum accepted Solana CLI JSON transfer size before parsing.
|
||||
pub use self::constants::KSPWALLET_TRANSFER_MAX_SOLANA_CLI_JSON_BYTES;
|
||||
/// Byte length of the AEAD authentication tag appended to V1 ciphertexts.
|
||||
pub use self::constants::KSPWALLET_V1_AEAD_TAG_BYTES;
|
||||
/// Argon2 version serialized by `.kspwallet` V1 key slots.
|
||||
pub use self::constants::KSPWALLET_V1_ARGON2_VERSION;
|
||||
/// Default Argon2id iteration count for newly created V1 slots.
|
||||
pub use self::constants::KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS;
|
||||
/// Default Argon2id memory cost for newly created V1 slots.
|
||||
pub use self::constants::KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB;
|
||||
/// Default Argon2id parallelism for newly created V1 slots.
|
||||
pub use self::constants::KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM;
|
||||
/// Default KDF salt size generated for newly created V1 slots.
|
||||
pub use self::constants::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES;
|
||||
/// Byte length of the Ed25519 format-authority public key.
|
||||
pub use self::constants::KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES;
|
||||
/// Byte length of the Ed25519 detached state signature.
|
||||
pub use self::constants::KSPWALLET_V1_ED25519_SIGNATURE_BYTES;
|
||||
/// Initial per-compartment protected payload version.
|
||||
pub use self::constants::KSPWALLET_V1_INITIAL_PAYLOAD_VERSION;
|
||||
/// Maximum alias size in protected metadata V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_ALIAS_BYTES;
|
||||
/// Maximum structural Argon2 iteration count accepted by the V1 parser.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_ARGON2_ITERATIONS;
|
||||
/// Maximum structural Argon2 memory cost accepted by the V1 parser.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_ARGON2_MEMORY_KIB;
|
||||
/// Maximum structural Argon2 parallelism accepted by the V1 parser.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_ARGON2_PARALLELISM;
|
||||
/// Maximum Argon2 salt size accepted by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_KDF_SALT_BYTES;
|
||||
/// Maximum number of OWNER/VIEW slots accepted by format V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_KEY_SLOTS;
|
||||
/// Maximum wrapped-key ciphertext size accepted by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_KEY_WRAP_CIPHERTEXT_BYTES;
|
||||
/// Maximum protected metadata ciphertext size accepted by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_METADATA_CIPHERTEXT_BYTES;
|
||||
/// Maximum protected metadata plaintext size defined by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_METADATA_PLAINTEXT_BYTES;
|
||||
/// Maximum protected note text size defined by metadata V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_NOTE_TEXT_BYTES;
|
||||
/// Maximum number of protected notes defined by metadata V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_NOTES;
|
||||
/// Maximum OWNER-control ciphertext size accepted by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_OWNER_CONTROL_CIPHERTEXT_BYTES;
|
||||
/// Maximum password size in exact UTF-8 bytes defined by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_PASSWORD_BYTES;
|
||||
/// Maximum OWNER-only secret ciphertext size accepted by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MAX_SECRET_CIPHERTEXT_BYTES;
|
||||
/// Domain separator for metadata-compartment AEAD AAD.
|
||||
pub use self::constants::KSPWALLET_V1_METADATA_AAD_DOMAIN;
|
||||
/// Minimum Argon2 salt size accepted by V1.
|
||||
pub use self::constants::KSPWALLET_V1_MIN_KDF_SALT_BYTES;
|
||||
/// Byte length of every protected metadata note identifier.
|
||||
pub use self::constants::KSPWALLET_V1_NOTE_ID_BYTES;
|
||||
/// Domain separator for OWNER-control compartment AEAD AAD.
|
||||
pub use self::constants::KSPWALLET_V1_OWNER_CONTROL_AAD_DOMAIN;
|
||||
/// Exact OWNER-control plaintext size fixed by V1.
|
||||
pub use self::constants::KSPWALLET_V1_OWNER_CONTROL_PLAINTEXT_BYTES;
|
||||
/// Domain separator for OWNER key-slot wrapping AAD.
|
||||
pub use self::constants::KSPWALLET_V1_OWNER_SLOT_AAD_DOMAIN;
|
||||
/// Domain separator for OWNER-only secret compartment AEAD AAD.
|
||||
pub use self::constants::KSPWALLET_V1_SECRET_AAD_DOMAIN;
|
||||
/// Exact Solana keypair plaintext size fixed by V1.
|
||||
pub use self::constants::KSPWALLET_V1_SECRET_PLAINTEXT_BYTES;
|
||||
/// Byte length of every V1 key-slot identifier.
|
||||
pub use self::constants::KSPWALLET_V1_SLOT_ID_BYTES;
|
||||
/// Domain separator for the OWNER state-signature transcript.
|
||||
pub use self::constants::KSPWALLET_V1_STATE_TRANSCRIPT_DOMAIN;
|
||||
/// Domain separator for VIEW key-slot wrapping AAD.
|
||||
pub use self::constants::KSPWALLET_V1_VIEW_SLOT_AAD_DOMAIN;
|
||||
/// Byte length of an XChaCha20-Poly1305 nonce.
|
||||
pub use self::constants::KSPWALLET_V1_XCHACHA_NONCE_BYTES;
|
||||
/// Error code used when an atomic Wallet persistence operation cannot publish a valid replacement.
|
||||
pub use self::error::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED;
|
||||
/// Error code used when an authenticated Wallet structure cannot be verified.
|
||||
pub use self::error::ERROR_CODE_AUTHENTICATION_FAILED;
|
||||
/// Error code used when an operation requires a capability that the caller does not own.
|
||||
pub use self::error::ERROR_CODE_CAPABILITY_INSUFFICIENT;
|
||||
/// Error code used when a blocking cryptographic task cannot complete.
|
||||
pub use self::error::ERROR_CODE_CRYPTO_OPERATION_FAILED;
|
||||
/// Error code used when serialized cryptographic parameters are invalid or unsupported.
|
||||
pub use self::error::ERROR_CODE_CRYPTO_PARAMETERS_INVALID;
|
||||
/// Error code used when a no-clobber create or import destination already exists.
|
||||
pub use self::error::ERROR_CODE_DESTINATION_EXISTS;
|
||||
/// Error code used when a native Wallet structure is invalid.
|
||||
pub use self::error::ERROR_CODE_FORMAT_INVALID;
|
||||
/// Error code used when a native Wallet format version is unsupported.
|
||||
pub use self::error::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED;
|
||||
/// Error code used when Wallet filesystem I/O fails.
|
||||
pub use self::error::ERROR_CODE_IO_FAILED;
|
||||
/// Error code used when imported or decoded key material is invalid.
|
||||
pub use self::error::ERROR_CODE_KEY_MATERIAL_INVALID;
|
||||
/// Error code used when a protected Wallet note identifier is absent.
|
||||
pub use self::error::ERROR_CODE_NOTE_NOT_FOUND;
|
||||
/// Error code used when an OWNER unlock attempt fails without exposing a finer cryptographic oracle.
|
||||
pub use self::error::ERROR_CODE_OWNER_UNLOCK_FAILED;
|
||||
/// Error code used when the operating-system cryptographic random source cannot provide bytes.
|
||||
pub use self::error::ERROR_CODE_RANDOMNESS_FAILED;
|
||||
/// Error code used when a Wallet signing operation fails.
|
||||
pub use self::error::ERROR_CODE_SIGNATURE_FAILED;
|
||||
/// Error code used when an administrative replacement sees a different or stale Wallet state.
|
||||
pub use self::error::ERROR_CODE_STATE_CONFLICT;
|
||||
/// Error code used when an import/export transfer format is unsupported.
|
||||
pub use self::error::ERROR_CODE_TRANSFER_FORMAT_UNSUPPORTED;
|
||||
/// Error code used when a VIEW unlock attempt fails without exposing a finer cryptographic oracle.
|
||||
pub use self::error::ERROR_CODE_VIEW_UNLOCK_FAILED;
|
||||
/// Minimal non-secret information available while a native Wallet remains locked.
|
||||
pub use self::metadata::LockedWalletInfo;
|
||||
/// Protected initial metadata supplied to native Wallet creation.
|
||||
pub use self::metadata::WalletCreateMetadataV1;
|
||||
/// Safe metadata projection produced after VIEW or OWNER authorization.
|
||||
pub use self::metadata::WalletInfo;
|
||||
/// One protected Wallet note exposed only after authorization.
|
||||
pub use self::metadata::WalletNote;
|
||||
/// Authorized OWNER capability handle.
|
||||
pub use self::owner::WalletOwner;
|
||||
/// Owned OWNER password material with redacted diagnostics and drop-time zeroization.
|
||||
pub use self::password::OwnerPassword;
|
||||
/// Owned VIEW password material with redacted diagnostics and drop-time zeroization.
|
||||
pub use self::password::ViewPassword;
|
||||
/// Creates and no-clobber persists a new native `.kspwallet` V1 file.
|
||||
pub use self::persistence::create_wallet_file_v1;
|
||||
/// Reads and verifies a locked native `.kspwallet` V1 file.
|
||||
pub use self::persistence::inspect_locked_wallet_file_v1;
|
||||
/// Opens a native `.kspwallet` V1 file with OWNER capability.
|
||||
pub use self::persistence::open_wallet_owner_file_v1;
|
||||
/// Opens a native `.kspwallet` V1 file with VIEW capability.
|
||||
pub use self::persistence::open_wallet_view_file_v1;
|
||||
/// Explicit secret-transfer format supported by Wallet.
|
||||
pub use self::transfer::WalletTransferFormat;
|
||||
/// Safe public identity projection of one validated secret-transfer source.
|
||||
pub use self::transfer::WalletTransferInspection;
|
||||
/// Imports one bounded external transfer file into a new no-clobber native Wallet V1.
|
||||
pub use self::transfer::import_wallet_transfer_file_v1;
|
||||
/// Imports one in-memory transfer payload into a new no-clobber native Wallet V1.
|
||||
pub use self::transfer::import_wallet_transfer_v1;
|
||||
/// Validates one in-memory transfer payload and exposes only its derived public identity.
|
||||
pub use self::transfer::inspect_wallet_transfer;
|
||||
/// Validates one bounded external transfer file and exposes only its derived public identity.
|
||||
pub use self::transfer::inspect_wallet_transfer_file;
|
||||
/// Authorized VIEW capability handle.
|
||||
pub use self::view::WalletView;
|
||||
/// Creates a new in-memory native Wallet V1.
|
||||
pub use self::wallet::create_wallet_v1;
|
||||
/// Parses and verifies locked native Wallet state without unlocking protected metadata.
|
||||
pub use self::wallet::inspect_locked_wallet_v1;
|
||||
/// Opens the OWNER capability from a native Wallet V1 document.
|
||||
pub use self::wallet::open_wallet_owner_v1;
|
||||
/// Opens the VIEW capability from a native Wallet V1 document.
|
||||
pub use self::wallet::open_wallet_view_v1;
|
||||
/// Strict semantic representation of one parsed native `.kspwallet` V1 envelope.
|
||||
pub use self::wire::KspWalletEnvelopeV1;
|
||||
/// Authenticated-encryption algorithm fixed by native Wallet V1.
|
||||
pub use self::wire::WalletAeadAlgorithmV1;
|
||||
/// Protected compartment kind fixed by native Wallet V1.
|
||||
pub use self::wire::WalletCompartmentKindV1;
|
||||
/// One validated encrypted V1 compartment.
|
||||
pub use self::wire::WalletEncryptedCompartmentV1;
|
||||
/// Password KDF fixed by native Wallet V1.
|
||||
pub use self::wire::WalletKdfAlgorithmV1;
|
||||
/// Parsed Argon2id parameters from one V1 key slot.
|
||||
pub use self::wire::WalletKdfParametersV1;
|
||||
/// Role of one native V1 key slot.
|
||||
pub use self::wire::WalletKeySlotRoleV1;
|
||||
/// One validated OWNER or VIEW V1 key slot.
|
||||
pub use self::wire::WalletKeySlotV1;
|
||||
/// Parsed AEAD wrapped-key payload from one V1 key slot.
|
||||
pub use self::wire::WalletKeyWrapV1;
|
||||
/// State-signature algorithm fixed by native Wallet V1.
|
||||
pub use self::wire::WalletStateSignatureAlgorithmV1;
|
||||
/// Detached OWNER state signature embedded in a V1 envelope.
|
||||
pub use self::wire::WalletStateSignatureV1;
|
||||
/// OWNER-signed stable descriptor of the optional VIEW slot.
|
||||
pub use self::wire::WalletViewDescriptorV1;
|
||||
|
||||
/// Wallet-owned tracing target used by the KSP logging facade.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
/// Exact V1 content-key and password-derived-key size in bytes.
|
||||
pub(crate) use self::crypto::SECRET_KEY_BYTES;
|
||||
/// Owned 32-byte secret key with redacted diagnostics and drop-time zeroization.
|
||||
pub(crate) use self::crypto::SecretKeyV1;
|
||||
/// Decrypts authenticated ciphertext bytes and returns a generic authentication error on tag failure.
|
||||
pub(crate) use self::crypto::decrypt_bytes;
|
||||
/// Derives one V1 password wrapping key from serialized Argon2id parameters.
|
||||
pub(crate) use self::crypto::derive_password_key;
|
||||
/// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
|
||||
pub(crate) use self::crypto::encrypt_bytes;
|
||||
/// Generates a fresh fixed-size byte array from the operating-system CSPRNG.
|
||||
pub(crate) use self::crypto::random_bytes;
|
||||
/// Generates a fresh XChaCha20-Poly1305 nonce from the operating-system CSPRNG.
|
||||
pub(crate) use self::crypto::random_nonce;
|
||||
/// Unwraps one 32-byte content key and maps every AEAD authentication failure to the generic Wallet authentication error.
|
||||
pub(crate) use self::crypto::unwrap_key;
|
||||
/// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
|
||||
pub(crate) use self::crypto::wrap_key;
|
||||
/// Crate-internal `MetadataPayloadV1` state shared across the owning crate.
|
||||
pub(crate) use self::payload::MetadataPayloadV1;
|
||||
/// Decodes metadata payload.
|
||||
pub(crate) use self::payload::decode_metadata_payload;
|
||||
/// Decodes owner control payload.
|
||||
pub(crate) use self::payload::decode_owner_control_payload;
|
||||
/// Encodes initial metadata payload.
|
||||
pub(crate) use self::payload::encode_initial_metadata_payload;
|
||||
/// Encodes owner control payload.
|
||||
pub(crate) use self::payload::encode_owner_control_payload;
|
||||
/// Internal no-clobber native persistence path shared by transfer adapters.
|
||||
pub(crate) use self::persistence::persist_new_wallet_content_v1;
|
||||
/// Persists new wallet fault before publish.
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::persistence::persist_new_wallet_fault_before_publish;
|
||||
/// Persists new wallet for test.
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::persistence::persist_new_wallet_for_test;
|
||||
/// Replaces wallet fault before publish.
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::persistence::replace_wallet_fault_before_publish;
|
||||
/// Replaces wallet file v1.
|
||||
pub(crate) use self::persistence::replace_wallet_file_v1;
|
||||
/// Replaces wallet for test.
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::persistence::replace_wallet_for_test;
|
||||
/// Internal deterministic compartment-AAD codec shared by Wallet crypto layers.
|
||||
pub(crate) use self::transcript::compartment_aad;
|
||||
/// Internal deterministic key-slot-AAD codec shared by Wallet crypto layers.
|
||||
pub(crate) use self::transcript::slot_aad;
|
||||
/// Internal deterministic OWNER-state transcript codec shared by Wallet crypto layers.
|
||||
pub(crate) use self::transcript::state_transcript;
|
||||
/// Internal no-clobber transfer-file writer used only by OWNER export.
|
||||
pub(crate) use self::transfer::write_wallet_transfer_file_v1;
|
||||
/// Crate-internal `OwnerStateV1` state shared across the owning crate.
|
||||
pub(crate) use self::wallet::OwnerStateV1;
|
||||
/// Crate-internal `ViewStateV1` state shared across the owning crate.
|
||||
pub(crate) use self::wallet::ViewStateV1;
|
||||
/// Internal imported-keypair creation path shared by transfer adapters.
|
||||
pub(crate) use self::wallet::create_wallet_v1_from_keypair;
|
||||
/// Verifies state signature.
|
||||
pub(crate) use self::wallet::verify_state_signature;
|
||||
161
crates/ksp-wallet-lib/src/metadata.rs
Normal file
161
crates/ksp-wallet-lib/src/metadata.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
// file: crates/ksp-wallet-lib/src/metadata.rs
|
||||
// version: 4
|
||||
|
||||
/// Protected metadata requested when creating a new native Wallet.
|
||||
///
|
||||
/// Alias and note texts are never rendered by `Debug`. Wallet generates stable random note identifiers when the metadata is sealed into the V1 payload.
|
||||
#[derive(Default, Eq, PartialEq)]
|
||||
pub struct WalletCreateMetadataV1 {
|
||||
alias: std::option::Option<std::string::String>,
|
||||
note_texts: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
impl WalletCreateMetadataV1 {
|
||||
/// Creates protected initial Wallet metadata.
|
||||
#[must_use]
|
||||
pub fn new(alias: std::option::Option<std::string::String>, note_texts: std::vec::Vec<std::string::String>) -> Self {
|
||||
return Self { alias, note_texts };
|
||||
}
|
||||
|
||||
/// Consumes this value and returns parts.
|
||||
pub(crate) fn into_parts(self) -> (std::option::Option<std::string::String>, std::vec::Vec<std::string::String>) {
|
||||
return (self.alias, self.note_texts);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WalletCreateMetadataV1 {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("WalletCreateMetadataV1")
|
||||
.field("alias", &self.alias.as_ref().map(|_| return "<redacted>"))
|
||||
.field("note_count", &self.note_texts.len())
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// One protected Wallet note exposed only after VIEW or OWNER authorization.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct WalletNote {
|
||||
id: std::string::String,
|
||||
text: std::string::String,
|
||||
}
|
||||
|
||||
impl WalletNote {
|
||||
/// Creates a new `WalletNote` value.
|
||||
pub(crate) fn new(id: std::string::String, text: std::string::String) -> Self {
|
||||
return Self { id, text };
|
||||
}
|
||||
|
||||
/// Returns the stable note identifier used by Wallet administration operations.
|
||||
#[must_use]
|
||||
pub fn id(&self) -> &str {
|
||||
return self.id.as_str();
|
||||
}
|
||||
|
||||
/// Returns the protected note text after authorization.
|
||||
#[must_use]
|
||||
pub fn text(&self) -> &str {
|
||||
return self.text.as_str();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WalletNote {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("WalletNote").field("id", &"<redacted>").field("text", &"<redacted>").finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Safe metadata projection produced after VIEW or OWNER authorization.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
pub struct WalletInfo {
|
||||
format_version: u32,
|
||||
capability: crate::WalletCapability,
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
alias: std::option::Option<std::string::String>,
|
||||
notes: std::vec::Vec<crate::WalletNote>,
|
||||
}
|
||||
|
||||
impl WalletInfo {
|
||||
/// Creates a new `WalletInfo` value.
|
||||
pub(crate) fn new(
|
||||
capability: crate::WalletCapability,
|
||||
pubkey: ksp_core_lib::Pubkey,
|
||||
alias: std::option::Option<std::string::String>,
|
||||
notes: std::vec::Vec<crate::WalletNote>,
|
||||
) -> Self {
|
||||
return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, capability, pubkey, alias, notes };
|
||||
}
|
||||
|
||||
/// Returns the native Wallet format version parsed for this projection.
|
||||
#[must_use]
|
||||
pub const fn format_version(&self) -> u32 {
|
||||
return self.format_version;
|
||||
}
|
||||
|
||||
/// Returns the authorization capability that produced this projection.
|
||||
#[must_use]
|
||||
pub const fn capability(&self) -> crate::WalletCapability {
|
||||
return self.capability;
|
||||
}
|
||||
|
||||
/// Returns the authorized Solana public key using the KSP Core re-export.
|
||||
#[must_use]
|
||||
pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
|
||||
return &self.pubkey;
|
||||
}
|
||||
|
||||
/// Returns the protected internal alias after authorization, when present.
|
||||
#[must_use]
|
||||
pub fn alias(&self) -> std::option::Option<&str> {
|
||||
return self.alias.as_deref();
|
||||
}
|
||||
|
||||
/// Returns the protected Wallet notes after authorization.
|
||||
#[must_use]
|
||||
pub fn notes(&self) -> &[crate::WalletNote] {
|
||||
return self.notes.as_slice();
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WalletInfo {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("WalletInfo")
|
||||
.field("format_version", &self.format_version)
|
||||
.field("capability", &self.capability)
|
||||
.field("pubkey", &self.pubkey)
|
||||
.field("alias", &self.alias.as_ref().map(|_| return "<redacted>"))
|
||||
.field("note_count", &self.notes.len())
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal non-secret information available while a native Wallet remains locked.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct LockedWalletInfo {
|
||||
format_version: u32,
|
||||
view_enabled: bool,
|
||||
}
|
||||
|
||||
impl LockedWalletInfo {
|
||||
/// Creates a new `LockedWalletInfo` value.
|
||||
pub(crate) const fn new(view_enabled: bool) -> Self {
|
||||
return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, view_enabled };
|
||||
}
|
||||
|
||||
/// Returns the native Wallet format version.
|
||||
#[must_use]
|
||||
pub const fn format_version(&self) -> u32 {
|
||||
return self.format_version;
|
||||
}
|
||||
|
||||
/// Reports whether a VIEW capability slot exists without exposing its protected contents.
|
||||
#[must_use]
|
||||
pub const fn view_enabled(&self) -> bool {
|
||||
return self.view_enabled;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/metadata.rs"]
|
||||
mod tests;
|
||||
316
crates/ksp-wallet-lib/src/owner.rs
Normal file
316
crates/ksp-wallet-lib/src/owner.rs
Normal file
@@ -0,0 +1,316 @@
|
||||
// file: crates/ksp-wallet-lib/src/owner.rs
|
||||
// version: 6
|
||||
|
||||
/// Authorized OWNER capability handle.
|
||||
///
|
||||
/// OWNER exposes protected metadata, Solana message signing and the authenticated administration operations defined by native Wallet V1. Secret key
|
||||
/// material remains encapsulated and is never exposed through a general-purpose getter.
|
||||
pub struct WalletOwner {
|
||||
info: crate::WalletInfo,
|
||||
state: crate::OwnerStateV1,
|
||||
}
|
||||
|
||||
impl WalletOwner {
|
||||
/// Builds `WalletOwner` from unlocked.
|
||||
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::OwnerStateV1) -> Self {
|
||||
return Self { info, state };
|
||||
}
|
||||
|
||||
/// Returns the authorization capability represented by this handle.
|
||||
#[must_use]
|
||||
pub const fn capability(&self) -> crate::WalletCapability {
|
||||
return crate::WalletCapability::Owner;
|
||||
}
|
||||
|
||||
/// Returns the authorized Solana public key.
|
||||
#[must_use]
|
||||
pub const fn pubkey(&self) -> &ksp_core_lib::Pubkey {
|
||||
return self.info.pubkey();
|
||||
}
|
||||
|
||||
/// Returns the protected internal alias, when present.
|
||||
#[must_use]
|
||||
pub fn alias(&self) -> std::option::Option<&str> {
|
||||
return self.info.alias();
|
||||
}
|
||||
|
||||
/// Returns the protected notes.
|
||||
#[must_use]
|
||||
pub fn notes(&self) -> &[crate::WalletNote] {
|
||||
return self.info.notes();
|
||||
}
|
||||
|
||||
/// Returns the complete safe metadata projection for this OWNER capability.
|
||||
#[must_use]
|
||||
pub fn info(&self) -> &crate::WalletInfo {
|
||||
ksp_logging_lib::trace!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_info_projection",
|
||||
capability = "owner",
|
||||
"wallet metadata projection requested"
|
||||
);
|
||||
return &self.info;
|
||||
}
|
||||
|
||||
/// Exports the immutable Solana keypair through one explicitly selected transfer adapter.
|
||||
///
|
||||
/// The returned bytes contain secret key material and are owned by the caller. Callers should minimize their lifetime and zeroize the buffer after use.
|
||||
/// VIEW has no corresponding export API.
|
||||
pub fn export_transfer(&self, format: crate::WalletTransferFormat) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
let encoded = self.state.export_transfer(format);
|
||||
if let std::result::Result::Ok(value) = encoded.as_ref() {
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_export_transfer",
|
||||
capability = "owner",
|
||||
transfer_format = format.code(),
|
||||
transfer_bytes = value.len(),
|
||||
"wallet secret encoded through explicit OWNER transfer adapter"
|
||||
);
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
/// Exports the immutable Solana keypair to a new no-clobber transfer file.
|
||||
///
|
||||
/// The destination parent must already exist. On Unix, Wallet attempts `0600` permissions as filesystem hygiene, but OS ACLs are not part of the
|
||||
/// `.kspwallet` cryptographic security guarantee. Existing destinations are never overwritten.
|
||||
pub async fn export_transfer_file(
|
||||
&self,
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
format: crate::WalletTransferFormat,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let encoded = match self.state.export_transfer(format) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let result = crate::write_wallet_transfer_file_v1(destination.as_ref().to_path_buf(), encoded, format).await;
|
||||
if result.is_ok() {
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_export_transfer_file",
|
||||
capability = "owner",
|
||||
transfer_format = format.code(),
|
||||
"wallet secret exported through explicit OWNER transfer adapter"
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Signs one caller-provided message with the Wallet's immutable Solana keypair.
|
||||
///
|
||||
/// The returned bytes are the 64-byte Ed25519 signature. The Solana secret key is never returned or logged.
|
||||
pub fn sign(&self, message: &[u8]) -> ksp_core_lib::Result<[u8; crate::KSPWALLET_SOLANA_SIGNATURE_BYTES]> {
|
||||
let signature = self.state.sign_message(message);
|
||||
if signature.is_ok() {
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_sign",
|
||||
capability = "owner",
|
||||
message_bytes = message.len(),
|
||||
"wallet message signed"
|
||||
);
|
||||
}
|
||||
return signature;
|
||||
}
|
||||
|
||||
/// Updates or clears the protected internal alias and atomically replaces the existing wallet file.
|
||||
pub async fn update_alias(
|
||||
&mut self,
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
alias: std::option::Option<std::string::String>,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
|
||||
let mutation_result = payload.set_alias(alias);
|
||||
if let std::result::Result::Err(error) = mutation_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return self.persist_metadata_payload(destination.as_ref().to_path_buf(), payload, "wallet_update_alias").await;
|
||||
}
|
||||
|
||||
/// Adds one protected note and returns its generated stable note identifier after successful persistence.
|
||||
pub async fn add_note(
|
||||
&mut self,
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
text: std::string::String,
|
||||
) -> ksp_core_lib::Result<std::string::String> {
|
||||
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
|
||||
let note_id = match payload.add_note(text) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let persist_result = self.persist_metadata_payload(destination.as_ref().to_path_buf(), payload, "wallet_add_note").await;
|
||||
if let std::result::Result::Err(error) = persist_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(note_id);
|
||||
}
|
||||
|
||||
/// Updates one protected note selected by its stable identifier.
|
||||
pub async fn update_note(
|
||||
&mut self,
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
note_id: &str,
|
||||
text: std::string::String,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
|
||||
let mutation_result = payload.update_note(note_id, text);
|
||||
if let std::result::Result::Err(error) = mutation_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return self.persist_metadata_payload(destination.as_ref().to_path_buf(), payload, "wallet_update_note").await;
|
||||
}
|
||||
|
||||
/// Deletes one protected note selected by its stable identifier.
|
||||
pub async fn delete_note(&mut self, destination: impl std::convert::AsRef<std::path::Path>, note_id: &str) -> ksp_core_lib::Result<()> {
|
||||
let mut payload = crate::MetadataPayloadV1::from_info(&self.info);
|
||||
let mutation_result = payload.delete_note(note_id);
|
||||
if let std::result::Result::Err(error) = mutation_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return self.persist_metadata_payload(destination.as_ref().to_path_buf(), payload, "wallet_delete_note").await;
|
||||
}
|
||||
|
||||
/// Rotates the OWNER password while preserving the Wallet identity, metadata content key and Solana keypair.
|
||||
pub async fn rotate_owner_password(
|
||||
&mut self,
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
new_password: crate::OwnerPassword,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let envelope = match self.state.stage_owner_password_rotation(new_password).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
|
||||
if let std::result::Result::Err(error) = persist_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
self.state.apply_envelope(envelope);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_rotate_owner_password",
|
||||
capability = "owner",
|
||||
"wallet OWNER password rotated"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
/// Rotates the existing VIEW password without knowing the previous VIEW password.
|
||||
///
|
||||
/// This is a credential rotation only: it rewraps the same metadata content key in the same OWNER-signed VIEW slot descriptor.
|
||||
pub async fn rotate_view_password(
|
||||
&mut self,
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
new_password: crate::ViewPassword,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let envelope = match self.state.stage_view_password_rotation(new_password).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
|
||||
if let std::result::Result::Err(error) = persist_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
self.state.apply_envelope(envelope);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_rotate_view_password",
|
||||
capability = "owner",
|
||||
"wallet VIEW password rotated by OWNER"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
/// Strongly disables VIEW by generating a new metadata content key and removing the VIEW slot/descriptor.
|
||||
///
|
||||
/// Existing historical copies remain outside the revocation guarantee, but the old VIEW metadata key cannot decrypt future metadata states.
|
||||
pub async fn disable_view(&mut self, destination: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<()> {
|
||||
let (envelope, metadata_key) = match self.state.stage_disable_view() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
|
||||
if let std::result::Result::Err(error) = persist_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
self.state.apply_strong_view_state(envelope, metadata_key);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_disable_view",
|
||||
capability = "owner",
|
||||
"wallet VIEW capability strongly disabled"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
/// Strongly recreates VIEW with a fresh metadata content key, fresh slot identifier and new VIEW password.
|
||||
pub async fn recreate_view(
|
||||
&mut self,
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
new_password: crate::ViewPassword,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let (envelope, metadata_key) = match self.state.stage_recreate_view(new_password).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
|
||||
if let std::result::Result::Err(error) = persist_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
self.state.apply_strong_view_state(envelope, metadata_key);
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_recreate_view",
|
||||
capability = "owner",
|
||||
"wallet VIEW capability strongly recreated"
|
||||
);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
/// Serializes the complete locked `.kspwallet` V1 document without exposing any unlocked secret material.
|
||||
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
return self.state.envelope().to_json_bytes();
|
||||
}
|
||||
|
||||
async fn persist_metadata_payload(
|
||||
&mut self,
|
||||
destination: std::path::PathBuf,
|
||||
payload: crate::MetadataPayloadV1,
|
||||
operation: &'static str,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let (envelope, info) = match self.state.stage_metadata_payload(payload) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let persist_result = persist_staged(destination, self.state.envelope(), &envelope).await;
|
||||
if let std::result::Result::Err(error) = persist_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
self.state.apply_envelope(envelope);
|
||||
self.info = info;
|
||||
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, operation = operation, capability = "owner", "wallet protected metadata updated");
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WalletOwner {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.debug_struct("WalletOwner").field("info", &self.info).field("unlocked_state", &"<redacted>").finish();
|
||||
}
|
||||
}
|
||||
|
||||
async fn persist_staged(
|
||||
destination: std::path::PathBuf,
|
||||
expected_current: &crate::KspWalletEnvelopeV1,
|
||||
envelope: &crate::KspWalletEnvelopeV1,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let serialized = match envelope.to_json_bytes() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::replace_wallet_file_v1(destination, expected_current.clone(), serialized).await;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/administration.rs"]
|
||||
mod tests;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user