Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7acac3fc8 | |||
| 039b93cfb3 | |||
| c9a0f7fd37 | |||
| 704110d509 | |||
| fb32ed03f8 | |||
| 4439589f0d | |||
| 1e1bc0d421 | |||
| bd26d4f636 | |||
| 2a7f3e8f40 | |||
| 1d37f04d63 | |||
| 1ccf11bcc1 | |||
| d06284fca2 | |||
| fff69ead9c | |||
| c93a833bac | |||
| 05e0e96083 | |||
| b57f796187 | |||
| ecfb9500eb | |||
| e14ce1c36f | |||
| 83cb861e54 | |||
| 1d1bc6a4d6 | |||
| c4636ac8b9 | |||
| f15448ff6e | |||
| bec1f2ec08 | |||
| f625ee5979 | |||
| e68f073505 | |||
| bffb4f9a31 | |||
| 9059a2dc45 | |||
| 6c3ecf1f18 | |||
| 79b67f8eae | |||
| e0a7ac0bf8 | |||
| 598474438b | |||
| f5d98c4e69 | |||
| 14bcbf2cfb | |||
| ac1b1033c4 | |||
| bc71fba289 | |||
| 9c1568ee1c | |||
| c1cea6e813 | |||
| ed978179d8 | |||
| babe7d9f2b | |||
| d3fc0c6d69 | |||
| 0cff0406ab | |||
| d98d152f08 | |||
| 624202c363 | |||
| 24cf2c5a11 | |||
| e721464a7c | |||
| 259bff6707 | |||
| 7d40de3249 |
14
.env.example
14
.env.example
@@ -1,10 +1,22 @@
|
|||||||
# file: .env.example
|
# file: .env.example
|
||||||
# version: 2
|
# version: 3
|
||||||
|
|
||||||
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
|
# KSP Logging root directory. Used by config/std.logging.json for relative log output paths.
|
||||||
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
|
# The current Config document fallback is "logs" when neither the process environment nor .env defines this variable.
|
||||||
KSP_LOGS_DIRECTORY=logs
|
KSP_LOGS_DIRECTORY=logs
|
||||||
|
|
||||||
|
# Optional public Solana Devnet HTTP endpoint override used by config/std.transport.json.
|
||||||
|
# The committed Transport document falls back to https://api.devnet.solana.com when this variable is absent.
|
||||||
|
KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL=https://api.devnet.solana.com
|
||||||
|
|
||||||
|
# Optional public Solana Mainnet HTTP endpoint override used by config/std.transport.json and its example.
|
||||||
|
# The committed Transport document falls back to https://api.mainnet-beta.solana.com when this variable is absent.
|
||||||
|
KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL=https://api.mainnet-beta.solana.com
|
||||||
|
|
||||||
|
# Optional complete private-provider HTTP endpoint URL used only by the Transport example when explicitly selected.
|
||||||
|
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
|
||||||
|
# KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me
|
||||||
|
|
||||||
# Minimum time in milliseconds that a KSP desk splash remains visible after its frontend is ready.
|
# Minimum time in milliseconds that a KSP desk splash remains visible after its frontend is ready.
|
||||||
KSP_DESK_SPLASH_MINIMUM_MS=1200
|
KSP_DESK_SPLASH_MINIMUM_MS=1200
|
||||||
|
|
||||||
|
|||||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,10 +1,26 @@
|
|||||||
<!-- file: CHANGELOG.md -->
|
<!-- file: CHANGELOG.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# Changelog KSP
|
# Changelog KSP
|
||||||
|
|
||||||
Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`.
|
Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`.
|
||||||
|
|
||||||
|
## 0.2.3 — HTTP Transactions — 2026-08-18
|
||||||
|
|
||||||
|
`0.2.3` complète la famille HTTP Transactions de `ksp-onchain-transport-lib` avec 11 wrappers typés, portant la surface HTTP typée courante à 37 méthodes : 4 foundation, 22 Accounts/Tokens/Cluster et 11 Transactions. La release stabilise la classification exacte `8 Read / RetrySafe`, `2 WriteSubmission / NeverAfterDispatch` (`requestAirdrop`, `sendTransaction`) et `1 Simulation / RetrySafe` (`simulateTransaction`), avec preuves end-to-end qu'une write submission n'est jamais resoumise après dispatch ambigu mais peut retenter lorsqu'un échec est prouvé `NotDispatched`. `getTransaction` couvre la config moderne complète et la forme bare encoding legacy dépréciée ; les variantes de transaction, metadata, version et `transactionIndex` restent préservées losslessly. `simulateTransaction` couvre sa config complète, ses invariants déterministes et son résultat riche sans introduire de client RPC Solana haut niveau ni de décodage transactionnel dans Transport. La release formalise `KSP-TRANSPORT-007` et réaudite rétroactivement les 37 wrappers courants sans remédiation fonctionnelle. Les canaries conservent 52 méthodes HTTP courantes, 14 historiques Deprecated/Removed et 15 Blocks/Economics réservées à `0.2.4`. Les graphes Cargo et les deux smokes Devnet — Transport pur read-only et composition Config -> Transport transitoire — ont été validés avant publication. Le prompt `prompts/009-V0_2_4_START_PROMPT.md` ouvre `0.2.4 — HTTP Blocks + Economics + compliance HTTP finale`.
|
||||||
|
|
||||||
|
## 0.2.2 — HTTP Accounts + Tokens + Cluster — 2026-08-18
|
||||||
|
|
||||||
|
`0.2.2` complète la surface HTTP typée de `ksp-onchain-transport-lib` avec 22 wrappers supplémentaires : 5 Accounts, 5 Tokens et 12 Cluster, portant la couverture typée totale à 26 méthodes avec les quatre canaris foundation de `0.2.1`. La release stabilise les DTOs/configs wire communs, préserve les formes Account encodées/`jsonParsed`, les `null` et champs optionnels, les selectors Token exclusifs, les contextes RPC, les structures Cluster/leader/vote et les limites locales auditées (`getMultipleAccounts <= 100`, `getProgramAccounts <= 4` filtres, `memcmp` raw <= 128 octets, `getSlotLeaders` entre 1 et 5000). Les 22 descriptors restent `Read / RetrySafe` et passent exclusivement par le flux central `descriptor -> execute_standard_rpc -> pool/admission -> executor HTTP`. La release n'ajoute aucune dépendance externe, ne crée aucun client HTTP parallèle et conserve le firewall Transport -> Config/Store/Program. Elle ajoute un smoke Devnet pur à Transport avec settings programmatiques couvrant Accounts/Tokens/Cluster, conserve séparément le smoke historique Config -> Transport comme exception transitoire, et valide les deux explicitement avant publication. Les canaries figent toujours 52 méthodes HTTP courantes, 14 historiques Deprecated/Removed et la partition typed `4 / 22 / 11 / 15`. Le prompt `prompts/008-V0_2_3_START_PROMPT.md` ouvre `0.2.3 — HTTP Transactions`.
|
||||||
|
|
||||||
|
## 0.2.1 — HTTP Solana foundation — 2026-08-17
|
||||||
|
|
||||||
|
`0.2.1` stabilise `ksp-onchain-transport-lib` comme foundation HTTP JSON-RPC Solana provider-neutral : settings publics, endpoints/pool/rôles, priorités et fairness, RPS/burst/concurrence/cooldown, deadline commune, retry/backoff, classification no-resend après dispatch ambigu, snapshots sûrs et exécution HTTP réelle via `reqwest`/rustls. La release fige un registre audité de 52 méthodes HTTP courantes et 14 méthodes historiques Deprecated/Removed, avec quatre wrappers typés canari (`getBalance`, `getGenesisHash`, `getHealth`, `getVersion`) et une partition explicite des 48 méthodes restantes sur `0.2.2`–`0.2.4`. Elle ajoute `std.transport`, son schema/exemple et l'adapter `ksp-config-lib -> ksp-onchain-transport-lib`, sans dépendance inverse, ainsi que la redaction des URLs/provider credentials, la neutralisation des URLs contenues dans les `reqwest::Error`, un sink Logging Transport dédié à `info`, des fixtures HTTP déterministes, des canaries de complétude et un smoke Devnet opt-in validant la composition Config -> Transport. Le smoke cross-crates reste temporairement hébergé dans Config et doit migrer vers une future surface d'intégration/orchestration ; ce placement n'est pas un modèle pour les futurs smokes `Config + autre crate`. Le prompt `prompts/007-V0_2_2_START_PROMPT.md` ouvre `0.2.2 — HTTP Accounts + Tokens + Cluster`.
|
||||||
|
|
||||||
|
## 0.2.0 — Audit bot3 et planification de la série `0.2.x` — 2026-08-17
|
||||||
|
|
||||||
|
`0.2.0` stabilise le cadrage de la prochaine phase fonctionnelle de KSP après audit de `khadhroony-bot3`. La release fixe l'ordre `0.2.1+` autour du transport HTTP Solana, du Wallet `.kspwallet`, de Wallet Desk, des transports WebSocket/LaserStream/Yellowstone, du transport off-chain de prix, de `ksp-interface-lib` et de `ksp-program-api`; elle impose la couverture exhaustive des surfaces Transport documentées avec warnings KSP pour les opérations deprecated/obsolete encore fonctionnelles et unstable/experimental. Elle stabilise également la progression durable `RAW -> CORE -> DECODE -> SPECIALIZED`, RAW/CORE sans décodage Program, puis des vertical slices complets par groupe à partir de DECODE, avec priorité Solana Core, SPL token/trading, metadata token, Anchor, Meteora/Raydium/Pump/Orca, routing et Market Desk progressive. Le prompt `prompts/006-V0_2_1_START_PROMPT.md` ouvre `0.2.1 — ksp-onchain-transport-lib / HTTP Solana foundation` avec un gate de sizing imposant qu'une release concrète reste clôturable dans une seule session.
|
||||||
|
|
||||||
## 0.1.4 — Config Desk — 2026-08-17
|
## 0.1.4 — Config Desk — 2026-08-17
|
||||||
|
|
||||||
`0.1.4` stabilise `ksp-app-config-desk` comme première application desktop/Tauri spécialisée et modèle de référence des futures applications KSP. La release valide de bout en bout les contrats de `ksp-config-lib` et le lifecycle de `ksp-logging-lib` : shell splash/main, inventaire et diagnostics des documents Config, profils et provenance sûre, management `.env` avec shadowing et reveal Secret privilégié, éditeur Logging typé multi-profils/multi-sinks, persistence atomique, hot reload transactionnel, rollback, sélection runtime explicite, génération observable, fichiers de logs distincts par lancement, bridge frontend vers la façade KSP et panneau Test Logging pour démontrer le routing niveau/target/domain. Elle ajoute également les audits desktop/ownership/sécurité, un registre extensible `file_id -> éditeur spécialisé`, une baseline Logging de release `info`/`warn`, et prépare `0.2.0` comme release intermédiaire d’audit de `khadhroony-bot3` et de planification du reste de `0.2.x`.
|
`0.1.4` stabilise `ksp-app-config-desk` comme première application desktop/Tauri spécialisée et modèle de référence des futures applications KSP. La release valide de bout en bout les contrats de `ksp-config-lib` et le lifecycle de `ksp-logging-lib` : shell splash/main, inventaire et diagnostics des documents Config, profils et provenance sûre, management `.env` avec shadowing et reveal Secret privilégié, éditeur Logging typé multi-profils/multi-sinks, persistence atomique, hot reload transactionnel, rollback, sélection runtime explicite, génération observable, fichiers de logs distincts par lancement, bridge frontend vers la façade KSP et panneau Test Logging pour démontrer le routing niveau/target/domain. Elle ajoute également les audits desktop/ownership/sécurité, un registre extensible `file_id -> éditeur spécialisé`, une baseline Logging de release `info`/`warn`, et prépare `0.2.0` comme release intermédiaire d’audit de `khadhroony-bot3` et de planification du reste de `0.2.x`.
|
||||||
|
|||||||
17
Cargo.toml
17
Cargo.toml
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 92
|
# version: 131
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
|
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.4"
|
version = "0.2.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
@@ -15,15 +15,16 @@ publish = false
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
fs2 = { version = "^0.4" }
|
fs2 = { version = "^0.4" }
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
serde = { version = "^1.0" }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
jsonschema = { version = "^0.49", default-features = false }
|
jsonschema = { version = "^0.49", default-features = false }
|
||||||
|
reqwest = { version = "^0.13", default-features = false }
|
||||||
solana-pubkey = { version = "^4.3", default-features = false }
|
solana-pubkey = { version = "^4.3", default-features = false }
|
||||||
tracing = { version = "^0.1", default-features = false, features = ["std"] }
|
tracing = { version = "^0.1", default-features = false }
|
||||||
tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt", "json", "ansi"] }
|
tracing-subscriber = { version = "^0.3", default-features = false }
|
||||||
tracing-appender = { version = "^0.2", default-features = false }
|
tracing-appender = { version = "^0.2", default-features = false }
|
||||||
tokio = { version = "^1.53", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time"] }
|
tokio = { version = "^1.53", default-features = false }
|
||||||
chrono = { version = "^0.4", default-features = false, features = ["std", "now"] }
|
chrono = { version = "^0.4", default-features = false }
|
||||||
tauri = { version = "^2.11" }
|
tauri = { version = "^2.11" }
|
||||||
tauri-build = { version = "^2.6" }
|
tauri-build = { version = "^2.6" }
|
||||||
tauri-plugin-tracing = { version = "^0.3" }
|
tauri-plugin-tracing = { version = "^0.3" }
|
||||||
|
|||||||
188
ROADMAP.md
188
ROADMAP.md
@@ -1,11 +1,9 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 20 -->
|
<!-- version: 40 -->
|
||||||
|
|
||||||
# Roadmap KSP
|
# Roadmap KSP
|
||||||
|
|
||||||
Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues pour y parvenir. Une série `X.Y.x` regroupe une famille fonctionnelle de travaux ; elle peut contenir plusieurs releases concrètes et plusieurs sessions.
|
Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. Une série `X.Y.x` regroupe une famille fonctionnelle ; chaque release concrète reste une unité de développement/session distincte.
|
||||||
|
|
||||||
Les décisions architecturales négatives ou de prudence n'apparaissent pas comme des tâches à cocher. Elles sont conservées dans les règles et documents d'architecture.
|
|
||||||
|
|
||||||
## Légende
|
## Légende
|
||||||
|
|
||||||
@@ -15,6 +13,14 @@ Les décisions architecturales négatives ou de prudence n'apparaissent pas comm
|
|||||||
- `[C]` — annulé ;
|
- `[C]` — annulé ;
|
||||||
- `[R]` — reporté.
|
- `[R]` — reporté.
|
||||||
|
|
||||||
|
## Discipline de livraison
|
||||||
|
|
||||||
|
- Une prerelease vise environ **15 à 20 minutes de travail effectif**.
|
||||||
|
- Une release concrète doit être dimensionnée pour pouvoir être ouverte et clôturée dans **une seule session de chat**.
|
||||||
|
- Cette règle fixe une durée maximale par release, pas une obligation de changer de session après chaque release : si une release est clôturée plus vite que prévu, la même session peut ouvrir puis clôturer la release suivante si son propre sizing reste positif et si toutes les frontières version/delta/validation sont conservées.
|
||||||
|
- Si `pre.001` révèle qu'une release est trop grosse, elle est scindée avant implémentation fonctionnelle lourde.
|
||||||
|
- À partir des couches Program/Decode, KSP progresse verticalement groupe par groupe plutôt que par grandes vagues horizontales de decoders/materializers/executors séparés.
|
||||||
|
|
||||||
## 0.0.x — Fondation
|
## 0.0.x — Fondation
|
||||||
|
|
||||||
- [X] `0.0.1` — Initialiser le dépôt.
|
- [X] `0.0.1` — Initialiser le dépôt.
|
||||||
@@ -25,97 +31,127 @@ Les décisions architecturales négatives ou de prudence n'apparaissent pas comm
|
|||||||
|
|
||||||
## 0.1.x — Fondations N1
|
## 0.1.x — Fondations N1
|
||||||
|
|
||||||
### Objectifs
|
- [X] `0.1.1` — `ksp-core-lib` : Error/Result, Program IDs fondamentaux et primitives N1.
|
||||||
|
- [X] `0.1.2` — `ksp-logging-lib` : façade KSP de tracing.
|
||||||
|
- [X] `0.1.3` — `ksp-config-lib` : documents, profils, environnement, persistence et adapters.
|
||||||
|
- [X] `0.1.4` — `ksp-app-config-desk` : première application Tauri de référence.
|
||||||
|
|
||||||
Regrouper les releases consacrées aux fondations N1. Chaque release concrète est une unité de développement/session distincte et commence par son propre `pre.001` de brainstorming/audit/planification.
|
## 0.2.x — Accès Solana, Wallet et contrats d'extension initiaux
|
||||||
|
|
||||||
### Releases concrètes
|
### Cadrage
|
||||||
|
|
||||||
- [X] `0.1.1` — Stabiliser `ksp-core-lib` : `Error`/`Result`, Program IDs fondamentaux et primitives réellement N1.
|
- [X] `0.2.0` — Audit bot3, ordre fonctionnel de `0.2.x`, architecture durable, discipline de sizing et pipeline RAW/CORE/DECODE/SPECIALIZED stabilisés.
|
||||||
- [X] `0.1.2` — Introduire `ksp-logging-lib` comme façade KSP de `tracing`, `tracing-appender` et `tracing-subscriber`.
|
- [X] `0.2.1` — HTTP foundation stable : matrice 52+14, runtime/routing/résilience/exécution HTTP, 4 canaris typés, `std.transport`, adapter Config -> Transport, canaries de clôture, smoke Devnet opt-in et documentation durable validés ; les 48 wrappers typés restants sont reportés à `0.2.2`–`0.2.4`.
|
||||||
- [X] `0.1.3` — Stabiliser `ksp-config-lib` : documents, profils, résolution, validation, environnement KSP/KSPB, management/persistence et adapter Logging.
|
|
||||||
- [X] `0.1.4` — Introduire `ksp-app-config-desk` pour valider réellement Config et la frontière Tauri.
|
|
||||||
|
|
||||||
`0.1.1`, `0.1.2`, `0.1.3` et `0.1.4` sont désormais stables. `0.1.4` publie `ksp-app-config-desk` comme première validation desktop/Tauri de Config et modèle de référence des futures applications Tauri KSP. La prochaine session est `0.2.0`, release intermédiaire d’audit de `khadhroony-bot3` et de planification du reste de `0.2.x`.
|
### Releases fonctionnelles décidées/pressenties
|
||||||
|
|
||||||
Les contrats publics supplémentaires ne sont introduits que lorsqu'une release concrète en démontre le besoin.
|
- [X] `0.2.1` — **HTTP transport foundation réduite par le gate `pre.001`** : crate/settings/JSON-RPC/registry 52 current + 14 deprecated historiques, pool/rôles/limites/retry, Config adapter, documentation et 4 méthodes typées canari (`getBalance`, `getGenesisHash`, `getHealth`, `getVersion`) publiés stables.
|
||||||
|
- [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.
|
||||||
|
- [ ] `0.2.4` — Compléter les 10 méthodes HTTP Blocks + 5 Economics et exécuter la compliance finale de toute la surface HTTP 52 current + 14 deprecated historiques.
|
||||||
|
- [ ] `0.2.5` — Introduire `ksp-wallet-lib`, le format `.kspwallet`, la gestion sûre des secrets et une architecture d'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.
|
||||||
|
- [ ] `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.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.
|
||||||
|
|
||||||
## 0.2.x — Accès Solana et fondation programmes
|
### Règles Transport pour toute la série
|
||||||
|
|
||||||
### Release de cadrage `0.2.0`
|
- [ ] Couvrir toutes les méthodes/opérations documentées pour la surface normative ciblée par chaque release.
|
||||||
|
- [ ] Conserver les méthodes deprecated/obsolete encore fonctionnelles et émettre un `warn` KSP lors de leur utilisation.
|
||||||
|
- [ ] Implémenter les méthodes unstable/experimental ciblées et émettre un `warn` KSP lors de leur utilisation.
|
||||||
|
- [ ] Centraliser la metadata de statut des méthodes plutôt que disperser des warnings ad hoc.
|
||||||
|
- [ ] Garder `ksp-onchain-transport-lib` indépendant de `ksp-config-lib`, du Store et des modèles Program/métier.
|
||||||
|
|
||||||
- [ ] `0.2.0` — Auditer les fonctionnalités pertinentes de `khadhroony-bot3`, décider ce qui doit être repris, refondu, abandonné ou ajouté dans KSP, puis découper et ordonner les releases fonctionnelles restantes de `0.2.x`.
|
## Architecture de données — progression canonique
|
||||||
|
|
||||||
`0.2.0` est une release intermédiaire de transition et de planification de série. Elle ne doit pas démarrer par l'implémentation arbitraire d'un composant N2 : elle établit d'abord la cartographie fonctionnelle, les écarts avec KSP, les dépendances, les contrats à préserver ou redéfinir et le découpage concret de `0.2.1`, `0.2.2`, etc.
|
La chaîne durable cible est :
|
||||||
|
|
||||||
### Capacités à répartir dans les releases fonctionnelles suivantes
|
```text
|
||||||
|
RAW -> CORE -> DECODE -> SPECIALIZED
|
||||||
|
```
|
||||||
|
|
||||||
- [ ] Introduire `ksp-onchain-transport-lib` avec des modèles de transport homogènes indépendants du store.
|
- **RAW** et **CORE** ne nécessitent aucun décodage Program.
|
||||||
- [ ] Introduire `ksp-wallet-lib` et `ksp-app-wallet-desk`.
|
- À la fin de chaque couche horizontale RAW/CORE, ajouter les jobs/workers/apps nécessaires pour la rendre réellement exploitable avant d'ouvrir la couche suivante.
|
||||||
- [ ] Développer la première surface utile de `ksp-interface-lib`.
|
- À partir de **DECODE**, avancer verticalement groupe par groupe : wire -> decode -> matérialisation -> projection spécialisée si utile -> préparation d'exécution -> policy -> execution -> scénarios Devnet.
|
||||||
- [ ] Introduire `ksp-program-api` puis `ksp-program-lib`.
|
|
||||||
- [ ] Définir `ksp-execution-policy-api` comme contrat de policy commun à plusieurs contextes.
|
|
||||||
- [ ] Introduire `ksp-execution-lib` lorsque le premier cycle d'exécution réel justifie l'orchestration programme/policy/wallet/transport.
|
|
||||||
- [ ] Introduire `ksp-offchain-transport-lib` seulement au premier besoin réel.
|
|
||||||
|
|
||||||
## 0.3.x — Données, stockage et acquisition raw
|
## 0.3.x — RAW / acquisition persistée
|
||||||
|
|
||||||
- [ ] Introduire `ksp-materializer-api` / `ksp-materializer-lib`.
|
- [ ] `0.3.1` — Introduire `ksp-store-api` + `ksp-store-lib` avec PostgreSQL de référence et **modèles/persistence RAW uniquement**.
|
||||||
- [ ] Introduire `ksp-store-api` / `ksp-store-lib` avec PostgreSQL de référence.
|
- [ ] `0.3.2` — Étendre `ksp-interface-lib` avec les wires génériques nécessaires aux acquisitions et à la future normalisation CORE.
|
||||||
- [ ] Établir les niveaux durables D1 Raw, D2 Core, D3 journal de matérialisation générique et D4 projections de domaine.
|
- [ ] `0.3.3` — Introduire `ksp-job-api` et un job de backfill historique concret.
|
||||||
- [ ] Garantir des replays indépendants D1 -> D2, D2 -> D3 et D3 -> D4.
|
- [ ] `0.3.4` — Introduire une application spécialisée de backfill/inspection RAW.
|
||||||
- [ ] Introduire `ksp-app-store-desk`.
|
- [ ] Compléter ensuite la couche RAW avec le worker/service live, son contrôle et les outils d'exploitation réellement nécessaires avant de passer à CORE.
|
||||||
- [ ] Introduire `ksp-worker-api` et `ksp-worker-raw-retriever`.
|
|
||||||
- [ ] Introduire `ksp-worker-control-lib` lorsque le manager W1 crée le premier besoin concret.
|
|
||||||
- [ ] Introduire `ksp-job-api` et `ksp-job-backfill`.
|
|
||||||
- [ ] Introduire les pipelines spécialisés raw ingestion, Core processing, generic materialization et domain projection lorsque leurs premières frontières fonctionnelles sont développées.
|
|
||||||
- [ ] Introduire les jobs de replay indépendants D1 -> D2, D2 -> D3 et D3 -> D4.
|
|
||||||
- [ ] Normaliser les notifications de données persistées indépendamment de leur producteur et conserver le Store comme source de vérité du backlog.
|
|
||||||
- [ ] Mettre en place claim/lease, outcomes durables et reprise après crash pour les traitements concurrents.
|
|
||||||
|
|
||||||
## 0.4.x — Baseline Solana, SPL et metadata
|
## Série CORE suivante
|
||||||
|
|
||||||
- [ ] Ajouter progressivement les decoders et `ProgramExecutionPreparer` Core/SPL nécessaires.
|
- [ ] Définir la persistence CORE canonique Solana générique.
|
||||||
- [ ] Ajouter Token, Token-2022, ATA et metadata utiles.
|
- [ ] Implémenter `RAW -> CORE` sans decoder Program : blocs, slots, signatures, transactions/messages, comptes, instructions/CPI brutes, logs/meta et relations structurelles.
|
||||||
- [ ] Introduire `ksp-offchain-transport-lib` au plus tard au premier besoin externe.
|
- [ ] Ajouter replay/backfill RAW -> CORE.
|
||||||
- [ ] Ajouter materializers et jobs ponctuels nécessaires.
|
- [ ] Ajouter worker/service CORE.
|
||||||
- [ ] Ajouter les crates `ksp-scenario-<domain>-lib` spécialisées.
|
- [ ] Ajouter l'application de contrôle/inspection CORE utile.
|
||||||
- [ ] Ajouter les demos `ksp-app-scenario-<domain>-<environment>-desk-demo` correspondantes.
|
|
||||||
- [ ] Garder Memo, Token, ATA, Token-2022 séparés ; regrouper uniquement Metaplex Token Metadata + Token-2022 Metadata dans la famille metadata ; garder SPM séparé.
|
|
||||||
|
|
||||||
## 0.5.x — Anchor et protocoles trading
|
## Séries DECODE/SPECIALIZED/EXECUTION — progression verticale
|
||||||
|
|
||||||
- [ ] Introduire Anchor.
|
### Priorité 1 — Solana Core Programs
|
||||||
- [ ] Étendre Meteora par surfaces bornées.
|
|
||||||
- [ ] Étendre Raydium par surfaces bornées.
|
|
||||||
- [ ] Ajouter progressivement Pump, Orca, Jupiter, OKX et autres intégrations utiles.
|
|
||||||
- [ ] Ajouter interfaces, program implementations, materializers, jobs/scénarios/demos nécessaires pour chaque surface.
|
|
||||||
|
|
||||||
## 0.6.x — Processing autonome et orchestration
|
- [ ] Wire/decoding des programmes Core nécessaires transversalement.
|
||||||
|
- [ ] Matérialisation et projections utiles.
|
||||||
|
- [ ] Préparation d'exécution, policy et scénarios Devnet pour les opérations retenues.
|
||||||
|
|
||||||
- [ ] Introduire `ksp-worker-core-processor` pour D1 Raw -> D2 Core canonique.
|
### Priorité 2 — SPL token/trading
|
||||||
- [ ] Introduire `ksp-worker-generic-materializer` pour D2 Core -> D3 journal de matérialisation générique.
|
|
||||||
- [ ] Introduire `ksp-worker-domain-projector` pour D3 -> D4 projections spécialisées ; nom révisable.
|
|
||||||
- [ ] Exploiter les mêmes pipelines spécialisés pour les workers live et les jobs de replay afin d'éviter la duplication des frontières de processing.
|
|
||||||
- [ ] Étendre `ksp-worker-control-lib` à la gouvernance de plusieurs workers autonomes.
|
|
||||||
- [ ] Fournir pour chaque worker un mode service autonome, avec logique réutilisable séparée du binaire d'enveloppe.
|
|
||||||
- [ ] Construire d'abord les applications spécialisées nécessaires au développement, test et exploitation de chaque capacité.
|
|
||||||
- [ ] Garder jobs et workers sous des lifecycle APIs séparées.
|
|
||||||
|
|
||||||
## 0.7.x — Trading Intelligence
|
- [ ] SPL Token.
|
||||||
|
- [ ] Associated Token Account.
|
||||||
|
- [ ] Token-2022 et extensions pertinentes.
|
||||||
|
- [ ] Pour chaque famille : decode -> materialize -> specialized -> prepare -> policy -> execute -> scenarios.
|
||||||
|
|
||||||
- [ ] Statistiques et métriques.
|
### Priorité 3 — Metadata token
|
||||||
- [ ] Features et datasets historiques.
|
|
||||||
- [ ] Signaux et risque.
|
- [ ] Metaplex Token Metadata.
|
||||||
|
- [ ] Token-2022 Metadata.
|
||||||
|
- [R] Solana Program Metadata (SPM) — redéveloppement plus tard avec le décodage généraliste.
|
||||||
|
|
||||||
|
### Priorité 4 — Anchor
|
||||||
|
|
||||||
|
- [ ] Introduire les contrats et mécanismes Anchor nécessaires aux protocoles trading suivants.
|
||||||
|
|
||||||
|
### Priorité 5 — DEX à fort intérêt
|
||||||
|
|
||||||
|
- [ ] Meteora, y compris vaults/fees/positions/états auxiliaires nécessaires à son groupe.
|
||||||
|
- [ ] Raydium, y compris programmes satellites nécessaires.
|
||||||
|
- [ ] Pump, y compris fee program et composants launch/bonding/pool nécessaires.
|
||||||
|
- [ ] Orca, y compris programmes satellites nécessaires.
|
||||||
|
- [ ] Chaque groupe est terminé verticalement avant de devenir secondaire au profit du suivant.
|
||||||
|
|
||||||
|
### Market Desk V1
|
||||||
|
|
||||||
|
- [ ] Après les premiers groupes Meteora/Raydium/Pump/Orca, introduire une petite `ksp-app-market-desk` spécialisée.
|
||||||
|
- [ ] Visualiser tokens, pools/markets, liquidité, swaps/trades, prix, volumes, OHLC/candles et activité live/récente lorsque disponible.
|
||||||
|
- [ ] Lire les projections SPECIALIZED KSP ; ne pas reconstruire la logique protocolaire dans l'UI.
|
||||||
|
|
||||||
|
### Routing
|
||||||
|
|
||||||
|
- [ ] Jupiter.
|
||||||
|
- [ ] OKX et autres routeurs selon besoin réel.
|
||||||
|
- [ ] Enrichir Market Desk avec routes, legs, DEX impliqués, fees/slippage et comparaison quote/execution lorsque disponible.
|
||||||
|
|
||||||
|
### Trading-adjacent puis décodage généraliste
|
||||||
|
|
||||||
|
- [ ] Ajouter ensuite les programmes indépendants utiles au trading : oracles, locks/vesting indépendants, lifecycle token, risk/signaux, etc.
|
||||||
|
- [ ] Ne jamais y repousser un satellite appartenant à un groupe DEX déjà ciblé.
|
||||||
|
- [ ] Étendre enfin le décodage au reste de Solana selon valeur fonctionnelle.
|
||||||
|
|
||||||
|
## Trading Intelligence et produits ultérieurs
|
||||||
|
|
||||||
|
- [ ] Statistiques/features/datasets historiques.
|
||||||
|
- [ ] Signaux, risque, anomalies et patterns.
|
||||||
- [ ] Replay analytique/backtests.
|
- [ ] Replay analytique/backtests.
|
||||||
- [ ] Patterns/anomalies.
|
- [ ] XGBoost puis autres modèles lorsque les données et contrats sont stables.
|
||||||
- [ ] XGBoost puis autres modèles lorsque les contrats sont stables.
|
- [ ] Construire ensuite les produits de trading opérationnel au-dessus de ces couches.
|
||||||
|
- [ ] Faire évoluer Market Desk vers davantage d'analyse sans la confondre avec l'application globale ou l'orchestrateur.
|
||||||
## 0.8.x et suivantes — Trading opérationnel et expansion produits
|
- [ ] Étudier plus tard les autres applications Wallet : mobile, extensions navigateur et web.
|
||||||
|
|
||||||
- [ ] Construire les couches puis l'application de trading monoposte au-dessus de Trading Intelligence.
|
|
||||||
- [ ] Étendre l'automatisation de trading.
|
|
||||||
- [ ] Étendre continuellement Program IDs, decoders, execution preparers et materializers.
|
|
||||||
- [ ] Construire progressivement l'explorer Solana.
|
|
||||||
- [ ] Construire progressivement l'explorer/analyse DEX.
|
|
||||||
- [ ] Étudier plus tard d'autres applications utilisant `ksp-wallet-lib`, notamment mobile, extensions navigateur et web.
|
|
||||||
|
|||||||
@@ -61,4 +61,4 @@
|
|||||||
"target_filters": []
|
"target_filters": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
64
config/examples/std.transport.example.json
Normal file
64
config/examples/std.transport.example.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"retry": {
|
||||||
|
"max_retries": 3,
|
||||||
|
"initial_backoff_ms": 150,
|
||||||
|
"max_backoff_ms": 3000
|
||||||
|
},
|
||||||
|
"default_profile": "mainnet_mixed",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "mainnet_mixed",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "mainnet_public",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
|
||||||
|
"connect_timeout_ms": 5000,
|
||||||
|
"request_timeout_ms": 15000,
|
||||||
|
"max_idle_connections_per_host": 8,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "default",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": ["*"],
|
||||||
|
"priority": 200,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 5,
|
||||||
|
"burst_capacity": 10,
|
||||||
|
"max_concurrent_requests": 8,
|
||||||
|
"pause_after_rate_limit_ms": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mainnet_private",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "private-provider",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"url": "${KSP_SECRET_SOLANA_HTTP_URL:-https://example.invalid}",
|
||||||
|
"connect_timeout_ms": 5000,
|
||||||
|
"request_timeout_ms": 10000,
|
||||||
|
"max_idle_connections_per_host": 16,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "default",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": ["*"],
|
||||||
|
"priority": 100,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 20,
|
||||||
|
"burst_capacity": 40,
|
||||||
|
"max_concurrent_requests": 16,
|
||||||
|
"pause_after_rate_limit_ms": 750
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -263,4 +263,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
124
config/schemas/std.transport.schema.json
Normal file
124
config/schemas/std.transport.schema.json
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "urn:ksp:schema:std.transport:v1",
|
||||||
|
"title": "KSP standard HTTP Transport configuration",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["format_version", "retry", "default_profile", "profiles"],
|
||||||
|
"properties": {
|
||||||
|
"format_version": {"const": 1},
|
||||||
|
"retry": {"$ref": "#/$defs/retry"},
|
||||||
|
"default_profile": {"$ref": "#/$defs/profileId"},
|
||||||
|
"profiles": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {"$ref": "#/$defs/profile"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"profileId": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z0-9][a-z0-9._-]*$"
|
||||||
|
},
|
||||||
|
"descriptor": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"pattern": "^\\S(?:.*\\S)?$"
|
||||||
|
},
|
||||||
|
"positiveMs": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 4294967295
|
||||||
|
},
|
||||||
|
"positiveU32": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 4294967295
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["max_retries", "initial_backoff_ms", "max_backoff_ms"],
|
||||||
|
"properties": {
|
||||||
|
"max_retries": {"type": "integer", "minimum": 0, "maximum": 100},
|
||||||
|
"initial_backoff_ms": {"$ref": "#/$defs/positiveMs"},
|
||||||
|
"max_backoff_ms": {"$ref": "#/$defs/positiveMs"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"limits": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"requests_per_second": {"$ref": "#/$defs/positiveU32"},
|
||||||
|
"burst_capacity": {"$ref": "#/$defs/positiveU32"},
|
||||||
|
"max_concurrent_requests": {"$ref": "#/$defs/positiveU32"},
|
||||||
|
"pause_after_rate_limit_ms": {"$ref": "#/$defs/positiveMs"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["role", "enabled", "request_kinds", "priority", "limits"],
|
||||||
|
"properties": {
|
||||||
|
"role": {"$ref": "#/$defs/descriptor"},
|
||||||
|
"enabled": {"type": "boolean"},
|
||||||
|
"request_kinds": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {"$ref": "#/$defs/descriptor"},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {"contains": {"const": "*"}},
|
||||||
|
"then": {"maxItems": 1}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"priority": {"type": "integer", "minimum": 0, "maximum": 4294967295},
|
||||||
|
"limits": {"$ref": "#/$defs/limits"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"endpoint": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"enabled",
|
||||||
|
"provider",
|
||||||
|
"cluster",
|
||||||
|
"url",
|
||||||
|
"connect_timeout_ms",
|
||||||
|
"request_timeout_ms",
|
||||||
|
"roles"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {"$ref": "#/$defs/descriptor"},
|
||||||
|
"enabled": {"type": "boolean"},
|
||||||
|
"provider": {"$ref": "#/$defs/descriptor"},
|
||||||
|
"cluster": {"$ref": "#/$defs/descriptor"},
|
||||||
|
"url": {"type": "string", "minLength": 1},
|
||||||
|
"connect_timeout_ms": {"$ref": "#/$defs/positiveMs"},
|
||||||
|
"request_timeout_ms": {"$ref": "#/$defs/positiveMs"},
|
||||||
|
"max_idle_connections_per_host": {"type": "integer", "minimum": 1},
|
||||||
|
"roles": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {"$ref": "#/$defs/role"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["profile_id", "endpoints"],
|
||||||
|
"properties": {
|
||||||
|
"profile_id": {"$ref": "#/$defs/profileId"},
|
||||||
|
"endpoints": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {"$ref": "#/$defs/endpoint"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,23 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"output_id": "file.onchain_transport.info",
|
||||||
|
"enabled": true,
|
||||||
|
"path": "transport/onchain/ksp-onchain-transport.log",
|
||||||
|
"rotation": "daily",
|
||||||
|
"format": "human",
|
||||||
|
"ansi": false,
|
||||||
|
"filter": {
|
||||||
|
"level": "info",
|
||||||
|
"targets": [
|
||||||
|
"ksp-onchain-transport-lib"
|
||||||
|
],
|
||||||
|
"domains": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"output_id": "file.config.error",
|
"output_id": "file.config.error",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -70,6 +87,10 @@
|
|||||||
{
|
{
|
||||||
"target_prefix": "ksp-app-config-desk",
|
"target_prefix": "ksp-app-config-desk",
|
||||||
"level": "info"
|
"level": "info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target_prefix": "ksp-onchain-transport-lib",
|
||||||
|
"level": "info"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
69
config/std.transport.json
Normal file
69
config/std.transport.json
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"retry": {
|
||||||
|
"max_retries": 2,
|
||||||
|
"initial_backoff_ms": 100,
|
||||||
|
"max_backoff_ms": 2000
|
||||||
|
},
|
||||||
|
"default_profile": "devnet_public",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "devnet_public",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "solana_devnet_public",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "devnet",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL:-https://api.devnet.solana.com}",
|
||||||
|
"connect_timeout_ms": 5000,
|
||||||
|
"request_timeout_ms": 15000,
|
||||||
|
"max_idle_connections_per_host": 8,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "default",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": ["*"],
|
||||||
|
"priority": 100,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 5,
|
||||||
|
"burst_capacity": 10,
|
||||||
|
"max_concurrent_requests": 8,
|
||||||
|
"pause_after_rate_limit_ms": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "mainnet_public",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "solana_mainnet_public",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_MAINNET_HTTP_URL:-https://api.mainnet-beta.solana.com}",
|
||||||
|
"connect_timeout_ms": 5000,
|
||||||
|
"request_timeout_ms": 15000,
|
||||||
|
"max_idle_connections_per_host": 8,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "default",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": ["*"],
|
||||||
|
"priority": 100,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 5,
|
||||||
|
"burst_capacity": 10,
|
||||||
|
"max_concurrent_requests": 8,
|
||||||
|
"pause_after_rate_limit_ms": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-app-config-desk/Cargo.toml
|
# file: crates/ksp-app-config-desk/Cargo.toml
|
||||||
# version: 7
|
# version: 8
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-app-config-desk"
|
name = "ksp-app-config-desk"
|
||||||
@@ -26,12 +26,12 @@ fs2.workspace = true
|
|||||||
ksp-config-lib = { path = "../ksp-config-lib" }
|
ksp-config-lib = { path = "../ksp-config-lib" }
|
||||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||||
serde.workspace = true
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tauri.workspace = true
|
tauri.workspace = true
|
||||||
tauri-plugin-tracing.workspace = true
|
tauri-plugin-tracing.workspace = true
|
||||||
chrono.workspace = true
|
chrono = { workspace = true, features = ["std", "now"] }
|
||||||
tokio.workspace = true
|
tokio = { workspace = true, features = ["time"] }
|
||||||
ts-rs.workspace = true
|
ts-rs.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-app-config-desk/README.md -->
|
<!-- file: crates/ksp-app-config-desk/README.md -->
|
||||||
<!-- version: 23 -->
|
<!-- version: 24 -->
|
||||||
|
|
||||||
# `ksp-app-config-desk`
|
# `ksp-app-config-desk`
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ Deux audits d'intégration applicatifs complètent les audits Config/Logging exi
|
|||||||
Les artefacts frontend construits ne sont pas versionnés. `tauri.conf.json` fixe :
|
Les artefacts frontend construits ne sont pas versionnés. `tauri.conf.json` fixe :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
../../builds/khadhroony-solana-project/ksp-app-config-desk/dist
|
../../../builds/khadhroony-solana-project/ksp-app-config-desk/dist
|
||||||
```
|
```
|
||||||
|
|
||||||
`vite.config.ts` résout cette même destination depuis la racine de la crate, ce qui maintient `dist` hors du workspace source et l'aligne avec la stratégie `.cargo/config.toml` pour les artefacts Rust.
|
`vite.config.ts` résout cette même destination depuis la racine de la crate, ce qui maintient `dist` hors du workspace source et l'aligne avec la stratégie `.cargo/config.toml` pour les artefacts Rust.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-app-config-desk/USAGE.md -->
|
<!-- file: crates/ksp-app-config-desk/USAGE.md -->
|
||||||
<!-- version: 23 -->
|
<!-- version: 24 -->
|
||||||
|
|
||||||
# Utilisation de `ksp-app-config-desk`
|
# Utilisation de `ksp-app-config-desk`
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ npm run build
|
|||||||
Vite construit les pages `main.html` et `splash.html` vers :
|
Vite construit les pages `main.html` et `splash.html` vers :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
../../builds/khadhroony-solana-project/ksp-app-config-desk/dist
|
../../../builds/khadhroony-solana-project/ksp-app-config-desk/dist
|
||||||
```
|
```
|
||||||
|
|
||||||
Cette destination est résolue depuis la racine de la crate dans `vite.config.ts` et correspond au `frontendDist` de `tauri.conf.json`.
|
Cette destination est résolue depuis la racine de la crate dans `vite.config.ts` et correspond au `frontendDist` de `tauri.conf.json`.
|
||||||
|
|||||||
@@ -10,4 +10,4 @@
|
|||||||
"core:default",
|
"core:default",
|
||||||
"tracing:default"
|
"tracing:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-config-desk/frontend/sass/_bootswatch.scss
|
// file: crates/ksp-app-config-desk/frontend/sass/_bootswatch.scss
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
// Pulse 5.3.8
|
// Pulse 5.3.8
|
||||||
// Bootswatch
|
// Bootswatch
|
||||||
@@ -157,4 +157,4 @@
|
|||||||
color: $list-group-disabled-color;
|
color: $list-group-disabled-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-config-desk/frontend/sass/_fontawesome.scss
|
// file: crates/ksp-app-config-desk/frontend/sass/_fontawesome.scss
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
//@use '@fortawesome/fontawesome-free/scss/variables' with (
|
//@use '@fortawesome/fontawesome-free/scss/variables' with (
|
||||||
// // customizing $font-path - make sure it points to where your webfonts are stored in your project
|
// // customizing $font-path - make sure it points to where your webfonts are stored in your project
|
||||||
@@ -16,4 +16,4 @@
|
|||||||
@use '@fortawesome/fontawesome-free/scss/fa' as fa;
|
@use '@fortawesome/fontawesome-free/scss/fa' as fa;
|
||||||
@use '@fortawesome/fontawesome-free/scss/brands' as fa-brands;
|
@use '@fortawesome/fontawesome-free/scss/brands' as fa-brands;
|
||||||
@use '@fortawesome/fontawesome-free/scss/regular' as fa-regular;
|
@use '@fortawesome/fontawesome-free/scss/regular' as fa-regular;
|
||||||
@use '@fortawesome/fontawesome-free/scss/solid' as fa-solid;
|
@use '@fortawesome/fontawesome-free/scss/solid' as fa-solid;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-config-desk/frontend/sass/_simplebar.scss
|
// file: crates/ksp-app-config-desk/frontend/sass/_simplebar.scss
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
/* Rtl support */
|
/* Rtl support */
|
||||||
[data-simplebar] {
|
[data-simplebar] {
|
||||||
@@ -245,4 +245,4 @@
|
|||||||
|
|
||||||
.simplebar-hover {
|
.simplebar-hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-app-config-desk/frontend/sass/_variables.scss
|
// file: crates/ksp-app-config-desk/frontend/sass/_variables.scss
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
// Pulse 5.3.8
|
// Pulse 5.3.8
|
||||||
// Bootswatch
|
// Bootswatch
|
||||||
@@ -92,4 +92,4 @@ $list-group-border-color: transparent !default;
|
|||||||
$list-group-hover-bg: lighten($list-group-bg, 10%) !default;
|
$list-group-hover-bg: lighten($list-group-bg, 10%) !default;
|
||||||
$list-group-active-color: $white !default;
|
$list-group-active-color: $white !default;
|
||||||
$list-group-active-bg: $list-group-bg !default;
|
$list-group-active-bg: $list-group-bg !default;
|
||||||
$list-group-disabled-color: lighten($list-group-bg, 30%) !default;
|
$list-group-disabled-color: lighten($list-group-bg, 30%) !default;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
"devUrl": "http://localhost:1430",
|
"devUrl": "http://localhost:1430",
|
||||||
"beforeBuildCommand": "npm run build",
|
"beforeBuildCommand": "npm run build",
|
||||||
"frontendDist": "../../builds/khadhroony-solana-project/ksp-app-config-desk/dist"
|
"frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-config-desk/dist"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
|
|||||||
@@ -28,4 +28,4 @@
|
|||||||
"frontend",
|
"frontend",
|
||||||
"vite.config.ts"
|
"vite.config.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs
|
// file: crates/ksp-app-config-desk/unit_tests/profiles.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn profile_inventory_exposes_logging_default_and_available_profiles() {
|
fn profile_inventory_exposes_registered_standard_profile_documents() {
|
||||||
let management = fixture_management();
|
let management = fixture_management();
|
||||||
assert!(management.is_ok(), "fixture management should construct: {management:?}");
|
assert!(management.is_ok(), "fixture management should construct: {management:?}");
|
||||||
if let std::result::Result::Ok(management) = management {
|
if let std::result::Result::Ok(management) = management {
|
||||||
let inventory = super::inventory_from_management(&management);
|
let inventory = super::inventory_from_management(&management);
|
||||||
assert!(inventory.is_ok(), "profile inventory should resolve: {inventory:?}");
|
assert!(inventory.is_ok(), "profile inventory should resolve: {inventory:?}");
|
||||||
if let std::result::Result::Ok(inventory) = inventory {
|
if let std::result::Result::Ok(inventory) = inventory {
|
||||||
assert_eq!(inventory.len(), 1);
|
assert!(inventory.iter().any(|document| -> bool {
|
||||||
assert_eq!(inventory[0].file_id, ksp_config_lib::FILE_ID_STD_LOGGING);
|
return document.file_id == ksp_config_lib::FILE_ID_STD_LOGGING;
|
||||||
assert!(!inventory[0].default_profile.is_empty());
|
|
||||||
assert!(inventory[0].profile_ids.iter().any(|profile_id| {
|
|
||||||
return profile_id == &inventory[0].default_profile;
|
|
||||||
}));
|
}));
|
||||||
|
assert!(inventory.iter().any(|document| -> bool {
|
||||||
|
return document.file_id == ksp_config_lib::FILE_ID_STD_TRANSPORT;
|
||||||
|
}));
|
||||||
|
for document in inventory {
|
||||||
|
assert!(!document.default_profile.is_empty());
|
||||||
|
assert!(document.profile_ids.iter().any(|profile_id| -> bool {
|
||||||
|
return profile_id == &document.default_profile;
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { defineConfig, normalizePath } from "vite";
|
|||||||
|
|
||||||
const appRoot = fileURLToPath(new URL(".", import.meta.url));
|
const appRoot = fileURLToPath(new URL(".", import.meta.url));
|
||||||
const frontendRoot = normalizePath(resolve(appRoot, "frontend"));
|
const frontendRoot = normalizePath(resolve(appRoot, "frontend"));
|
||||||
const frontendDist = normalizePath(resolve(appRoot, "../../builds/khadhroony-solana-project/ksp-app-config-desk/dist"));
|
const frontendDist = normalizePath(resolve(appRoot, "../../../builds/khadhroony-solana-project/ksp-app-config-desk/dist"));
|
||||||
const devHost = process.env.TAURI_DEV_HOST;
|
const devHost = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-config-lib/Cargo.toml
|
# file: crates/ksp-config-lib/Cargo.toml
|
||||||
# version: 3
|
# version: 6
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-config-lib"
|
name = "ksp-config-lib"
|
||||||
@@ -10,9 +10,13 @@ repository.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||||
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||||
serde.workspace = true
|
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
jsonschema.workspace = true
|
jsonschema.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-config-lib/README.md -->
|
<!-- file: crates/ksp-config-lib/README.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# ksp-config-lib
|
# ksp-config-lib
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
|
|||||||
- la classification `Public`, `Internal`, `Secret` ;
|
- la classification `Public`, `Internal`, `Secret` ;
|
||||||
- les représentations réelle et sûre/redacted ainsi que la provenance des valeurs résolues ;
|
- les représentations réelle et sûre/redacted ainsi que la provenance des valeurs résolues ;
|
||||||
- l'adapter du document Logging effectif vers `ksp_logging_lib::LoggingSettings` ;
|
- l'adapter du document Logging effectif vers `ksp_logging_lib::LoggingSettings` ;
|
||||||
|
- l'adapter du document HTTP Transport effectif vers `ksp_onchain_transport_lib::HttpTransportSettings`, y compris redaction/provenance des URLs `KSP_SECRET_*` ;
|
||||||
- la surface de management pour inspecter et réparer les sources Config enregistrées, modifier `std.logging.json`, consulter les rapports d'environnement, révéler explicitement une valeur réelle et modifier `.env` ;
|
- la surface de management pour inspecter et réparer les sources Config enregistrées, modifier `std.logging.json`, consulter les rapports d'environnement, révéler explicitement une valeur réelle et modifier `.env` ;
|
||||||
- les écritures atomiques JSON/`.env` et la protection des permissions `.env` ;
|
- les écritures atomiques JSON/`.env` et la protection des permissions `.env` ;
|
||||||
- les audits workspace empêchant les bypass d'ownership Config et les oublis dans `.env.example`.
|
- les audits workspace empêchant les bypass d'ownership Config et les oublis dans `.env.example`.
|
||||||
@@ -32,9 +33,11 @@ La crate centralise les documents JSON, leurs schemas, les profils et compositio
|
|||||||
Le registre par défaut connaît :
|
Le registre par défaut connaît :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
cfg.std.logging -> config/std.logging.json
|
cfg.std.logging -> config/std.logging.json
|
||||||
schema.std.logging -> config/schemas/std.logging.schema.json
|
cfg.std.transport -> config/std.transport.json
|
||||||
schema.composite -> config/schemas/composite.schema.json
|
schema.std.logging -> config/schemas/std.logging.schema.json
|
||||||
|
schema.std.transport -> config/schemas/std.transport.schema.json
|
||||||
|
schema.composite -> config/schemas/composite.schema.json
|
||||||
```
|
```
|
||||||
|
|
||||||
`ConfigFileRegistry::descriptors()` expose ces descripteurs en lecture seule et dans un ordre déterministe par `file_id`. Une application de management peut ainsi découvrir les fichiers connus sans maintenir une liste parallèle ni dépendre de leurs filenames physiques.
|
`ConfigFileRegistry::descriptors()` expose ces descripteurs en lecture seule et dans un ordre déterministe par `file_id`. Une application de management peut ainsi découvrir les fichiers connus sans maintenir une liste parallèle ni dépendre de leurs filenames physiques.
|
||||||
@@ -59,15 +62,15 @@ Les autres crates et applications KSP ne doivent pas :
|
|||||||
- parser ou écrire directement `.env` ;
|
- parser ou écrire directement `.env` ;
|
||||||
- ouvrir directement les documents Config connus par leur filename physique ;
|
- ouvrir directement les documents Config connus par leur filename physique ;
|
||||||
- réimplémenter la sélection de profils, les compositions ou les placeholders ;
|
- réimplémenter la sélection de profils, les compositions ou les placeholders ;
|
||||||
- reconstruire elles-mêmes la configuration Logging depuis le JSON.
|
- reconstruire elles-mêmes la configuration Logging ou Transport depuis le JSON.
|
||||||
|
|
||||||
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result` et de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`.
|
`ksp-config-lib` dépend de `ksp-core-lib` pour `Error`/`Result`, de `ksp-logging-lib` pour les événements Config utiles et le contrat `LoggingSettings`, et de `ksp-onchain-transport-lib` pour construire le contrat runtime Transport dans la direction Config -> Transport.
|
||||||
|
|
||||||
La dépendance inverse est interdite : `ksp-core-lib` et `ksp-logging-lib` ne dépendent pas de Config.
|
La dépendance inverse est interdite : `ksp-core-lib`, `ksp-logging-lib` et `ksp-onchain-transport-lib` ne dépendent pas de Config.
|
||||||
|
|
||||||
Config ne possède pas le `LoggingGuard`. L'application ou le service qui orchestre le runtime construit la configuration effective puis possède le lifecycle `ksp_logging_lib::initialize/reinitialize`.
|
Config ne possède pas le `LoggingGuard`. L'application ou le service qui orchestre le runtime construit la configuration effective puis possède le lifecycle `ksp_logging_lib::initialize/reinitialize`.
|
||||||
|
|
||||||
Tauri et les DTO TS-RS restent hors de cette crate. La future `ksp-app-config-desk` doit rester une frontière applicative mince au-dessus des APIs Config.
|
Tauri et les DTO TS-RS restent hors de cette crate. `ksp-app-config-desk` reste une frontière applicative mince au-dessus des APIs Config et découvre les documents standards via le registre Config sans déplacer leur logique métier dans l'application.
|
||||||
|
|
||||||
## Secrets
|
## Secrets
|
||||||
|
|
||||||
@@ -75,12 +78,13 @@ Un secret reste accessible au runtime ou au management lorsqu'un consumer autori
|
|||||||
|
|
||||||
Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées.
|
Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées.
|
||||||
|
|
||||||
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective.
|
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs endpoint : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [`USAGE.md`](USAGE.md) — construction du moteur, résolution runtime et management ;
|
- [`USAGE.md`](USAGE.md) — construction du moteur, résolution runtime et management ;
|
||||||
- [`TODO.md`](TODO.md) — points explicitement différés après `0.1.3` ;
|
- [`TODO.md`](TODO.md) — points explicitement différés après `0.1.3` ;
|
||||||
- [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ;
|
- [`../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md`](../../docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md) — plan historique détaillé de la fondation Config ;
|
||||||
- [`../../config/std.logging.json`](../../config/std.logging.json) — premier document standard concret ;
|
- [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ;
|
||||||
|
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard HTTP Transport ;
|
||||||
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.
|
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-config-lib/TODO.md -->
|
<!-- file: crates/ksp-config-lib/TODO.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# TODO ksp-config-lib
|
# TODO ksp-config-lib
|
||||||
|
|
||||||
@@ -31,11 +31,15 @@ La validation applicative desktop appartient à `ksp-app-config-desk` :
|
|||||||
|
|
||||||
Ces points ne nécessitent pas de duplication de logique dans `ksp-config-lib`; toute lacune réelle révélée par l'application ouvrira un delta Config explicite.
|
Ces points ne nécessitent pas de duplication de logique dans `ksp-config-lib`; toute lacune réelle révélée par l'application ouvrira un delta Config explicite.
|
||||||
|
|
||||||
|
## Extension `0.2.1` — Transport HTTP
|
||||||
|
|
||||||
|
`0.2.1-pre.006` introduit le premier nouveau domaine standard depuis Logging : `std.transport.json`, son schema, son exemple, son enregistrement et l'adapter Config -> `HttpTransportSettings`. Cette extension confirme que les nouveaux domaines restent ajoutés à la demande d'un consumer réel, sans transformer Config en propriétaire du runtime Transport.
|
||||||
|
|
||||||
## Futur, uniquement au besoin
|
## Futur, uniquement au besoin
|
||||||
|
|
||||||
Les capacités suivantes sont différées jusqu'à l'apparition de composants réels :
|
Les capacités suivantes sont différées jusqu'à l'apparition de composants réels :
|
||||||
|
|
||||||
- nouveaux documents `std.<domain>.json` et schemas associés ;
|
- documents `std.<domain>.json` supplémentaires et schemas associés ;
|
||||||
- descriptors `cfg.composite.<consumer>` pour de vrais consumers ;
|
- descriptors `cfg.composite.<consumer>` pour de vrais consumers ;
|
||||||
- contrats typés de management supplémentaires pour les nouveaux documents ;
|
- contrats typés de management supplémentaires pour les nouveaux documents ;
|
||||||
- watcher filesystem/reload automatique si une application ou un service démontre le besoin ;
|
- watcher filesystem/reload automatique si une application ou un service démontre le besoin ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Utilisation de ksp-config-lib
|
# Utilisation de ksp-config-lib
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ Les arguments compris par Config sont :
|
|||||||
--cfgpath=/path/to/config
|
--cfgpath=/path/to/config
|
||||||
--schemapath=/path/to/schemas
|
--schemapath=/path/to/schemas
|
||||||
--filemap=cfg.std.logging=my-logging.json
|
--filemap=cfg.std.logging=my-logging.json
|
||||||
|
--filemap=cfg.std.transport=my-transport.json
|
||||||
```
|
```
|
||||||
|
|
||||||
`cfgpath` et `schemapath` ne sont jamais lus depuis JSON, `.env` ou une variable KSP : cette règle évite un bootstrap récursif.
|
`cfgpath` et `schemapath` ne sont jamais lus depuis JSON, `.env` ou une variable KSP : cette règle évite un bootstrap récursif.
|
||||||
@@ -122,6 +123,23 @@ Un `logs_directory` relatif est ancré sur le current working directory du proce
|
|||||||
|
|
||||||
Les `files[].path` restent relatifs sous le root Logging, y compris après interpolation.
|
Les `files[].path` restent relatifs sous le root Logging, y compris après interpolation.
|
||||||
|
|
||||||
|
### 4.1 Construire le Transport HTTP depuis Config
|
||||||
|
|
||||||
|
Config possède également l'adapter du document `std.transport` vers le contrat runtime de `ksp-onchain-transport-lib` :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let transport = match engine.load_resolved_transport_config(std::option::Option::None, &environment) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
|
||||||
|
let transport_settings = transport.into_settings();
|
||||||
|
```
|
||||||
|
|
||||||
|
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. Les URLs peuvent provenir de `KSP_PUBLIC_*` ou de `KSP_SECRET_*`; dans ce dernier cas la valeur réelle reste disponible au runtime Transport, mais `ResolvedTransportConfig::effective().safe_value()` et les représentations `Debug` sont redacted.
|
||||||
|
|
||||||
|
La dépendance reste unidirectionnelle : Config connaît le contrat Transport pour le construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP.
|
||||||
|
|
||||||
## 5. Profils et composites
|
## 5. Profils et composites
|
||||||
|
|
||||||
Pour un document standard profilé :
|
Pour un document standard profilé :
|
||||||
@@ -213,6 +231,7 @@ Ce guide reste volontairement indépendant des numéros de release. Les contrats
|
|||||||
| Environnement | `ConfigEnvironment`, `ConfigEnvironmentSource`, `ConfigEnvironmentValue`, `DEFAULT_DOTENV_PATH`, `DEFAULT_DOTENV_EXAMPLE_PATH` | §3, §7–8 |
|
| Environnement | `ConfigEnvironment`, `ConfigEnvironmentSource`, `ConfigEnvironmentValue`, `DEFAULT_DOTENV_PATH`, `DEFAULT_DOTENV_EXAMPLE_PATH` | §3, §7–8 |
|
||||||
| Sensibilité/provenance | `ConfigSensitivity`, `ConfigValueProvenance`, `ResolvedConfigText`, `ResolvedConfigJson`, `REDACTED_CONFIG_VALUE` | §3 |
|
| Sensibilité/provenance | `ConfigSensitivity`, `ConfigValueProvenance`, `ResolvedConfigText`, `ResolvedConfigJson`, `REDACTED_CONFIG_VALUE` | §3 |
|
||||||
| Logging effectif | `ResolvedLoggingConfig` | §4 |
|
| Logging effectif | `ResolvedLoggingConfig` | §4 |
|
||||||
|
| Transport effectif | `ResolvedTransportConfig` | §4.1 |
|
||||||
| Management | `ConfigManagement`, `ConfigManagedSource`, `ConfigDocumentChangeReport`, `ConfigEnvironmentReport`, `ConfigEnvironmentChangeReport` | §6–7 |
|
| Management | `ConfigManagement`, `ConfigManagedSource`, `ConfigDocumentChangeReport`, `ConfigEnvironmentReport`, `ConfigEnvironmentChangeReport` | §6–7 |
|
||||||
| Source Logging typée | `LoggingConfigDocument`, `LoggingProfileConfig`, `LoggingConsoleConfig`, `LoggingFileConfig`, `LoggingOutputFilterConfig`, `LoggingTargetFilterConfig` | §6 et exemple ci-dessous |
|
| Source Logging typée | `LoggingConfigDocument`, `LoggingProfileConfig`, `LoggingConsoleConfig`, `LoggingFileConfig`, `LoggingOutputFilterConfig`, `LoggingTargetFilterConfig` | §6 et exemple ci-dessous |
|
||||||
| Erreurs Config | constantes `ERROR_CODE_*` réexportées par la crate | exemple ci-dessous |
|
| Erreurs Config | constantes `ERROR_CODE_*` réexportées par la crate | exemple ci-dessous |
|
||||||
@@ -324,4 +343,3 @@ if let std::result::Result::Err(error) = loaded {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Le message/context d'erreur reste destiné au diagnostic ; l'identité machine-readable passe par `ErrorCode`.
|
Le message/context d'erreur reste destiné au diagnostic ; l'identité machine-readable passe par `ErrorCode`.
|
||||||
|
|
||||||
|
|||||||
7
crates/ksp-config-lib/src/constants.rs
Normal file
7
crates/ksp-config-lib/src/constants.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// file: crates/ksp-config-lib/src/constants.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Config-owned tracing constants.
|
||||||
|
|
||||||
|
/// Owning tracing target for events emitted by the Config crate.
|
||||||
|
pub(crate) const TRACING_TARGET: &str = "ksp-config-lib";
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/src/environment.rs
|
// file: crates/ksp-config-lib/src/environment.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
/// Default local environment file read by Config from the process launch directory.
|
/// Default local environment file read by Config from the process launch directory.
|
||||||
pub const DEFAULT_DOTENV_PATH: &str = ".env";
|
pub const DEFAULT_DOTENV_PATH: &str = ".env";
|
||||||
@@ -7,7 +7,6 @@ pub const DEFAULT_DOTENV_PATH: &str = ".env";
|
|||||||
/// Versioned environment contract template expected at the repository/runtime root.
|
/// Versioned environment contract template expected at the repository/runtime root.
|
||||||
pub const DEFAULT_DOTENV_EXAMPLE_PATH: &str = ".env.example";
|
pub const DEFAULT_DOTENV_EXAMPLE_PATH: &str = ".env.example";
|
||||||
|
|
||||||
const LOGGING_TARGET: &str = "ksp-config-lib";
|
|
||||||
const LOGGING_DOMAIN: &str = "config.environment";
|
const LOGGING_DOMAIN: &str = "config.environment";
|
||||||
|
|
||||||
/// Source that supplied one resolved Config environment variable.
|
/// Source that supplied one resolved Config environment variable.
|
||||||
@@ -556,7 +555,7 @@ fn is_generic_dotenv_name(variable_name: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn emit_missing_variable_warning(variable_name: &str) {
|
fn emit_missing_variable_warning(variable_name: &str) {
|
||||||
ksp_logging_lib::warn!(target: LOGGING_TARGET, domain = LOGGING_DOMAIN, variable_name = variable_name, "Config environment variable is missing");
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, domain = LOGGING_DOMAIN, variable_name = variable_name, "Config environment variable is missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn missing_variable_error(variable_name: &str) -> ksp_core_lib::Error {
|
fn missing_variable_error(variable_name: &str) -> ksp_core_lib::Error {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/src/lib.rs
|
// file: crates/ksp-config-lib/src/lib.rs
|
||||||
// version: 10
|
// version: 12
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -8,11 +8,12 @@
|
|||||||
//!
|
//!
|
||||||
//! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and
|
//! The `0.1.3` surface owns bootstrap roots, the logical file registry, JSON/JSON Schema validation, standard-document profiles, generic composites and
|
||||||
//! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and
|
//! KSP/KSPB environment resolution through process + `.env` + fallback precedence. Resolved values preserve real/safe representations, sensitivity and
|
||||||
//! provenance. The standard Logging document maps explicitly to `ksp_logging_lib::LoggingSettings`, while the management surface provides typed Logging
|
//! provenance. Standard Logging and HTTP Transport documents map explicitly to their runtime settings contracts, while the management surface provides
|
||||||
//! mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence.
|
//! typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence.
|
||||||
|
|
||||||
mod bootstrap;
|
mod bootstrap;
|
||||||
mod composite;
|
mod composite;
|
||||||
|
mod constants;
|
||||||
mod document;
|
mod document;
|
||||||
mod environment;
|
mod environment;
|
||||||
mod error;
|
mod error;
|
||||||
@@ -22,6 +23,9 @@ mod persistence;
|
|||||||
mod profile;
|
mod profile;
|
||||||
mod registry;
|
mod registry;
|
||||||
mod sensitivity;
|
mod sensitivity;
|
||||||
|
mod transport;
|
||||||
|
|
||||||
|
pub(crate) use self::constants::TRACING_TARGET;
|
||||||
|
|
||||||
/// Bootstrap argument used to replace the configuration document root.
|
/// Bootstrap argument used to replace the configuration document root.
|
||||||
pub use self::bootstrap::ARG_CFG_PATH;
|
pub use self::bootstrap::ARG_CFG_PATH;
|
||||||
@@ -141,12 +145,20 @@ pub use self::registry::DEFAULT_COMPOSITE_SCHEMA_FILENAME;
|
|||||||
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
|
pub use self::registry::DEFAULT_STD_LOGGING_FILENAME;
|
||||||
/// Default physical filename for the standard Logging JSON Schema document.
|
/// Default physical filename for the standard Logging JSON Schema document.
|
||||||
pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
|
pub use self::registry::DEFAULT_STD_LOGGING_SCHEMA_FILENAME;
|
||||||
|
/// Default physical filename for the standard HTTP Transport configuration document.
|
||||||
|
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
|
||||||
|
/// Default physical filename for the standard HTTP Transport JSON Schema document.
|
||||||
|
pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME;
|
||||||
/// Logical file identifier for the generic composite JSON Schema document.
|
/// Logical file identifier for the generic composite JSON Schema document.
|
||||||
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
|
pub use self::registry::FILE_ID_SCHEMA_COMPOSITE;
|
||||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||||
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
|
pub use self::registry::FILE_ID_SCHEMA_STD_LOGGING;
|
||||||
|
/// Logical file identifier for the standard HTTP Transport JSON Schema document.
|
||||||
|
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
|
||||||
/// Logical file identifier for the standard Logging configuration document.
|
/// Logical file identifier for the standard Logging configuration document.
|
||||||
pub use self::registry::FILE_ID_STD_LOGGING;
|
pub use self::registry::FILE_ID_STD_LOGGING;
|
||||||
|
/// Logical file identifier for the standard HTTP Transport configuration document.
|
||||||
|
pub use self::registry::FILE_ID_STD_TRANSPORT;
|
||||||
/// Sensitivity assigned to one Config value after environment resolution.
|
/// Sensitivity assigned to one Config value after environment resolution.
|
||||||
pub use self::sensitivity::ConfigSensitivity;
|
pub use self::sensitivity::ConfigSensitivity;
|
||||||
/// Provenance segment participating in one resolved Config value.
|
/// Provenance segment participating in one resolved Config value.
|
||||||
@@ -157,3 +169,5 @@ pub use self::sensitivity::REDACTED_CONFIG_VALUE;
|
|||||||
pub use self::sensitivity::ResolvedConfigJson;
|
pub use self::sensitivity::ResolvedConfigJson;
|
||||||
/// One resolved Config string preserving real/safe representations and provenance.
|
/// One resolved Config string preserving real/safe representations and provenance.
|
||||||
pub use self::sensitivity::ResolvedConfigText;
|
pub use self::sensitivity::ResolvedConfigText;
|
||||||
|
/// Effective standard HTTP Transport configuration mapped to `ksp_onchain_transport_lib::HttpTransportSettings`.
|
||||||
|
pub use self::transport::ResolvedTransportConfig;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/src/persistence.rs
|
// file: crates/ksp-config-lib/src/persistence.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
static NEXT_TEMPORARY_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
static NEXT_TEMPORARY_FILE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ fn cleanup_temporary_file(path: &std::path::Path) {
|
|||||||
&& error.kind() != std::io::ErrorKind::NotFound
|
&& error.kind() != std::io::ErrorKind::NotFound
|
||||||
{
|
{
|
||||||
ksp_logging_lib::warn!(
|
ksp_logging_lib::warn!(
|
||||||
target: "ksp-config-lib",
|
target: crate::TRACING_TARGET,
|
||||||
domain = "config.persistence",
|
domain = "config.persistence",
|
||||||
path = %path.to_string_lossy(),
|
path = %path.to_string_lossy(),
|
||||||
error = %error,
|
error = %error,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/src/registry.rs
|
// file: crates/ksp-config-lib/src/registry.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
/// Bootstrap argument used to replace a known Config filename mapping.
|
/// Bootstrap argument used to replace a known Config filename mapping.
|
||||||
pub const ARG_FILE_MAP: &str = "--filemap";
|
pub const ARG_FILE_MAP: &str = "--filemap";
|
||||||
@@ -7,12 +7,20 @@ pub const ARG_FILE_MAP: &str = "--filemap";
|
|||||||
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
|
pub const FILE_ID_STD_LOGGING: &str = "cfg.std.logging";
|
||||||
/// Logical file identifier for the standard Logging JSON Schema document.
|
/// Logical file identifier for the standard Logging JSON Schema document.
|
||||||
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
|
pub const FILE_ID_SCHEMA_STD_LOGGING: &str = "schema.std.logging";
|
||||||
|
/// Logical file identifier for the standard 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.
|
/// Logical file identifier for the generic composite JSON Schema document.
|
||||||
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
|
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
|
||||||
/// Default physical filename for the standard Logging configuration document.
|
/// Default physical filename for the standard Logging configuration document.
|
||||||
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
|
pub const DEFAULT_STD_LOGGING_FILENAME: &str = "std.logging.json";
|
||||||
/// Default physical filename for the standard Logging JSON Schema document.
|
/// Default physical filename for the standard Logging JSON Schema document.
|
||||||
pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
|
pub const DEFAULT_STD_LOGGING_SCHEMA_FILENAME: &str = "std.logging.schema.json";
|
||||||
|
/// Default physical filename for the standard HTTP Transport configuration document.
|
||||||
|
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.
|
/// Default physical filename for the generic composite JSON Schema document.
|
||||||
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
|
pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
|
||||||
|
|
||||||
@@ -135,13 +143,29 @@ impl ConfigFileRegistry {
|
|||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
|
let transport = ConfigFileDescriptor::new(
|
||||||
|
FILE_ID_STD_TRANSPORT,
|
||||||
|
ConfigFileKind::Config,
|
||||||
|
DEFAULT_STD_TRANSPORT_FILENAME,
|
||||||
|
std::option::Option::Some(FILE_ID_SCHEMA_STD_TRANSPORT),
|
||||||
|
);
|
||||||
|
let transport = match transport {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let transport_schema =
|
||||||
|
ConfigFileDescriptor::new(FILE_ID_SCHEMA_STD_TRANSPORT, ConfigFileKind::Schema, DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME, std::option::Option::None);
|
||||||
|
let transport_schema = match transport_schema {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
let composite_schema =
|
let composite_schema =
|
||||||
ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None);
|
ConfigFileDescriptor::new(FILE_ID_SCHEMA_COMPOSITE, ConfigFileKind::Schema, DEFAULT_COMPOSITE_SCHEMA_FILENAME, std::option::Option::None);
|
||||||
let composite_schema = match composite_schema {
|
let composite_schema = match composite_schema {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
return build_registry([logging, logging_schema, composite_schema]);
|
return 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.
|
/// Creates the default registry and applies repeatable `--filemap=<file_id>=<filename>` overrides from raw process arguments.
|
||||||
|
|||||||
316
crates/ksp-config-lib/src/transport.rs
Normal file
316
crates/ksp-config-lib/src/transport.rs
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
// file: crates/ksp-config-lib/src/transport.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
/// Effective standard HTTP Transport configuration resolved from Config and mapped to the Transport runtime contract.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct ResolvedTransportConfig {
|
||||||
|
file_id: crate::ConfigFileId,
|
||||||
|
source_path: std::path::PathBuf,
|
||||||
|
profile_id: String,
|
||||||
|
selection_source: crate::ConfigProfileSelectionSource,
|
||||||
|
effective: crate::ResolvedConfigJson,
|
||||||
|
settings: ksp_onchain_transport_lib::HttpTransportSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvedTransportConfig {
|
||||||
|
/// Returns the logical Config document identifier used by this runtime configuration.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn file_id(&self) -> &crate::ConfigFileId {
|
||||||
|
return &self.file_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the physical source Config document path.
|
||||||
|
#[must_use]
|
||||||
|
pub fn source_path(&self) -> &std::path::Path {
|
||||||
|
return self.source_path.as_path();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the selected standard Transport profile identifier.
|
||||||
|
#[must_use]
|
||||||
|
pub fn profile_id(&self) -> &str {
|
||||||
|
return self.profile_id.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the source that selected the standard Transport profile.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource {
|
||||||
|
return self.selection_source;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the detailed environment-resolved effective Config view.
|
||||||
|
///
|
||||||
|
/// The real tree is available to legitimate runtime consumers. The safe tree redacts values originating from `KSP_SECRET_*` or `KSPB_SECRET_*`
|
||||||
|
/// placeholders and is the only representation used by this type's [`std::fmt::Debug`] implementation.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn effective(&self) -> &crate::ResolvedConfigJson {
|
||||||
|
return &self.effective;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the validated runtime HTTP Transport settings.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn settings(&self) -> &ksp_onchain_transport_lib::HttpTransportSettings {
|
||||||
|
return &self.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes this resolved Config and returns the mapped runtime HTTP Transport settings.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_settings(self) -> ksp_onchain_transport_lib::HttpTransportSettings {
|
||||||
|
return self.settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for ResolvedTransportConfig {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("ResolvedTransportConfig")
|
||||||
|
.field("file_id", &self.file_id)
|
||||||
|
.field("source_path", &self.source_path)
|
||||||
|
.field("profile_id", &self.profile_id)
|
||||||
|
.field("selection_source", &self.selection_source)
|
||||||
|
.field("effective", &self.effective)
|
||||||
|
.finish_non_exhaustive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::ConfigDocumentEngine {
|
||||||
|
/// Loads the standard HTTP Transport document, selects a profile, resolves environment placeholders and maps it to Transport runtime settings.
|
||||||
|
///
|
||||||
|
/// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. Secret endpoint URLs are allowed
|
||||||
|
/// because the Transport URL wrapper owns runtime redaction. Invalid environment-resolved values are reported as
|
||||||
|
/// [`crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID`] without copying endpoint URL values into ordinary error context.
|
||||||
|
pub fn load_resolved_transport_config(
|
||||||
|
&self,
|
||||||
|
requested_profile: std::option::Option<&str>,
|
||||||
|
environment: &crate::ConfigEnvironment,
|
||||||
|
) -> ksp_core_lib::Result<ResolvedTransportConfig> {
|
||||||
|
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_TRANSPORT);
|
||||||
|
let file_id = match file_id {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let profile = self.load_resolved_profile(&file_id, requested_profile);
|
||||||
|
let profile = match profile {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return resolve_transport_profile(&profile, environment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveTransportSource {
|
||||||
|
format_version: u32,
|
||||||
|
profile_id: String,
|
||||||
|
retry: EffectiveRetrySource,
|
||||||
|
endpoints: std::vec::Vec<EffectiveEndpointSource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveRetrySource {
|
||||||
|
max_retries: u32,
|
||||||
|
initial_backoff_ms: u64,
|
||||||
|
max_backoff_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveEndpointSource {
|
||||||
|
name: String,
|
||||||
|
enabled: bool,
|
||||||
|
provider: String,
|
||||||
|
cluster: String,
|
||||||
|
url: String,
|
||||||
|
connect_timeout_ms: u64,
|
||||||
|
request_timeout_ms: u64,
|
||||||
|
max_idle_connections_per_host: std::option::Option<usize>,
|
||||||
|
roles: std::vec::Vec<EffectiveRoleSource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveRoleSource {
|
||||||
|
role: String,
|
||||||
|
enabled: bool,
|
||||||
|
request_kinds: std::vec::Vec<String>,
|
||||||
|
priority: u32,
|
||||||
|
limits: EffectiveLimitsSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct EffectiveLimitsSource {
|
||||||
|
requests_per_second: std::option::Option<u32>,
|
||||||
|
burst_capacity: std::option::Option<u32>,
|
||||||
|
max_concurrent_requests: std::option::Option<u32>,
|
||||||
|
pause_after_rate_limit_ms: std::option::Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedTransportConfig> {
|
||||||
|
let effective = profile.resolve_effective_environment_detailed(environment);
|
||||||
|
let effective = match effective {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let source = serde_json::from_value::<EffectiveTransportSource>(effective.value().clone());
|
||||||
|
let source = match source {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
effective_error(profile, "effective Transport Config cannot be decoded into the runtime adapter contract").with_source(error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if source.format_version != 1 {
|
||||||
|
return std::result::Result::Err(effective_error(profile, "effective Transport format_version is unsupported"));
|
||||||
|
}
|
||||||
|
if source.profile_id != profile.profile_id() {
|
||||||
|
return std::result::Result::Err(effective_error(profile, "effective Transport profile_id does not match the selected profile"));
|
||||||
|
}
|
||||||
|
let retry = ksp_onchain_transport_lib::HttpRetrySettings::new(
|
||||||
|
source.retry.max_retries,
|
||||||
|
std::time::Duration::from_millis(source.retry.initial_backoff_ms),
|
||||||
|
std::time::Duration::from_millis(source.retry.max_backoff_ms),
|
||||||
|
);
|
||||||
|
let endpoints = map_endpoints(source.endpoints, profile);
|
||||||
|
let endpoints = match endpoints {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(endpoints, retry);
|
||||||
|
let validation = settings.validate();
|
||||||
|
if let std::result::Result::Err(error) = validation {
|
||||||
|
return std::result::Result::Err(transport_contract_error(profile, "effective Transport settings fail the Transport runtime contract", &error));
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
profile_id = profile.profile_id(),
|
||||||
|
endpoint_count = settings.endpoints().len(),
|
||||||
|
"mapped standard Transport Config to runtime settings"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(ResolvedTransportConfig {
|
||||||
|
file_id: profile.file_id().clone(),
|
||||||
|
source_path: profile.path().to_path_buf(),
|
||||||
|
profile_id: profile.profile_id().to_owned(),
|
||||||
|
selection_source: profile.selection_source(),
|
||||||
|
effective,
|
||||||
|
settings,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_endpoints(
|
||||||
|
sources: std::vec::Vec<EffectiveEndpointSource>,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::HttpEndpointSettings>> {
|
||||||
|
let mut endpoints = std::vec::Vec::<ksp_onchain_transport_lib::HttpEndpointSettings>::with_capacity(sources.len());
|
||||||
|
for source in sources {
|
||||||
|
let endpoint_name = source.name.clone();
|
||||||
|
let url = ksp_onchain_transport_lib::HttpEndpointUrl::parse(source.url);
|
||||||
|
let url = match url {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
transport_contract_error(profile, "effective endpoint URL is invalid", &error).with_context("endpoint_name", endpoint_name),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let roles = map_roles(source.roles, profile, endpoint_name.as_str());
|
||||||
|
let roles = match roles {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
endpoints.push(ksp_onchain_transport_lib::HttpEndpointSettings::new(
|
||||||
|
source.name,
|
||||||
|
source.enabled,
|
||||||
|
ksp_onchain_transport_lib::HttpProviderName::new(source.provider),
|
||||||
|
ksp_onchain_transport_lib::HttpClusterName::new(source.cluster),
|
||||||
|
url,
|
||||||
|
std::time::Duration::from_millis(source.connect_timeout_ms),
|
||||||
|
std::time::Duration::from_millis(source.request_timeout_ms),
|
||||||
|
source.max_idle_connections_per_host,
|
||||||
|
roles,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(endpoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_roles(
|
||||||
|
sources: std::vec::Vec<EffectiveRoleSource>,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
endpoint_name: &str,
|
||||||
|
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::HttpEndpointRoleSettings>> {
|
||||||
|
let mut roles = std::vec::Vec::<ksp_onchain_transport_lib::HttpEndpointRoleSettings>::with_capacity(sources.len());
|
||||||
|
for source in sources {
|
||||||
|
let requests_per_second = map_non_zero(source.limits.requests_per_second, profile, "limits.requests_per_second", endpoint_name);
|
||||||
|
let requests_per_second = match requests_per_second {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let burst_capacity = map_non_zero(source.limits.burst_capacity, profile, "limits.burst_capacity", endpoint_name);
|
||||||
|
let burst_capacity = match burst_capacity {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let max_concurrent_requests = map_non_zero(source.limits.max_concurrent_requests, profile, "limits.max_concurrent_requests", endpoint_name);
|
||||||
|
let max_concurrent_requests = match max_concurrent_requests {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let pause_after_rate_limit = match source.limits.pause_after_rate_limit_ms {
|
||||||
|
std::option::Option::Some(value) => std::option::Option::Some(std::time::Duration::from_millis(value)),
|
||||||
|
std::option::Option::None => std::option::Option::None,
|
||||||
|
};
|
||||||
|
let limits = ksp_onchain_transport_lib::HttpRoleLimits::new(requests_per_second, burst_capacity, max_concurrent_requests, pause_after_rate_limit);
|
||||||
|
let mut request_kinds = std::vec::Vec::<ksp_onchain_transport_lib::HttpRequestKind>::with_capacity(source.request_kinds.len());
|
||||||
|
for request_kind in source.request_kinds {
|
||||||
|
request_kinds.push(ksp_onchain_transport_lib::HttpRequestKind::new(request_kind));
|
||||||
|
}
|
||||||
|
roles.push(ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
|
||||||
|
ksp_onchain_transport_lib::HttpRoleName::new(source.role),
|
||||||
|
source.enabled,
|
||||||
|
request_kinds,
|
||||||
|
source.priority,
|
||||||
|
limits,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_non_zero(
|
||||||
|
value: std::option::Option<u32>,
|
||||||
|
profile: &crate::ResolvedConfigProfile,
|
||||||
|
field: &'static str,
|
||||||
|
endpoint_name: &str,
|
||||||
|
) -> ksp_core_lib::Result<std::option::Option<std::num::NonZeroU32>> {
|
||||||
|
let value = match value {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
|
||||||
|
};
|
||||||
|
let non_zero = std::num::NonZeroU32::new(value);
|
||||||
|
return match non_zero {
|
||||||
|
std::option::Option::Some(value) => std::result::Result::Ok(std::option::Option::Some(value)),
|
||||||
|
std::option::Option::None => std::result::Result::Err(
|
||||||
|
effective_error(profile, "effective Transport role limit must be greater than zero")
|
||||||
|
.with_context("field", field)
|
||||||
|
.with_context("endpoint_name", endpoint_name),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transport_contract_error(profile: &crate::ResolvedConfigProfile, reason: &'static str, transport_error: &ksp_core_lib::Error) -> ksp_core_lib::Error {
|
||||||
|
return effective_error(profile, reason)
|
||||||
|
.with_context("transport_error_domain", transport_error.code().domain())
|
||||||
|
.with_context("transport_error_code", transport_error.code().code());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error {
|
||||||
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract")
|
||||||
|
.with_context("file_id", profile.file_id().as_str())
|
||||||
|
.with_context("profile_id", profile.profile_id())
|
||||||
|
.with_context("reason", reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/transport.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/tests/ownership.rs
|
// file: crates/ksp-config-lib/tests/ownership.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
//! Workspace ownership audits for KSP application configuration boundaries.
|
//! Workspace ownership audits for KSP application configuration boundaries.
|
||||||
|
|
||||||
@@ -239,7 +239,14 @@ fn workspace_crates_do_not_hardcode_config_managed_physical_files() {
|
|||||||
std::result::Result::Ok(value) => non_comment_source(value.as_str()),
|
std::result::Result::Ok(value) => non_comment_source(value.as_str()),
|
||||||
std::result::Result::Err(_) => continue,
|
std::result::Result::Err(_) => continue,
|
||||||
};
|
};
|
||||||
for token in ["\".env\"", "\"std.logging.json\"", "\"std.logging.schema.json\"", "\"composite.schema.json\""] {
|
for token in [
|
||||||
|
"\".env\"",
|
||||||
|
"\"std.logging.json\"",
|
||||||
|
"\"std.logging.schema.json\"",
|
||||||
|
"\"std.transport.json\"",
|
||||||
|
"\"std.transport.schema.json\"",
|
||||||
|
"\"composite.schema.json\"",
|
||||||
|
] {
|
||||||
assert!(!source.contains(token), "{} hardcodes Config-managed physical resource {token}; use ksp-config-lib contracts", rust_file.display());
|
assert!(!source.contains(token), "{} hardcodes Config-managed physical resource {token}; use ksp-config-lib contracts", rust_file.display());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// file: crates/ksp-config-lib/tests/public_api.rs
|
// file: crates/ksp-config-lib/tests/public_api.rs
|
||||||
// version: 16
|
// version: 17
|
||||||
|
|
||||||
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity, Logging-adapter and
|
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
|
||||||
//! management contracts.
|
//! Logging/Transport adapters and management contracts.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bootstrap_contract_is_available_from_crate_root() {
|
fn bootstrap_contract_is_available_from_crate_root() {
|
||||||
@@ -80,15 +80,18 @@ fn registry_descriptor_inventory_is_available_from_crate_root() {
|
|||||||
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
|
assert!(registry.is_ok(), "public registry should remain constructible: {registry:?}");
|
||||||
if let std::result::Result::Ok(registry) = registry {
|
if let std::result::Result::Ok(registry) = registry {
|
||||||
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
|
let descriptors: std::vec::Vec<&ksp_config_lib::ConfigFileDescriptor> = registry.descriptors().collect();
|
||||||
assert_eq!(descriptors.len(), 3);
|
assert_eq!(descriptors.len(), 5);
|
||||||
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
|
assert_eq!(descriptors[0].file_id().as_str(), ksp_config_lib::FILE_ID_STD_LOGGING);
|
||||||
assert_eq!(descriptors[0].kind(), ksp_config_lib::ConfigFileKind::Config);
|
assert_eq!(descriptors[0].kind(), ksp_config_lib::ConfigFileKind::Config);
|
||||||
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
|
assert_eq!(descriptors[1].file_id().as_str(), ksp_config_lib::FILE_ID_STD_TRANSPORT);
|
||||||
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
|
assert_eq!(descriptors[1].kind(), ksp_config_lib::ConfigFileKind::Config);
|
||||||
let schema_file_id = descriptors[0].schema_file_id();
|
assert_eq!(descriptors[2].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_COMPOSITE);
|
||||||
assert!(schema_file_id.is_some(), "public descriptor inventory should preserve schema association");
|
assert_eq!(descriptors[3].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
|
||||||
|
assert_eq!(descriptors[4].file_id().as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||||
|
let schema_file_id = descriptors[1].schema_file_id();
|
||||||
|
assert!(schema_file_id.is_some(), "public Transport descriptor should preserve schema association");
|
||||||
if let std::option::Option::Some(schema_file_id) = schema_file_id {
|
if let std::option::Option::Some(schema_file_id) = schema_file_id {
|
||||||
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_LOGGING);
|
assert_eq!(schema_file_id.as_str(), ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,3 +244,13 @@ fn management_contracts_are_available_from_crate_root() {
|
|||||||
let _ = (save_source_candidate, reveal_effective, reveal_dotenv);
|
let _ = (save_source_candidate, reveal_effective, reveal_dotenv);
|
||||||
assert_ne!(ksp_config_lib::ERROR_CODE_MANAGEMENT_OPERATION_INVALID, ksp_config_lib::ERROR_CODE_PERSISTENCE_WRITE_FAILED);
|
assert_ne!(ksp_config_lib::ERROR_CODE_MANAGEMENT_OPERATION_INVALID, ksp_config_lib::ERROR_CODE_PERSISTENCE_WRITE_FAILED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transport_adapter_contract_is_available_from_crate_root() {
|
||||||
|
let _loader = ksp_config_lib::ConfigDocumentEngine::load_resolved_transport_config;
|
||||||
|
assert!(std::mem::size_of::<ksp_config_lib::ResolvedTransportConfig>() > 0);
|
||||||
|
assert_eq!(ksp_config_lib::FILE_ID_STD_TRANSPORT, "cfg.std.transport");
|
||||||
|
assert_eq!(ksp_config_lib::FILE_ID_SCHEMA_STD_TRANSPORT, "schema.std.transport");
|
||||||
|
assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_FILENAME, "std.transport.json");
|
||||||
|
assert_eq!(ksp_config_lib::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME, "std.transport.schema.json");
|
||||||
|
}
|
||||||
|
|||||||
32
crates/ksp-config-lib/tests/transport_devnet_smoke.rs
Normal file
32
crates/ksp-config-lib/tests/transport_devnet_smoke.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// file: crates/ksp-config-lib/tests/transport_devnet_smoke.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Opt-in live Devnet smoke for the Config -> Transport foundation path.
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
#[ignore = "opt-in live Solana Devnet smoke; performs external network requests"]
|
||||||
|
async fn committed_devnet_transport_profile_reaches_all_foundation_canaries() {
|
||||||
|
let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||||
|
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"))
|
||||||
|
.expect("committed Config roots must be valid");
|
||||||
|
let registry = ksp_config_lib::ConfigFileRegistry::defaults().expect("default Config registry must be valid");
|
||||||
|
let engine = ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry);
|
||||||
|
let environment = ksp_config_lib::ConfigEnvironment::load().expect("Config must capture the opt-in smoke environment");
|
||||||
|
let resolved = engine
|
||||||
|
.load_resolved_transport_config(std::option::Option::Some("devnet_public"), &environment)
|
||||||
|
.expect("committed devnet_public Transport profile must resolve");
|
||||||
|
assert_eq!(resolved.profile_id(), "devnet_public");
|
||||||
|
let pool = ksp_onchain_transport_lib::HttpTransportPool::new(resolved.into_settings()).expect("resolved Transport settings must construct the HTTP pool");
|
||||||
|
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
|
||||||
|
let health = pool.get_health(&role).await.expect("Devnet getHealth smoke must succeed");
|
||||||
|
assert_eq!(health, ksp_onchain_transport_lib::SolanaNodeHealth::Healthy);
|
||||||
|
let genesis_hash = pool.get_genesis_hash(&role).await.expect("Devnet getGenesisHash smoke must succeed");
|
||||||
|
assert!(!genesis_hash.as_str().is_empty());
|
||||||
|
let version = pool.get_version(&role).await.expect("Devnet getVersion smoke must succeed");
|
||||||
|
assert!(!version.solana_core().is_empty());
|
||||||
|
let balance = pool
|
||||||
|
.get_balance(&role, &ksp_core_lib::PRGIDPK_SOLANA_SYSTEM, std::option::Option::None)
|
||||||
|
.await
|
||||||
|
.expect("Devnet getBalance smoke must succeed");
|
||||||
|
assert!(balance.context().slot() > 0);
|
||||||
|
}
|
||||||
@@ -22,4 +22,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
40
crates/ksp-config-lib/unit_tests/fixtures/std.transport.json
Normal file
40
crates/ksp-config-lib/unit_tests/fixtures/std.transport.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"retry": {
|
||||||
|
"max_retries": 4,
|
||||||
|
"initial_backoff_ms": 125,
|
||||||
|
"max_backoff_ms": 2500
|
||||||
|
},
|
||||||
|
"default_profile": "secret_test",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"profile_id": "secret_test",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "fixture_private",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "fixture-provider",
|
||||||
|
"cluster": "fixture-cluster",
|
||||||
|
"url": "${KSP_SECRET_TRANSPORT_TEST_URL:-https://fallback.invalid}",
|
||||||
|
"connect_timeout_ms": 750,
|
||||||
|
"request_timeout_ms": 2500,
|
||||||
|
"max_idle_connections_per_host": 3,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "default",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": ["*"],
|
||||||
|
"priority": 7,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 9,
|
||||||
|
"burst_capacity": 12,
|
||||||
|
"max_concurrent_requests": 4,
|
||||||
|
"pause_after_rate_limit_ms": 650
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/unit_tests/registry.rs
|
// file: crates/ksp-config-lib/unit_tests/registry.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
||||||
@@ -7,19 +7,29 @@ fn descriptors_expose_complete_registry_in_deterministic_file_id_order() {
|
|||||||
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
assert!(registry.is_ok(), "default registry should be valid: {registry:?}");
|
||||||
if let std::result::Result::Ok(registry) = registry {
|
if let std::result::Result::Ok(registry) = registry {
|
||||||
let descriptors: std::vec::Vec<&super::ConfigFileDescriptor> = registry.descriptors().collect();
|
let descriptors: std::vec::Vec<&super::ConfigFileDescriptor> = registry.descriptors().collect();
|
||||||
assert_eq!(descriptors.len(), 3);
|
assert_eq!(descriptors.len(), 5);
|
||||||
assert_eq!(descriptors[0].file_id().as_str(), super::FILE_ID_STD_LOGGING);
|
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].kind(), super::ConfigFileKind::Config);
|
||||||
assert_eq!(descriptors[0].filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_FILENAME));
|
assert_eq!(descriptors[0].filename(), std::path::Path::new(super::DEFAULT_STD_LOGGING_FILENAME));
|
||||||
let schema_file_id = descriptors[0].schema_file_id();
|
let logging_schema_file_id = descriptors[0].schema_file_id();
|
||||||
assert!(schema_file_id.is_some(), "logging descriptor should expose its validation schema");
|
assert!(logging_schema_file_id.is_some(), "logging descriptor should expose its validation schema");
|
||||||
if let std::option::Option::Some(schema_file_id) = schema_file_id {
|
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(), super::FILE_ID_SCHEMA_STD_LOGGING);
|
||||||
}
|
}
|
||||||
assert_eq!(descriptors[1].file_id().as_str(), super::FILE_ID_SCHEMA_COMPOSITE);
|
assert_eq!(descriptors[1].file_id().as_str(), super::FILE_ID_STD_TRANSPORT);
|
||||||
assert_eq!(descriptors[1].kind(), super::ConfigFileKind::Schema);
|
assert_eq!(descriptors[1].kind(), super::ConfigFileKind::Config);
|
||||||
assert_eq!(descriptors[2].file_id().as_str(), super::FILE_ID_SCHEMA_STD_LOGGING);
|
assert_eq!(descriptors[1].filename(), std::path::Path::new(super::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!(descriptors[2].file_id().as_str(), super::FILE_ID_SCHEMA_COMPOSITE);
|
||||||
assert_eq!(descriptors[2].kind(), super::ConfigFileKind::Schema);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +93,31 @@ 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();
|
||||||
|
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);
|
||||||
|
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) {
|
||||||
|
let transport = registry.descriptor(&transport_id);
|
||||||
|
let schema = registry.descriptor(&schema_id);
|
||||||
|
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.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_path_uses_descriptor_kind_to_select_bootstrap_root() {
|
fn resolve_path_uses_descriptor_kind_to_select_bootstrap_root() {
|
||||||
let registry = super::ConfigFileRegistry::defaults();
|
let registry = super::ConfigFileRegistry::defaults();
|
||||||
|
|||||||
202
crates/ksp-config-lib/unit_tests/transport.rs
Normal file
202
crates/ksp-config-lib/unit_tests/transport.rs
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
// file: crates/ksp-config-lib/unit_tests/transport.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fixture_transport_profile_maps_complete_runtime_contract() {
|
||||||
|
let engine = fixture_engine();
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||||
|
let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment);
|
||||||
|
assert!(resolved.is_ok(), "fixture Transport Config should map: {resolved:?}");
|
||||||
|
let resolved = match resolved {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(resolved.file_id().as_str(), crate::FILE_ID_STD_TRANSPORT);
|
||||||
|
assert_eq!(resolved.profile_id(), "secret_test");
|
||||||
|
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::DefaultProfile);
|
||||||
|
assert_eq!(resolved.settings().retry().max_retries(), 4);
|
||||||
|
assert_eq!(resolved.settings().retry().initial_backoff(), std::time::Duration::from_millis(125));
|
||||||
|
assert_eq!(resolved.settings().retry().max_backoff(), std::time::Duration::from_millis(2500));
|
||||||
|
assert_eq!(resolved.settings().endpoints().len(), 1);
|
||||||
|
let endpoint = &resolved.settings().endpoints()[0];
|
||||||
|
assert_eq!(endpoint.name(), "fixture_private");
|
||||||
|
assert_eq!(endpoint.provider().as_str(), "fixture-provider");
|
||||||
|
assert_eq!(endpoint.cluster().as_str(), "fixture-cluster");
|
||||||
|
assert_eq!(endpoint.url().as_str(), "https://fallback.invalid");
|
||||||
|
assert_eq!(endpoint.connect_timeout(), std::time::Duration::from_millis(750));
|
||||||
|
assert_eq!(endpoint.request_timeout(), std::time::Duration::from_millis(2500));
|
||||||
|
assert_eq!(endpoint.max_idle_connections_per_host(), std::option::Option::Some(3));
|
||||||
|
assert_eq!(endpoint.roles().len(), 1);
|
||||||
|
let role = &endpoint.roles()[0];
|
||||||
|
assert_eq!(role.role().as_str(), "default");
|
||||||
|
assert!(role.enabled());
|
||||||
|
assert_eq!(role.priority(), 7);
|
||||||
|
assert_eq!(role.request_kinds().len(), 1);
|
||||||
|
assert!(role.request_kinds()[0].is_wildcard());
|
||||||
|
assert_eq!(role.limits().requests_per_second().map(std::num::NonZeroU32::get), std::option::Option::Some(9));
|
||||||
|
assert_eq!(role.limits().burst_capacity().map(std::num::NonZeroU32::get), std::option::Option::Some(12));
|
||||||
|
assert_eq!(role.limits().max_concurrent_requests().map(std::num::NonZeroU32::get), std::option::Option::Some(4));
|
||||||
|
assert_eq!(role.limits().pause_after_rate_limit(), std::option::Option::Some(std::time::Duration::from_millis(650)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn committed_transport_document_maps_default_and_explicit_profiles() {
|
||||||
|
let engine = committed_engine();
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
|
||||||
|
let default = engine.load_resolved_transport_config(std::option::Option::None, &environment);
|
||||||
|
let mainnet = engine.load_resolved_transport_config(std::option::Option::Some("mainnet_public"), &environment);
|
||||||
|
assert!(default.is_ok(), "committed default Transport profile should map: {default:?}");
|
||||||
|
assert!(mainnet.is_ok(), "committed explicit Transport profile should map: {mainnet:?}");
|
||||||
|
if let std::result::Result::Ok(default) = default {
|
||||||
|
assert_eq!(default.profile_id(), "devnet_public");
|
||||||
|
assert_eq!(default.settings().endpoints()[0].cluster().as_str(), "devnet");
|
||||||
|
assert_eq!(default.settings().endpoints()[0].url().as_str(), "https://api.devnet.solana.com");
|
||||||
|
}
|
||||||
|
if let std::result::Result::Ok(mainnet) = mainnet {
|
||||||
|
assert_eq!(mainnet.profile_id(), "mainnet_public");
|
||||||
|
assert_eq!(mainnet.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
|
||||||
|
assert_eq!(mainnet.settings().endpoints()[0].cluster().as_str(), "mainnet-beta");
|
||||||
|
assert_eq!(mainnet.settings().endpoints()[0].url().as_str(), "https://api.mainnet-beta.solana.com");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transport_profile_preserves_global_and_profile_origin() {
|
||||||
|
let engine = committed_engine();
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_TRANSPORT);
|
||||||
|
let file_id = match file_id {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let profile = engine.load_resolved_profile(&file_id, std::option::Option::None);
|
||||||
|
assert!(profile.is_ok(), "committed Transport profile should resolve: {profile:?}");
|
||||||
|
if let std::result::Result::Ok(profile) = profile {
|
||||||
|
assert_eq!(profile.origin("retry"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
||||||
|
assert_eq!(profile.origin("endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
|
||||||
|
assert_eq!(profile.origin("format_version"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn secret_transport_url_is_runtime_available_but_safe_projection_is_redacted() {
|
||||||
|
let engine = fixture_engine();
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let canary = "https://secret-provider.invalid/?api-key=transport-secret-canary";
|
||||||
|
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||||
|
process.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), canary.to_owned());
|
||||||
|
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||||
|
let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment);
|
||||||
|
assert!(resolved.is_ok(), "secret Transport endpoint should map without being rejected: {resolved:?}");
|
||||||
|
let resolved = match resolved {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(resolved.effective().sensitivity(), crate::ConfigSensitivity::Secret);
|
||||||
|
assert_eq!(resolved.settings().endpoints()[0].url().as_str(), canary);
|
||||||
|
let safe_url = resolved.effective().safe_value().pointer("/endpoints/0/url").and_then(serde_json::Value::as_str);
|
||||||
|
assert_eq!(safe_url, std::option::Option::Some(crate::REDACTED_CONFIG_VALUE));
|
||||||
|
let debug = format!("{resolved:?}");
|
||||||
|
assert!(!debug.contains("transport-secret-canary"));
|
||||||
|
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transport_secret_url_provenance_uses_process_and_process_beats_dotenv() {
|
||||||
|
let engine = fixture_engine();
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||||
|
process.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), "https://process.invalid".to_owned());
|
||||||
|
let mut dotenv = std::collections::BTreeMap::<String, String>::new();
|
||||||
|
dotenv.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), "https://dotenv.invalid".to_owned());
|
||||||
|
let environment = crate::ConfigEnvironment::from_maps(process, dotenv);
|
||||||
|
let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment);
|
||||||
|
assert!(resolved.is_ok(), "secret Transport environment precedence should map: {resolved:?}");
|
||||||
|
let resolved = match resolved {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
assert_eq!(resolved.settings().endpoints()[0].url().as_str(), "https://process.invalid");
|
||||||
|
let provenance = resolved.effective().provenance_at("/endpoints/0/url");
|
||||||
|
assert!(provenance.is_some(), "endpoint URL should retain environment provenance");
|
||||||
|
if let std::option::Option::Some(provenance) = provenance {
|
||||||
|
assert_eq!(provenance.len(), 1);
|
||||||
|
assert_eq!(provenance[0].environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Process));
|
||||||
|
assert_eq!(provenance[0].variable_name(), std::option::Option::Some("KSP_SECRET_TRANSPORT_TEST_URL"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_secret_transport_url_is_effective_config_error_without_secret_leak() {
|
||||||
|
let engine = fixture_engine();
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let canary = "transport-invalid-secret-canary";
|
||||||
|
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||||
|
process.insert("KSP_SECRET_TRANSPORT_TEST_URL".to_owned(), canary.to_owned());
|
||||||
|
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||||
|
let resolved = engine.load_resolved_transport_config(std::option::Option::None, &environment);
|
||||||
|
assert!(resolved.is_err(), "invalid secret-derived endpoint URL must fail effective mapping");
|
||||||
|
if let std::result::Result::Err(error) = resolved {
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
|
||||||
|
let debug = format!("{error:?}");
|
||||||
|
assert!(!debug.contains(canary));
|
||||||
|
assert!(error.context().iter().any(|item| -> bool {
|
||||||
|
return item.key() == "transport_error_code" && item.value() == "invalid_settings";
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||||
|
let workspace = workspace_root();
|
||||||
|
let fixture_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("unit_tests/fixtures");
|
||||||
|
let bootstrap = crate::ConfigBootstrapOptions::from_paths(fixture_root, workspace.join("config/schemas"));
|
||||||
|
let bootstrap = match bootstrap {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let registry = crate::ConfigFileRegistry::defaults();
|
||||||
|
let registry = match registry {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
|
||||||
|
let workspace = workspace_root();
|
||||||
|
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));
|
||||||
|
let bootstrap = match bootstrap {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let registry = crate::ConfigFileRegistry::defaults();
|
||||||
|
let registry = match registry {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workspace_root() -> std::path::PathBuf {
|
||||||
|
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||||
|
}
|
||||||
68
crates/ksp-core-lib/tests/workspace_dependencies.rs
Normal file
68
crates/ksp-core-lib/tests/workspace_dependencies.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// file: crates/ksp-core-lib/tests/workspace_dependencies.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Workspace-level dependency policy canaries owned by the foundational KSP test surface.
|
||||||
|
|
||||||
|
fn workspace_root() -> std::path::PathBuf {
|
||||||
|
let manifest_directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let parent = manifest_directory.parent();
|
||||||
|
assert!(parent.is_some(), "core crate must have a crates directory parent");
|
||||||
|
let parent = match parent {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return manifest_directory.to_path_buf(),
|
||||||
|
};
|
||||||
|
let root = parent.parent();
|
||||||
|
assert!(root.is_some(), "core crate must have a workspace root");
|
||||||
|
return match root {
|
||||||
|
std::option::Option::Some(value) => value.to_path_buf(),
|
||||||
|
std::option::Option::None => parent.to_path_buf(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_dependency_table_does_not_activate_consumer_features() {
|
||||||
|
let manifest_path = workspace_root().join("Cargo.toml");
|
||||||
|
let manifest = std::fs::read_to_string(manifest_path.as_path());
|
||||||
|
assert!(manifest.is_ok(), "workspace manifest must be readable during integration tests");
|
||||||
|
let manifest = match manifest {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let workspace_dependencies = match manifest.split("[workspace.dependencies]").nth(1) {
|
||||||
|
std::option::Option::Some(tail) => tail.split("[workspace.lints.rust]").next(),
|
||||||
|
std::option::Option::None => std::option::Option::None,
|
||||||
|
};
|
||||||
|
assert!(workspace_dependencies.is_some(), "workspace dependencies section must exist");
|
||||||
|
let workspace_dependencies = match workspace_dependencies {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
for line in workspace_dependencies.lines() {
|
||||||
|
let content = match line.split('#').next() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => continue,
|
||||||
|
};
|
||||||
|
for inline_field in content.split(',') {
|
||||||
|
let key = match inline_field.split('=').next() {
|
||||||
|
std::option::Option::Some(value) => value.trim().trim_start_matches('{').trim(),
|
||||||
|
std::option::Option::None => continue,
|
||||||
|
};
|
||||||
|
assert_ne!(key, "features", "consumer feature activation must stay in member manifests");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transport_manifest_preserves_ksp_dependency_firewall() {
|
||||||
|
let manifest_path = workspace_root().join("crates/ksp-onchain-transport-lib/Cargo.toml");
|
||||||
|
let manifest = std::fs::read_to_string(manifest_path).expect("transport manifest must be readable during workspace integration tests");
|
||||||
|
for forbidden in ["ksp-config-lib", "ksp-store-api", "ksp-store-lib", "ksp-program-api", "ksp-program-lib", "tracing =", "tracing."] {
|
||||||
|
assert!(!manifest.contains(forbidden), "forbidden direct transport dependency detected: {forbidden}");
|
||||||
|
}
|
||||||
|
assert!(manifest.contains("ksp-core-lib"));
|
||||||
|
assert!(manifest.contains("ksp-logging-lib"));
|
||||||
|
assert!(manifest.contains("reqwest = { workspace = true, features = [\"rustls\"] }"));
|
||||||
|
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"sync\", \"time\"] }"));
|
||||||
|
assert!(manifest.contains("[dev-dependencies]"));
|
||||||
|
assert!(manifest.contains("tokio = { workspace = true, features = [\"rt\"] }"));
|
||||||
|
}
|
||||||
121
crates/ksp-core-lib/tests/workspace_logging.rs
Normal file
121
crates/ksp-core-lib/tests/workspace_logging.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
// file: crates/ksp-core-lib/tests/workspace_logging.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Workspace-level logging ownership canaries for KSP behavioral crates.
|
||||||
|
|
||||||
|
fn workspace_root() -> std::path::PathBuf {
|
||||||
|
let manifest_directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let parent = manifest_directory.parent();
|
||||||
|
assert!(parent.is_some(), "core crate must have a crates directory parent");
|
||||||
|
let parent = match parent {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return manifest_directory.to_path_buf(),
|
||||||
|
};
|
||||||
|
let root = parent.parent();
|
||||||
|
assert!(root.is_some(), "core crate must have a workspace root");
|
||||||
|
return match root {
|
||||||
|
std::option::Option::Some(value) => value.to_path_buf(),
|
||||||
|
std::option::Option::None => parent.to_path_buf(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rust_source_files(directory: &std::path::Path) -> std::vec::Vec<std::path::PathBuf> {
|
||||||
|
let mut files = std::vec::Vec::new();
|
||||||
|
let entries = std::fs::read_dir(directory);
|
||||||
|
if entries.is_err() {
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
let entries = match entries {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return files,
|
||||||
|
};
|
||||||
|
for entry in entries {
|
||||||
|
let entry = match entry {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
files.extend(rust_source_files(path.as_path()));
|
||||||
|
} else if path.extension() == std::option::Option::Some(std::ffi::OsStr::new("rs")) {
|
||||||
|
files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn package_name(manifest: &str) -> std::option::Option<&str> {
|
||||||
|
for line in manifest.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if !trimmed.starts_with("name = ") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return trimmed.split('"').nth(1);
|
||||||
|
}
|
||||||
|
return std::option::Option::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn behavioral_crates_own_explicit_tracing_targets() {
|
||||||
|
let crates_root = workspace_root().join("crates");
|
||||||
|
let entries = std::fs::read_dir(crates_root.as_path());
|
||||||
|
assert!(entries.is_ok(), "workspace crates directory must be readable");
|
||||||
|
let entries = match entries {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
for entry in entries {
|
||||||
|
let entry = match entry {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
let crate_root = entry.path();
|
||||||
|
if !crate_root.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let source_root = crate_root.join("src");
|
||||||
|
let source_files = rust_source_files(source_root.as_path());
|
||||||
|
let mut emits_ksp_logs = false;
|
||||||
|
for source_file in source_files.iter() {
|
||||||
|
let source = std::fs::read_to_string(source_file.as_path());
|
||||||
|
assert!(source.is_ok(), "Rust source must be readable: {}", source_file.display());
|
||||||
|
let source = match source {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
if source.contains("ksp_logging_lib::") {
|
||||||
|
emits_ksp_logs = true;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
!source.contains("target: env!(\"CARGO_PKG_NAME\")"),
|
||||||
|
"KSP tracing target must be explicit rather than derived from Cargo metadata: {}",
|
||||||
|
source_file.display()
|
||||||
|
);
|
||||||
|
assert!(!source.contains("target: \"ksp-"), "KSP tracing target literals must be owned by src/constants.rs: {}", source_file.display());
|
||||||
|
}
|
||||||
|
if !emits_ksp_logs {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let manifest = std::fs::read_to_string(crate_root.join("Cargo.toml"));
|
||||||
|
assert!(manifest.is_ok(), "behavioral crate manifest must be readable: {}", crate_root.display());
|
||||||
|
let manifest = match manifest {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
let package_name = package_name(manifest.as_str());
|
||||||
|
assert!(package_name.is_some(), "behavioral crate package name must be discoverable: {}", crate_root.display());
|
||||||
|
let package_name = match package_name {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => continue,
|
||||||
|
};
|
||||||
|
let constants_path = source_root.join("constants.rs");
|
||||||
|
let constants = std::fs::read_to_string(constants_path.as_path());
|
||||||
|
assert!(constants.is_ok(), "behavioral crate must own src/constants.rs: {}", crate_root.display());
|
||||||
|
let constants = match constants {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => continue,
|
||||||
|
};
|
||||||
|
let expected = std::format!("pub(crate) const TRACING_TARGET: &str = \"{package_name}\";");
|
||||||
|
assert!(constants.contains(expected.as_str()), "behavioral crate must own its Cargo-name tracing target: {}", crate_root.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-logging-lib/Cargo.toml
|
# file: crates/ksp-logging-lib/Cargo.toml
|
||||||
# version: 4
|
# version: 5
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-logging-lib"
|
name = "ksp-logging-lib"
|
||||||
@@ -9,12 +9,12 @@ repository.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ksp-core-lib = { path = "../ksp-core-lib" }
|
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||||
tracing.workspace = true
|
tracing = { workspace = true, features = ["std"] }
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber = { workspace = true, features = ["fmt", "json", "ansi"] }
|
||||||
tracing-appender.workspace = true
|
tracing-appender.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio.workspace = true
|
tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
22
crates/ksp-onchain-transport-lib/Cargo.toml
Normal file
22
crates/ksp-onchain-transport-lib/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# file: crates/ksp-onchain-transport-lib/Cargo.toml
|
||||||
|
# version: 3
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "ksp-onchain-transport-lib"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ksp-core-lib = { path = "../ksp-core-lib" }
|
||||||
|
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||||
|
reqwest = { workspace = true, features = ["rustls"] }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json.workspace = true
|
||||||
|
tokio = { workspace = true, features = ["macros", "sync", "time"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { workspace = true, features = ["rt"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
149
crates/ksp-onchain-transport-lib/README.md
Normal file
149
crates/ksp-onchain-transport-lib/README.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
||||||
|
<!-- version: 7 -->
|
||||||
|
|
||||||
|
# `ksp-onchain-transport-lib`
|
||||||
|
|
||||||
|
`ksp-onchain-transport-lib` est la bibliothèque KSP propriétaire du transport on-chain Solana. Sa première surface est le transport HTTP JSON-RPC ; les extensions WebSocket et gRPC sont introduites séparément lorsque leur release les cible.
|
||||||
|
|
||||||
|
## Responsabilités
|
||||||
|
|
||||||
|
La crate possède :
|
||||||
|
|
||||||
|
- les settings runtime HTTP publics ;
|
||||||
|
- les endpoints nommés et leurs metadata provider/cluster ;
|
||||||
|
- les rôles, capabilities/request kinds et priorités ;
|
||||||
|
- la sélection/fairness/fallback du pool ;
|
||||||
|
- les limites RPS/burst/concurrence et le cooldown ;
|
||||||
|
- les deadlines et timeouts ;
|
||||||
|
- le retry/backoff borné et la règle no-resend après dispatch ambigu ;
|
||||||
|
- les enveloppes JSON-RPC 2.0 et leur validation ;
|
||||||
|
- le registre audité des méthodes Solana HTTP ;
|
||||||
|
- l'exécution générique des méthodes standard supportées ;
|
||||||
|
- les wrappers typés explicitement livrés par KSP ;
|
||||||
|
- les snapshots runtime sûrs ;
|
||||||
|
- l'observabilité Transport via `ksp-logging-lib`.
|
||||||
|
|
||||||
|
La crate ne possède ni documents Config, ni persistence Store, ni modèles Program/métier.
|
||||||
|
|
||||||
|
## Frontières de dépendances
|
||||||
|
|
||||||
|
La direction autorisée est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-config-lib
|
||||||
|
-> ksp-onchain-transport-lib
|
||||||
|
-> ksp-core-lib
|
||||||
|
-> ksp-logging-lib
|
||||||
|
-> reqwest / tokio / serde
|
||||||
|
```
|
||||||
|
|
||||||
|
La direction inverse est interdite :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-onchain-transport-lib -X-> ksp-config-lib
|
||||||
|
ksp-onchain-transport-lib -X-> Store
|
||||||
|
ksp-onchain-transport-lib -X-> Program
|
||||||
|
ksp-onchain-transport-lib -X-> tracing direct
|
||||||
|
```
|
||||||
|
|
||||||
|
`ksp-config-lib` peut donc charger `std.transport.json` et construire `HttpTransportSettings`, tandis que Transport reste directement utilisable par un consumer qui fournit lui-même ses settings.
|
||||||
|
|
||||||
|
## Surface HTTP standard
|
||||||
|
|
||||||
|
Le registre KSP conserve deux inventaires distincts :
|
||||||
|
|
||||||
|
```text
|
||||||
|
52 méthodes HTTP courantes
|
||||||
|
14 méthodes historiques Deprecated / runtime Removed
|
||||||
|
```
|
||||||
|
|
||||||
|
Le registre porte notamment :
|
||||||
|
|
||||||
|
- catégorie ;
|
||||||
|
- request kind ;
|
||||||
|
- statut documentaire ;
|
||||||
|
- statut runtime ;
|
||||||
|
- forme de requête stable/legacy ;
|
||||||
|
- type d'opération ;
|
||||||
|
- classe de retry ;
|
||||||
|
- remplacement historique éventuel ;
|
||||||
|
- release de couverture typée KSP.
|
||||||
|
|
||||||
|
La release stable `0.2.3` porte la surface typée à **37 méthodes courantes** :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.1 foundation : 4
|
||||||
|
0.2.2 Accounts : 5
|
||||||
|
0.2.2 Tokens : 5
|
||||||
|
0.2.2 Cluster : 12
|
||||||
|
0.2.3 Transactions : 11
|
||||||
|
```
|
||||||
|
|
||||||
|
Les quatre canaris foundation restent `getBalance`, `getGenesisHash`, `getHealth` et `getVersion`. `0.2.2` ajoute les 22 wrappers Accounts/Tokens/Cluster. `0.2.3` ajoute les 11 wrappers Transactions, dont `requestAirdrop` et `sendTransaction` en `WriteSubmission / NeverAfterDispatch` et `simulateTransaction` en `Simulation / RetrySafe`.
|
||||||
|
|
||||||
|
Les **15 méthodes Blocks/Economics** encore affectées à `0.2.4` peuvent déjà passer par l'exécuteur JSON-RPC standard générique lorsqu'un consumer fournit explicitement descriptor et paramètres JSON, mais cette surface raw/générique **ne vaut pas couverture typée**.
|
||||||
|
|
||||||
|
`KSP-TRANSPORT-007` impose qu'un wrapper typé couvre toutes les possibilités RPC supportées retenues par l'audit : paramètres/options, overloads et formes legacy encore supportées, contraintes déterministes utiles et variantes de réponse pertinentes sans perte. Le réaudit `0.2.3-pre.008` confirme cette complétude sur les 37 wrappers courants.
|
||||||
|
|
||||||
|
Les 14 méthodes historiques restent découvrables pour la compliance mais sont `Removed` et ne sont pas simulées comme appelables.
|
||||||
|
|
||||||
|
## Résilience
|
||||||
|
|
||||||
|
L'admission est calculée par couple endpoint/rôle. Le pool applique :
|
||||||
|
|
||||||
|
1. rôle et capability ;
|
||||||
|
2. priorité croissante ;
|
||||||
|
3. round-robin dans le meilleur tier ;
|
||||||
|
4. RPS/burst ;
|
||||||
|
5. concurrence ;
|
||||||
|
6. cooldown rate-limit ;
|
||||||
|
7. fallback vers les pairs puis les tiers inférieurs ;
|
||||||
|
8. deadline commune à l'opération et ses retries.
|
||||||
|
|
||||||
|
Les retries ne sont autorisés que lorsque la metadata de méthode et l'état de dispatch les rendent sûrs. `WriteSubmission / NeverAfterDispatch` interdit tout resend automatique après un dispatch ambigu.
|
||||||
|
|
||||||
|
Les erreurs JSON-RPC applicatives ne sont pas transformées en retries transport génériques.
|
||||||
|
|
||||||
|
## Sécurité et diagnostics
|
||||||
|
|
||||||
|
Les URLs d'endpoint peuvent contenir des credentials. Elles ne sont donc pas exposées par les `Debug`, snapshots ou logs ordinaires.
|
||||||
|
|
||||||
|
Les `reqwest::Error` attachées comme source sont neutralisées avec `without_url()` avant exposition dans le contrat d'erreur KSP.
|
||||||
|
|
||||||
|
Le target de tracing est possédé explicitement par :
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/constants.rs
|
||||||
|
TRACING_TARGET = "ksp-onchain-transport-lib"
|
||||||
|
```
|
||||||
|
|
||||||
|
La configuration Logging de référence conserve un fichier dédié Transport à niveau `info`. Un niveau `debug`/`trace` ciblé peut être réactivé temporairement via Config lors d'un développement ou diagnostic explicite.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Les tests par défaut sont déterministes et n'exigent pas Internet : fixtures JSON et serveur HTTP local couvrent requêtes, réponses, retry, 429, timeout, redaction et routing.
|
||||||
|
|
||||||
|
Deux smokes Devnet opt-in sont séparés par responsabilité :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport pur : settings programmatiques -> HttpTransportPool
|
||||||
|
-> Accounts/Tokens/Cluster représentatifs
|
||||||
|
-> getLatestBlockhash/isBlockhashValid/getTransactionCount
|
||||||
|
|
||||||
|
Composition historique : Config -> std.transport/devnet_public -> HttpTransportPool
|
||||||
|
-> getHealth/getGenesisHash/getVersion/getBalance
|
||||||
|
```
|
||||||
|
|
||||||
|
Le smoke Transport utilise pour sa branche Token la forme Devnet documentée `getTokenAccountsByOwner(owner, { programId }, { commitment: finalized, encoding: jsonParsed })`. L'owner est une Pubkey ordinaire de l'exemple officiel ; aucune présence de token account n'est exigée, donc une liste vide reste valide.
|
||||||
|
|
||||||
|
Les deux sont `ignored` par défaut. Le smoke Transport appartient durablement à cette crate ; le smoke cross-crates hébergé dans Config reste transitoire jusqu'à l'existence d'une surface KSP d'intégration/orchestration appropriée.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [`USAGE.md`](USAGE.md) — consommation directe, Config -> Transport, API typed/raw, smokes et inspection runtime ;
|
||||||
|
- [`../../docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md`](../../docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md) — foundation HTTP stable ;
|
||||||
|
- [`../../docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md`](../../docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md) — extension typed Accounts/Tokens/Cluster ;
|
||||||
|
- [`../../docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md`](../../docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md) — matrice finale validée `0.2.2` ;
|
||||||
|
- [`../../docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](../../docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) — plan historique clôturé de `0.2.3` ;
|
||||||
|
- [`../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md) — matrice finale validée `0.2.3` ;
|
||||||
|
- [`../../config/std.transport.json`](../../config/std.transport.json) — configuration standard HTTP.
|
||||||
191
crates/ksp-onchain-transport-lib/USAGE.md
Normal file
191
crates/ksp-onchain-transport-lib/USAGE.md
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
|
||||||
|
<!-- version: 7 -->
|
||||||
|
|
||||||
|
# Utilisation de `ksp-onchain-transport-lib`
|
||||||
|
|
||||||
|
Ce guide présente les surfaces publiques destinées aux consumers. Les notes de release restent dans `CHANGELOG.md` et les deltas.
|
||||||
|
|
||||||
|
## 1. Construction directe du runtime
|
||||||
|
|
||||||
|
Transport peut être utilisé sans Config. Le consumer construit les settings publics puis le pool :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let url = match ksp_onchain_transport_lib::HttpEndpointUrl::parse("https://api.devnet.solana.com") {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new(
|
||||||
|
ksp_onchain_transport_lib::HttpRoleName::new("default"),
|
||||||
|
true,
|
||||||
|
vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()],
|
||||||
|
100,
|
||||||
|
ksp_onchain_transport_lib::HttpRoleLimits::new(None, None, None, None),
|
||||||
|
);
|
||||||
|
let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new(
|
||||||
|
"solana_devnet_public",
|
||||||
|
true,
|
||||||
|
ksp_onchain_transport_lib::HttpProviderName::new("solana-public"),
|
||||||
|
ksp_onchain_transport_lib::HttpClusterName::new("devnet"),
|
||||||
|
url,
|
||||||
|
std::time::Duration::from_secs(5),
|
||||||
|
std::time::Duration::from_secs(15),
|
||||||
|
Some(8),
|
||||||
|
vec![role],
|
||||||
|
);
|
||||||
|
let settings = ksp_onchain_transport_lib::HttpTransportSettings::new(
|
||||||
|
vec![endpoint],
|
||||||
|
ksp_onchain_transport_lib::HttpRetrySettings::new(
|
||||||
|
2,
|
||||||
|
std::time::Duration::from_millis(100),
|
||||||
|
std::time::Duration::from_secs(2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let pool = match ksp_onchain_transport_lib::HttpTransportPool::new(settings) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`HttpTransportSettings::validate()` peut être appelé explicitement avant la construction du pool lorsque le consumer veut séparer validation et initialisation.
|
||||||
|
|
||||||
|
## 2. Construction via `ksp-config-lib`
|
||||||
|
|
||||||
|
Lorsque le consumer utilise Config, la direction reste Config -> Transport :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let resolved = match engine.load_resolved_transport_config(Some("devnet_public"), &environment) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let pool = match ksp_onchain_transport_lib::HttpTransportPool::new(resolved.into_settings()) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Le document standard peut contenir une URL provenant d'un `KSP_SECRET_*`. La valeur réelle est transmise au runtime, mais les projections sûres et `Debug` restent redacted.
|
||||||
|
|
||||||
|
## 3. Appels typés
|
||||||
|
|
||||||
|
Les wrappers typés se trouvent directement sur `HttpTransportPool`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let role = ksp_onchain_transport_lib::HttpRoleName::new("default");
|
||||||
|
let health = pool.get_health(&role).await;
|
||||||
|
let genesis_hash = pool.get_genesis_hash(&role).await;
|
||||||
|
let version = pool.get_version(&role).await;
|
||||||
|
let balance = pool
|
||||||
|
.get_balance(
|
||||||
|
&role,
|
||||||
|
&ksp_core_lib::PRGIDPK_SOLANA_SYSTEM,
|
||||||
|
Some(&ksp_onchain_transport_lib::GetBalanceConfig::new(
|
||||||
|
Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||||
|
None,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
```
|
||||||
|
|
||||||
|
Les quatre canaris `0.2.1` restent disponibles. `0.2.2` ajoute les wrappers typés Accounts, Tokens et Cluster. Exemples représentatifs :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let account = pool
|
||||||
|
.get_account_info(&role, &ksp_core_lib::PRGIDPK_SOLANA_SYSTEM, None)
|
||||||
|
.await;
|
||||||
|
let epoch = pool.get_epoch_info(&role, None).await;
|
||||||
|
let vote_accounts = pool.get_vote_accounts(&role, None).await;
|
||||||
|
```
|
||||||
|
|
||||||
|
La release stable `0.2.3` contient 37 wrappers typés : 4 foundation + 5 Accounts + 5 Tokens + 12 Cluster + 11 Transactions. Les DTOs Transport conservent les `null`, options et formes wire sans décodage Program/SPL métier.
|
||||||
|
|
||||||
|
Exemples Transaction représentatifs :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let context = ksp_onchain_transport_lib::SolanaContextConfig::new(
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized),
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
let latest = pool
|
||||||
|
.get_latest_blockhash(&role, std::option::Option::Some(&context))
|
||||||
|
.await;
|
||||||
|
let transaction_count = pool
|
||||||
|
.get_transaction_count(&role, std::option::Option::Some(&context))
|
||||||
|
.await;
|
||||||
|
```
|
||||||
|
|
||||||
|
`getTransaction` expose la config moderne complète et une forme bare-encoding legacy séparée et deprecated. `requestAirdrop` et `sendTransaction` sont des write submissions : elles utilisent la protection centrale `NeverAfterDispatch`. `simulateTransaction` reste une simulation retry-safe et conserve son résultat riche sans introduire de décodage Program.
|
||||||
|
|
||||||
|
## 4. Exécution JSON-RPC standard générique
|
||||||
|
|
||||||
|
Une méthode courante auditée peut être appelée via son descriptor :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
if let Some(descriptor) = ksp_onchain_transport_lib::find_http_rpc_method("getSlot") {
|
||||||
|
let _result = pool.execute_standard_rpc(&role, descriptor, vec![]).await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette API retourne un `serde_json::Value`. Elle est utile pour les consumers techniques et pour préparer les futures surfaces typées, mais elle ne remplace pas le wrapper typé d'une méthode dans la matrice de couverture KSP.
|
||||||
|
|
||||||
|
Avant exécution, `ensure_runtime_supported()` est appliqué. Une méthode historique `Removed` retourne `ERROR_CODE_METHOD_REMOVED` au lieu d'émettre un appel réseau fictif.
|
||||||
|
|
||||||
|
## 5. Sélection et admission sans exécuter la requête
|
||||||
|
|
||||||
|
Pour inspecter le routing :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
if let Some(descriptor) = ksp_onchain_transport_lib::find_http_rpc_method("getBalance") {
|
||||||
|
let _selection = pool.select_for_method(&role, descriptor);
|
||||||
|
let _permit = pool.acquire_for_method(&role, descriptor).await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Dans le même bloc, `acquire_for_method()` réserve réellement la capacité RPS/concurrence sous deadline.
|
||||||
|
|
||||||
|
`HttpRequestPermit` détient la capacité de concurrence jusqu'à sa destruction. Aucun verrou synchrone n'est conservé pendant l'attente réseau.
|
||||||
|
|
||||||
|
## 6. Snapshots runtime
|
||||||
|
|
||||||
|
`HttpTransportPool::snapshot()` fournit une vue sûre des endpoints/rôles : disponibilité, limites, requêtes en vol, cooldown restant et compteurs runtime.
|
||||||
|
|
||||||
|
Les URLs d'endpoint n'y apparaissent jamais.
|
||||||
|
|
||||||
|
## 7. Retry et write submissions
|
||||||
|
|
||||||
|
La policy de retry est portée par la metadata des méthodes et `evaluate_transport_retry()`.
|
||||||
|
|
||||||
|
Les reads/simulations classés `RetrySafe` peuvent être réessayés dans le budget configuré lorsqu'une cause transport est explicitement retryable.
|
||||||
|
|
||||||
|
Pour une opération `WriteSubmission / NeverAfterDispatch`, un timeout ou autre résultat ambigu après dispatch arrête la resoumission automatique. Le consumer métier ne doit pas contourner cette protection avec une boucle de retry externe aveugle.
|
||||||
|
|
||||||
|
## 8. Logging
|
||||||
|
|
||||||
|
Les événements Transport utilisent le target :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-onchain-transport-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
Ne jamais journaliser l'URL complète, un token provider, un body massif, une transaction complète ou une réponse complète.
|
||||||
|
|
||||||
|
La configuration standard route les événements `info` de Transport vers un fichier dédié. Pour une investigation temporaire, élever uniquement ce target/sink à `debug` ou `trace`, puis revenir à `info` avant clôture du développement.
|
||||||
|
|
||||||
|
## 9. Smokes Devnet opt-in
|
||||||
|
|
||||||
|
Le smoke **Transport pur** construit ses settings programmatiquement et exerce un sous-ensemble représentatif de `0.2.2` puis trois reads Transactions de `0.2.3` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
Il appelle `getAccountInfo`, `getTokenAccountsByOwner`, `getEpochInfo`, `getVoteAccounts`, puis `getLatestBlockhash`, `isBlockhashValid` et `getTransactionCount`. La branche Token suit la forme Devnet documentée : owner Pubkey ordinaire de l'exemple officiel, selector `programId` avec l'ID canonique du programme SPL Token, puis config explicite `commitment: finalized` + `encoding: jsonParsed`. Une réponse vide reste acceptable. La branche Transaction reste read-only : elle ne déclenche ni airdrop ni soumission de transaction et ne remplace pas les fixtures déterministes couvrant les 11 wrappers.
|
||||||
|
|
||||||
|
Le smoke historique de **composition Config -> Transport** reste également disponible :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test -p ksp-config-lib --test transport_devnet_smoke -- --ignored --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
Il valide le profil committé `devnet_public` et les quatre canaris foundation. Il reste transitoirement hébergé dans Config : les futurs smokes cross-crates ne doivent pas faire de Config leur destination générale et devront migrer vers une surface d'intégration/orchestration dédiée lorsqu'elle existera.
|
||||||
|
|
||||||
|
Les endpoints publics Solana sont rate-limités et non destinés à la production. Un échec réseau externe n'est pas assimilé automatiquement à une régression locale ; les fixtures HTTP locales restent les gates reproductibles.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
{"lamports":1,"data":"3MN5","owner":"11111111111111111111111111111111","executable":false,"rentEpoch":0,"space":null},
|
||||||
|
{"lamports":2,"data":["KLUv/Q==","base64+zstd"],"owner":"11111111111111111111111111111111","executable":false,"rentEpoch":1,"space":4},
|
||||||
|
{"lamports":3,"data":{"program":"spl-token","parsed":{"type":"account","info":{"state":"initialized"}},"space":165},"owner":"11111111111111111111111111111111","executable":false,"rentEpoch":2,"space":165}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"pubkey":"11111111111111111111111111111111","featureSet":123,"gossip":"127.0.0.1:8001","pubsub":null,"rpc":"127.0.0.1:8899","serveRepair":"127.0.0.1:8003","shredVersion":456,"tpu":"127.0.0.1:8004","tpuForwards":null,"tpuForwardsQuic":"127.0.0.1:8006","tpuQuic":"127.0.0.1:8005","tpuVote":null,"tvu":"127.0.0.1:8002","version":"4.2.1","clientId":"Agave"}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"slot": 430000123,
|
||||||
|
"blockTime": 1787000000,
|
||||||
|
"transaction": ["AQIDBAUGBwg=", "base64"],
|
||||||
|
"meta": {
|
||||||
|
"err": null,
|
||||||
|
"status": {"Ok": null},
|
||||||
|
"fee": 5000,
|
||||||
|
"preBalances": [10000],
|
||||||
|
"postBalances": [5000]
|
||||||
|
},
|
||||||
|
"version": 0,
|
||||||
|
"transactionIndex": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slot": 430000100,
|
||||||
|
"blockTime": null,
|
||||||
|
"transaction": "3Bxs4NN8M2Yn4TLb",
|
||||||
|
"meta": null,
|
||||||
|
"version": "legacy",
|
||||||
|
"transactionIndex": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slot": 430000050,
|
||||||
|
"blockTime": null,
|
||||||
|
"transaction": {
|
||||||
|
"signatures": [],
|
||||||
|
"message": {
|
||||||
|
"accountKeys": [],
|
||||||
|
"recentBlockhash": "11111111111111111111111111111111",
|
||||||
|
"instructions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
"3Bxs4NN8M2Yn4TLb",
|
||||||
|
["AQIDBAUGBwg=", "base64"],
|
||||||
|
{
|
||||||
|
"signatures": ["5h6xBEauJ3PK6SWQkgLxHcQpWyRZyG7c4pUpK9fd2B2B7hRQ1S6F9J7BqvQPEmxQxK4DThfYF5f5f5f5f5f5f5f5"],
|
||||||
|
"message": {
|
||||||
|
"accountKeys": [],
|
||||||
|
"recentBlockhash": "11111111111111111111111111111111",
|
||||||
|
"instructions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000003},"value":{"data":["","base64"],"executable":false,"lamports":1,"owner":"not-a-pubkey","rentEpoch":0,"space":0}},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000002},"value":null},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000001},"value":{"data":["AQIDBA==","base64"],"executable":false,"lamports":2039280,"owner":"11111111111111111111111111111111","rentEpoch":18446744073709551615,"space":4}},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"3.1.8","slot":123456789},"value":424242},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":[{"pubkey":"not-a-pubkey","rpc":"127.0.0.1:8899"}],"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":[{"pubkey":"11111111111111111111111111111111","featureSet":3073396398,"gossip":"127.0.0.1:8001","pubsub":null,"rpc":"127.0.0.1:8899","serveRepair":"127.0.0.1:8004","shredVersion":50093,"tpu":"127.0.0.1:8003","tpuForwards":"127.0.0.1:8004","tpuForwardsQuic":"127.0.0.1:8006","tpuQuic":"127.0.0.1:8009","tpuVote":"127.0.0.1:8005","tvu":"127.0.0.1:8000","version":"4.2.1","clientId":"Agave"},{"pubkey":"Stake11111111111111111111111111111111111111"}],"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"absoluteSlot":430000001,"blockHeight":429900000,"epoch":995,"slotIndex":12345,"slotsInEpoch":432000,"transactionCount":null},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"firstNormalEpoch":0,"firstNormalSlot":0,"leaderScheduleSlotOffset":432000,"slotsPerEpoch":432000,"warmup":false},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param: unable to decode message"},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000002},"value":null},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000001},"value":5000},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":"GH7ome3EiwEr7tu9JuTh2dpYWBJK3z69Xm1ZE3MEE6JC","id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":"ok","id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","error":{"code":-32008,"message":"No snapshot"},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"full":429990000,"incremental":null},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"identity":"invalid-identity"},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"identity":"ComputeBudget111111111111111111111111111111"},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000005},"value":[{"address":"invalid-address","lamports":1}]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000004},"value":[{"address":"11111111111111111111111111111111","lamports":999999999},{"address":"ComputeBudget111111111111111111111111111111","lamports":888888888}]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000004},"value":{"blockhash":"","lastValidBlockHeight":431123456}},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000003},"value":{"blockhash":"11111111111111111111111111111111","lastValidBlockHeight":431123456}},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":null,"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"11111111111111111111111111111111":[0,2,4],"ComputeBudget111111111111111111111111111111":[1,3]} ,"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":430000010,"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":430000011,"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param"},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":890880,"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000006},"value":[{"data":["","base64"],"executable":false,"lamports":10,"owner":"11111111111111111111111111111111","rentEpoch":0,"space":0},null,{"data":{"program":"system","parsed":{"type":"nonce"},"space":80},"executable":false,"lamports":20,"owner":"11111111111111111111111111111111","rentEpoch":1,"space":80}]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":[{"pubkey":"ComputeBudget111111111111111111111111111111","account":{"data":["AQID","base64"],"executable":false,"lamports":42,"owner":"11111111111111111111111111111111","rentEpoch":0,"space":3}}],"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000007},"value":[{"pubkey":"ComputeBudget111111111111111111111111111111","account":{"data":["AQID","base64"],"executable":false,"lamports":42,"owner":"11111111111111111111111111111111","rentEpoch":0,"space":3}}]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":[{"slot":431000030,"prioritizationFee":1200},{"slot":431000029,"prioritizationFee":0}],"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000052},"value":[null]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000051},"value":[]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000053},"value":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000050},"value":[null,{"slot":431000049,"confirmations":null,"status":{"Ok":null},"err":null,"confirmationStatus":"finalized"},{"slot":431000048,"confirmations":2,"status":{"Err":{"InstructionError":[0,"Custom"]}},"err":{"InstructionError":[0,"Custom"]},"confirmationStatus":"confirmed"}]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","error":{"code":-32015,"message":"Transaction history is not available from this node"},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":[{"signature":"newest-signature","slot":431000040,"err":null,"memo":"memo-newest","blockTime":1787000100,"confirmationStatus":"finalized","transactionIndex":9},{"signature":"older-signature","slot":431000039,"err":{"InstructionError":[0,"Custom"]},"memo":null,"blockTime":null,"confirmationStatus":"confirmed"}],"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":430000020,"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":"not-a-pubkey","id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":"ComputeBudget111111111111111111111111111111","id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":["11111111111111111111111111111111","not-a-pubkey"],"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":["11111111111111111111111111111111","ComputeBudget111111111111111111111111111111"],"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param: could not find account"},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":420000001},"value":{"amount":"18446744073709551615","decimals":9,"uiAmount":null,"uiAmountString":"18446744073.709551615"}},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":420000002},"value":[{"pubkey":"ComputeBudget111111111111111111111111111111","account":{"data":{"program":"spl-token","parsed":{"type":"account","info":{"tokenAmount":{"amount":"1","decimals":1,"uiAmount":0.1,"uiAmountString":"0.1"}}},"space":165},"executable":false,"lamports":2039280,"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","rentEpoch":9,"space":165}}]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"slot":420000004},"value":[{"pubkey":"not-a-pubkey","account":{"data":["","base64"],"executable":false,"lamports":1,"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","rentEpoch":0,"space":165}}]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":null,"slot":420000003},"value":[{"pubkey":"Stake11111111111111111111111111111111111111","account":{"data":["AQID","base64"],"executable":false,"lamports":2039280,"owner":"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA","rentEpoch":10,"space":165}}]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"slot":420000006},"value":[{"address":"invalid-address","amount":"1","decimals":0,"uiAmount":1.0,"uiAmountString":"1"}]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":420000005},"value":[{"address":"11111111111111111111111111111111","amount":"9000","decimals":2,"uiAmount":90.0,"uiAmountString":"90"},{"address":"ComputeBudget111111111111111111111111111111","amount":"8000","decimals":2,"uiAmount":80.0,"uiAmountString":"80"}]},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":420000007},"value":{"amount":"1000000000000000000000000","decimals":6,"uiAmount":1000000000000000000.0,"uiAmountString":"1000000000000000000"}},"id":1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"jsonrpc":"2.0","result":{"slot":431000061,"blockTime":1787000201,"transaction":["3Bxs4NN8M2Yn4TLb","base58"],"meta":null,"version":"legacy","transactionIndex":0},"id":1}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user