57 Commits

Author SHA1 Message Date
5b3967327a v0.2.4-rel.001 2026-08-18 23:51:56 +02:00
804b92b957 v0.2.4-pre.009-fix.001 2026-08-18 23:26:17 +02:00
e6772c109d v0.2.4-pre.009 2026-08-18 22:43:09 +02:00
7e84522787 v0.2.4-pre.008-fix.001 2026-08-18 21:53:33 +02:00
37461ec248 v0.2.4-pre.008 2026-08-18 21:52:00 +02:00
4252a21140 v0.2.4-pre.007 2026-08-18 21:33:21 +02:00
7a889dd466 v0.2.4-pre.006 2026-08-18 21:21:02 +02:00
d3cc7c93f0 v0.2.4-pre.005 2026-08-18 21:03:00 +02:00
561b6678ed v0.2.4-pre.004 2026-08-18 20:54:16 +02:00
e500024c3a v0.2.4-pre.003 2026-08-18 20:51:52 +02:00
033800f897 v0.2.4-pre.002-fix.001 2026-08-18 20:21:01 +02:00
cf8d79567d v0.2.4-pre.002 2026-08-18 20:12:46 +02:00
cb19742cf7 v0.2.4-pre.001-fix.001 2026-08-18 19:51:00 +02:00
c03a3eaf5d v0.2.4-pre.001 2026-08-18 19:36:57 +02:00
f7acac3fc8 v0.2.3-rel.001 2026-08-18 17:42:12 +02:00
039b93cfb3 v0.2.3-pre.009 2026-08-18 17:33:46 +02:00
c9a0f7fd37 v0.2.3-pre.008 2026-08-18 16:47:54 +02:00
704110d509 v0.2.3-pre.007 2026-08-18 16:12:07 +02:00
fb32ed03f8 v0.2.3-pre.006-fix.001 2026-08-18 12:40:24 +02:00
4439589f0d v0.2.3-pre.006 2026-08-18 12:35:29 +02:00
1e1bc0d421 v0.2.3-pre.005 2026-08-18 12:33:16 +02:00
bd26d4f636 v0.2.3-pre.004 2026-08-18 12:14:59 +02:00
2a7f3e8f40 v0.2.3-pre.003 2026-08-18 11:35:51 +02:00
1d37f04d63 v0.2.3-pre.002-fix.001 2026-08-18 11:27:24 +02:00
1ccf11bcc1 v0.2.3-pre.002 2026-08-18 11:13:04 +02:00
d06284fca2 v0.2.3-pre.001 2026-08-18 10:47:19 +02:00
fff69ead9c v0.2.2-rel.001 2026-08-18 10:14:43 +02:00
c93a833bac v0.2.2-pre.007-fix.002 2026-08-18 10:04:18 +02:00
05e0e96083 v0.2.2-pre.007-fix.001 2026-08-18 09:56:55 +02:00
b57f796187 v0.2.2-pre.007 2026-08-18 09:48:11 +02:00
ecfb9500eb v0.2.2-pre.006-fix.001 2026-08-18 09:37:03 +02:00
e14ce1c36f v0.2.2-pre.006 2026-08-18 08:27:42 +02:00
83cb861e54 v0.2.2-pre.005 2026-08-18 08:17:10 +02:00
1d1bc6a4d6 v0.2.2-pre.004 2026-08-18 08:09:20 +02:00
c4636ac8b9 v0.2.2-pre.003-fix.001 2026-08-18 07:53:17 +02:00
f15448ff6e v0.2.2-pre.003 2026-08-18 07:31:07 +02:00
bec1f2ec08 v0.2.2-pre.002-fix.002 2026-08-18 07:20:44 +02:00
f625ee5979 v0.2.2-pre.002-fix.001 2026-08-18 07:17:49 +02:00
e68f073505 v0.2.2-pre.002 2026-08-18 07:15:59 +02:00
bffb4f9a31 v0.2.2-pre.001-fix.001 2026-08-18 06:46:59 +02:00
9059a2dc45 v0.2.2-pre.001 2026-08-18 06:41:41 +02:00
6c3ecf1f18 v0.2.1-rel.001 2026-08-17 22:50:56 +02:00
79b67f8eae v0.2.1-pre.007 2026-08-17 22:43:07 +02:00
e0a7ac0bf8 v0.2.1-pre.006-fix.002 2026-08-17 21:41:20 +02:00
598474438b v0.2.1-pre.006-fix.001 2026-08-17 21:16:26 +02:00
f5d98c4e69 v0.2.1-pre.006 2026-08-17 21:03:51 +02:00
14bcbf2cfb v0.2.1-pre.005-fix.001 2026-08-17 20:40:08 +02:00
ac1b1033c4 v0.2.1-pre.005 2026-08-17 20:27:44 +02:00
bc71fba289 v0.2.1-pre.004-fix.002 2026-08-17 19:47:24 +02:00
9c1568ee1c v0.2.1-pre.004-fix.001 2026-08-17 19:40:12 +02:00
c1cea6e813 v0.2.1-pre.004 2026-08-17 19:26:26 +02:00
ed978179d8 v0.2.1-pre.003-fix.001 2026-08-17 18:59:01 +02:00
babe7d9f2b v0.2.1-pre.003 2026-08-17 18:55:14 +02:00
d3fc0c6d69 v0.2.1-pre.002-fix.001 2026-08-17 18:46:52 +02:00
0cff0406ab v0.2.1-pre.002 2026-08-17 18:02:18 +02:00
d98d152f08 v0.2.1-pre.001-fix.001 2026-08-17 17:36:35 +02:00
624202c363 0.2.1-pre.1 2026-08-17 17:20:41 +02:00
291 changed files with 39158 additions and 247 deletions

View File

@@ -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

View File

@@ -1,10 +1,26 @@
<!-- file: CHANGELOG.md --> <!-- file: CHANGELOG.md -->
<!-- version: 4 --> <!-- version: 8 -->
# Changelog KSP # Changelog KSP
## 0.2.4 — HTTP Blocks + Economics + compliance HTTP finale — 2026-08-18
`0.2.4` achève la surface HTTP Solana standard de `ksp-onchain-transport-lib` : les 10 wrappers Blocks et 5 wrappers Economics portent la couverture typée à **52/52 méthodes HTTP courantes**, tandis que les **14/14 méthodes historiques Deprecated/runtime Removed** restent conservées pour compliance. La release stabilise notamment `getBlock` moderne avec sa forme bare encoding legacy dépréciée, les quatre variantes `transactionDetails`, les versions transaction numériques génériques, `numRewardPartitions` (SIMD-0118), `commissionBps` (SIMD-0291), les overloads de ranges/performance, `getBlockProduction`, les valeurs Economics fournies par le runtime et `getInflationReward` positionnel sans plafond d'adresses inventé. Le réaudit final confirme l'égalité exacte entre l'inventaire HTTP officiel courant et le registre KSP, applique `KSP-TRANSPORT-007` aux **52/52 wrappers**, et ne matérialise aucune extension spéculative issue de la watchlist SIMD. Les canaries de frontières, le workspace complet et les deux smokes Devnet — Transport pur couvrant Accounts/Tokens/Cluster/Transactions/Blocks/Economics et composition Config -> Transport transitoire — sont validés avant publication. Le prompt `prompts/010-V0_2_5_START_PROMPT.md`, enrichi par `pre.009-fix.001`, ouvre `0.2.5 — Wallet foundation` avec threat model, interopérabilité `.kspwallet` et capacités indépendantes VIEW/OWNER.
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 — 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.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.

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 95 # version: 142
[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.2.0" version = "0.2.4"
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" }

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 24 --> <!-- version: 43 -->
# Roadmap KSP # Roadmap KSP
@@ -17,6 +17,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
- Une prerelease vise environ **15 à 20 minutes de travail effectif**. - 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**. - 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. - 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. - À 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.
@@ -39,24 +40,24 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
### Cadrage ### Cadrage
- [X] `0.2.0` — Audit bot3, ordre de `0.2.x`, architecture durable et prompt `0.2.1` stabilisés par `0.2.0-rel.001`. - [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.2.0-pre.001` — Méthode d'audit, cartographie initiale et matrice provisoire. - [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.2.0-pre.002` — Fixer l'ordre fonctionnel, la discipline de sizing, le pipeline RAW/CORE/DECODE/SPECIALIZED et préparer le prompt `0.2.1`.
- [X] `0.2.0-pre.003` — Audit de cohérence final : règles résiduelles supersédées corrigées, fiches `0.2.1+` complétées, TODO bot3 utiles préservés et prompt `0.2.1` finalisé.
- [X] `0.2.0-rel.001` — Publication stable du cadrage `0.2.x`; prochaine release : `0.2.1 — ksp-onchain-transport-lib / HTTP Solana foundation`.
### Releases fonctionnelles décidées/pressenties ### Releases fonctionnelles décidées/pressenties
- [ ] `0.2.1`Introduire `ksp-onchain-transport-lib` avec HTTP Solana/JSON-RPC, settings publics, document Config standard + adapter, pools, rôles, priorités, limites, retry/backoff et couverture complète de la documentation HTTP ciblée. - [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.
- [ ] `0.2.2`Introduire `ksp-wallet-lib`, le format `.kspwallet`, la gestion sûre des secrets et une architecture d'import/export extensible ; exclure `WalletPolicy`. - [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.
- [ ] `0.2.3`Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP, notamment pour afficher l'identité et le solde d'un wallet. - [X] `0.2.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`É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. - [X] `0.2.4`HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
- [ ] `0.2.5`Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client. - [ ] `0.2.5`Introduire `ksp-wallet-lib` et le format interopérable `.kspwallet` avec threat model offline, capacités indépendantes VIEW/OWNER, metadata protégées, signature, rotations de key slots et import/export extensible ; exclure `WalletPolicy`.
- [ ] `0.2.6`Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle. - [ ] `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`Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR. - [ ] `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`Introduire une petite application desk de visualisation des prix. - [ ] `0.2.8`Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
- [ ] `0.2.9`Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes. - [ ] `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-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet. - [ ] `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.
### Règles Transport pour toute la série ### Règles Transport pour toute la série

View File

@@ -61,4 +61,4 @@
"target_filters": [] "target_filters": []
} }
] ]
} }

View 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
}
}
]
}
]
}
]
}

View File

@@ -263,4 +263,4 @@
} }
} }
} }
} }

View 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"}
}
}
}
}
}

View File

@@ -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
View 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
}
}
]
}
]
}
]
}

View File

@@ -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]

View File

@@ -10,4 +10,4 @@
"core:default", "core:default",
"tracing:default" "tracing:default"
] ]
} }

View File

@@ -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;
} }
} }
} }

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -28,4 +28,4 @@
"frontend", "frontend",
"vite.config.ts" "vite.config.ts"
] ]
} }

View File

@@ -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;
}));
}
} }
} }
} }

View File

@@ -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

View File

@@ -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.

View File

@@ -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 ;

View File

@@ -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, §78 | | Environnement | `ConfigEnvironment`, `ConfigEnvironmentSource`, `ConfigEnvironmentValue`, `DEFAULT_DOTENV_PATH`, `DEFAULT_DOTENV_EXAMPLE_PATH` | §3, §78 |
| 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` | §67 | | Management | `ConfigManagement`, `ConfigManagedSource`, `ConfigDocumentChangeReport`, `ConfigEnvironmentReport`, `ConfigEnvironmentChangeReport` | §67 |
| 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`.

View 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";

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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,

View File

@@ -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.

View 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;

View File

@@ -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());
} }
} }

View File

@@ -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");
}

View 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);
}

View File

@@ -22,4 +22,4 @@
] ]
} }
] ]
} }

View 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
}
}
]
}
]
}
]
}

View File

@@ -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();

View 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("../..");
}

View 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\"] }"));
}

View 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());
}
}

View File

@@ -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

View 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

View File

@@ -0,0 +1,151 @@
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
<!-- version: 9 -->
# `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.4` complète la surface typée des **52 méthodes courantes** :
```text
0.2.1 foundation : 4
0.2.2 Accounts/Tokens/Cluster : 22
0.2.3 Transactions : 11
0.2.4 Blocks/Economics : 15
total : 52
```
`0.2.4` ajoute les dix wrappers Blocks et les cinq wrappers Economics. Les points sensibles restent notamment `getBlock` moderne + bare encoding legacy deprecated, les quatre `transactionDetails`, les versions transaction numériques génériques, `numRewardPartitions`, `commissionBps`, les overloads de `getBlocks`, les ranges de production, les valeurs d'inflation/minimum de délégation fournies par le runtime et les `null` positionnels de `getInflationReward`.
`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 final `0.2.4` agrège le réaudit 37/37 de `0.2.3` avec les 15 nouveaux wrappers et porte la preuve stable à **52/52**.
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
-> trois reads Transactions
-> getBlockHeight
-> getInflationRate/getStakeMinimumDelegation
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` ;
- [`../../docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](../../docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) — plan Blocks/Economics et compliance HTTP finale ;
- [`../../docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md`](../../docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md) — matrice finale validée `52/52 + 14/14` et audit `KSP-TRANSPORT-007` global ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — configuration standard HTTP.

View File

@@ -0,0 +1,201 @@
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
<!-- version: 9 -->
# 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.4` contient les **52 wrappers typés courants** : 4 foundation + 22 Accounts/Tokens/Cluster + 11 Transactions + 10 Blocks + 5 Economics. Les DTOs Transport conservent les `null`, options, overloads 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.
Exemples Blocks/Economics représentatifs :
```rust
let block_height = pool.get_block_height(&role, Some(&context)).await;
let inflation_rate = pool.get_inflation_rate(&role).await;
let stake_minimum = pool.get_stake_minimum_delegation(&role, Some(&context)).await;
```
`getBlock` possède également une forme bare-encoding legacy séparée et deprecated. Les valeurs Economics restent celles du runtime : le consumer ne doit pas supposer localement un taux d'inflation ou un minimum de délégation constant.
## 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 reste utile pour les extensions provider, les diagnostics et les méthodes hors registre standard, mais elle ne remplace jamais le wrapper typé d'une méthode HTTP standard désormais couverte par 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 d'Accounts/Tokens/Cluster, trois reads Transactions, puis des reads Blocks/Economics de la release stable `0.2.4` :
```bash
cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture
```
Il appelle `getAccountInfo`, `getTokenAccountsByOwner`, `getEpochInfo`, `getVoteAccounts`, puis `getLatestBlockhash`, `isBlockhashValid`, `getTransactionCount`, `getBlockHeight`, `getInflationRate` et `getStakeMinimumDelegation`. 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.

View File

@@ -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}
]

View File

@@ -0,0 +1,4 @@
[
{"commitment":[1,2,3,4],"totalStake":1000000000},
{"commitment":null,"totalStake":1000000000}
]

View File

@@ -0,0 +1,7 @@
{
"byIdentity": {
"11111111111111111111111111111111": [8,7],
"Vote111111111111111111111111111111111111111": [4,4]
},
"range": {"firstSlot":430000000,"lastSlot":430000099}
}

View File

@@ -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"}

View File

@@ -0,0 +1,20 @@
[
{
"previousBlockhash":"previous-a",
"blockhash":"block-a",
"parentSlot":1,
"blockTime":null,
"blockHeight":null
},
{
"previousBlockhash":"previous-b",
"blockhash":"block-b",
"parentSlot":2,
"transactions":null,
"signatures":null,
"rewards":null,
"numRewardPartitions":null,
"blockTime":null,
"blockHeight":null
}
]

View File

@@ -0,0 +1,36 @@
{
"previousBlockhash":"previous-blockhash-fixture",
"blockhash":"blockhash-fixture",
"parentSlot":430000122,
"transactions":[
{
"transaction":{"signatures":["signature-fixture"],"message":{"accountKeys":[],"recentBlockhash":"recent-fixture","instructions":[]}},
"meta":{"err":null,"fee":5000},
"version":1
},
{
"transaction":["AQID","base64"],
"meta":null
}
],
"rewards":[
{
"pubkey":"11111111111111111111111111111111",
"lamports":2500,
"postBalance":1000002500,
"rewardType":"Fee",
"commission":null,
"commissionBps":1234
},
{
"pubkey":"Vote111111111111111111111111111111111111111",
"lamports":1250,
"postBalance":2000001250,
"rewardType":"Voting",
"commission":5
}
],
"numRewardPartitions":4,
"blockTime":1787072400,
"blockHeight":410000000
}

View File

@@ -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": []
}
}
}
]

View File

@@ -0,0 +1,12 @@
[
"3Bxs4NN8M2Yn4TLb",
["AQIDBAUGBwg=", "base64"],
{
"signatures": ["5h6xBEauJ3PK6SWQkgLxHcQpWyRZyG7c4pUpK9fd2B2B7hRQ1S6F9J7BqvQPEmxQxK4DThfYF5f5f5f5f5f5f5f5"],
"message": {
"accountKeys": [],
"recentBlockhash": "11111111111111111111111111111111",
"instructions": []
}
}
]

View File

@@ -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}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":410000002},"value":null},"id":1}

View File

@@ -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}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"3.1.8","slot":123456789},"value":424242},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-accounts"],"accountKeys":[{"pubkey":"11111111111111111111111111111111","writable":true,"signer":true,"source":"transaction"}]},"meta":{"err":null,"status":{"Ok":null},"fee":5000,"preBalances":[10],"postBalances":[5]},"version":1}]},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":["3BxsFixtureBase58","base58"],"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":["AQID","base64"],"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":"3BxsFixtureLegacyBinary","meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","error":{"code":-32004,"message":"Block not available for slot 430000123"},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","error":{"code":-32015,"message":"Transaction version (1) is not supported by the requesting client. Please try the request again with maxSupportedTransactionVersion 1"},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-v1"],"message":{"accountKeys":[],"recentBlockhash":"recent-v1","instructions":[],"transactionConfig":{"priorityFee":1,"computeUnitLimit":200000}}},"meta":{"err":null,"fee":5000,"status":{"Ok":null},"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":1,"postBalance":2,"rewardType":"Fee","commission":null,"commissionBps":1234}]},"version":1},{"transaction":["AQID","base64"],"meta":null}]},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-full-no-rewards"],"message":{"accountKeys":[],"recentBlockhash":"recent-full-no-rewards","instructions":[]}},"meta":{"err":null,"fee":5000,"rewards":null},"version":1}]},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-json"],"message":{"accountKeys":[],"recentBlockhash":"recent-json","instructions":[]}},"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"rewards":[{"pubkey":"11111111111111111111111111111111","lamports":2500,"postBalance":1000002500,"rewardType":"Fee","commission":null,"commissionBps":1234},{"pubkey":"Vote111111111111111111111111111111111111111","lamports":1250,"postBalance":2000001250,"rewardType":"Voting","commission":5}],"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"transactions":[{"transaction":{"signatures":["sig-parsed"],"message":{"accountKeys":[],"recentBlockhash":"recent-parsed","instructions":[]}},"meta":{"err":null,"fee":5000},"version":"legacy"}]},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":null,"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-b","blockhash":"block-b","parentSlot":2,"transactions":null,"signatures":null,"rewards":null,"numRewardPartitions":null,"blockTime":null,"blockHeight":null},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-a","blockhash":"block-a","parentSlot":1,"blockTime":null,"blockHeight":null},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"previousBlockhash":"previous-blockhash-fixture","blockhash":"blockhash-fixture","parentSlot":430000122,"numRewardPartitions":4,"blockTime":1787072400,"blockHeight":410000000,"signatures":["sig-a","sig-b"]},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"commitment":[1,2,3,4],"totalStake":1000000000},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":410000000,"id":1}

View File

@@ -0,0 +1,11 @@
{
"jsonrpc":"2.0",
"result":{
"context":{"slot":430000123,"apiVersion":"4.2.1"},
"value":{
"byIdentity":{"not-a-pubkey":[8,7]},
"range":{"firstSlot":430000000,"lastSlot":430000099}
}
},
"id":1
}

View File

@@ -0,0 +1,11 @@
{
"jsonrpc":"2.0",
"result":{
"context":{"slot":430000123,"apiVersion":"4.2.1"},
"value":{
"byIdentity":{"11111111111111111111111111111111":[8,7]},
"range":{"firstSlot":430000000,"lastSlot":430000099}
}
},
"id":1
}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":null,"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":1787072400,"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":[],"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":[430000100,430000103,430000109],"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":[],"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":[430000200,430000201,430000205],"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":[{"pubkey":"not-a-pubkey","rpc":"127.0.0.1:8899"}],"id":1}

View File

@@ -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}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"absoluteSlot":430000001,"blockHeight":429900000,"epoch":995,"slotIndex":12345,"slotsInEpoch":432000,"transactionCount":null},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"firstNormalEpoch":0,"firstNormalSlot":0,"leaderScheduleSlotOffset":432000,"slotsPerEpoch":432000,"warmup":false},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid param: unable to decode message"},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000002},"value":null},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"context":{"apiVersion":"4.2.1","slot":431000001},"value":5000},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":250000,"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":"GH7ome3EiwEr7tu9JuTh2dpYWBJK3z69Xm1ZE3MEE6JC","id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":"ok","id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","error":{"code":-32008,"message":"No snapshot"},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"full":429990000,"incremental":null},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"identity":"invalid-identity"},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"identity":"ComputeBudget111111111111111111111111111111"},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"initial":0.07123456789,"terminal":0.0123456789,"taper":0.2718281828,"foundation":0.0042,"foundationTerm":6.25},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":{"total":0.0387654321,"validator":0.037654321,"foundation":0.0011111111,"epoch":913},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":[{"epoch":912,"effectiveSlot":431500010,"amount":777,"postBalance":1000777,"commission":5,"commissionBps":500},null,{"epoch":912,"effectiveSlot":431500010,"amount":777,"postBalance":1000777,"commission":5,"commissionBps":500}],"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":[],"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","error":{"code":-32017,"message":"Epoch rewards period still active at slot 431500000","data":{"slot":431500000,"currentBlockHeight":400000000,"rewardsCompleteBlockHeight":400000100}},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","error":{"code":-32016,"message":"Minimum context slot has not been reached","data":{"contextSlot":431000000}},"id":1}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":[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,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}

View File

@@ -0,0 +1 @@
{"jsonrpc":"2.0","result":[null],"id":1}

Some files were not shown because too many files have changed in this diff Show More