32 Commits

Author SHA1 Message Date
09aacc36ab v0.2.9-rel.001 2026-08-25 08:36:34 +02:00
15b129ad04 v0.2.9-pre.015 2026-08-25 08:35:16 +02:00
6ddf2b4995 v0.2.9-pre.014 2026-08-25 05:44:41 +02:00
dcabceb52f v0.2.9-pre.013-fix.004 2026-08-24 23:39:11 +02:00
51bcf870d3 v0.2.9-pre.013-fix.003 2026-08-24 23:13:49 +02:00
c6c5793c61 v0.2.9-pre.013-fix.002 2026-08-24 23:02:18 +02:00
d98ee736be v0.2.9-pre.013-fix.001 2026-08-24 22:48:33 +02:00
39dbd86f1e v0.2.9-pre.013 2026-08-24 21:13:38 +02:00
c938b2409e v0.2.9-pre.012-fix.001 2026-08-24 20:56:14 +02:00
5b30bb9948 v0.2.9-pre.012-fix.001 2026-08-24 20:56:04 +02:00
813a45385a v0.2.9-pre.012 2026-08-24 20:48:30 +02:00
21ed5f88da v0.2.9-pre.011 2026-08-24 19:53:45 +02:00
3198cc5666 v0.2.9-pre.010-fix.001 2026-08-24 18:53:10 +02:00
4568d94a62 v0.2.9-pre.010 2026-08-24 18:39:19 +02:00
d32d8de3a5 v0.2.9-pre.009-fix.001 2026-08-24 17:36:41 +02:00
a9fb6a7ac8 v0.2.9-pre.009 2026-08-24 17:26:27 +02:00
433e69272a v0.2.9-pre.008-fix.001 2026-08-24 17:10:34 +02:00
affa18a6e9 v0.2.9-pre.008 2026-08-24 16:16:48 +02:00
05aec88178 v0.2.9-pre.007 2026-08-24 14:10:57 +02:00
b32de48c0b v0.2.9-pre.006 2026-08-24 13:17:24 +02:00
15e3402e1c v0.2.9-pre.005-fix.001 2026-08-24 12:53:15 +02:00
dca0a4b809 v0.2.9-pre.005 2026-08-24 12:31:19 +02:00
20320dba7b v0.2.9-pre.004-fix.001 2026-08-24 12:09:48 +02:00
e4bbc78a41 v0.2.9-pre.004 2026-08-24 11:55:17 +02:00
feb9befb35 v0.2.9-pre.003-fix.001 2026-08-24 11:16:04 +02:00
a038194679 v0.2.9-pre.003 2026-08-24 11:10:59 +02:00
3172cda241 v0.2.9-pre.002-fix.002 2026-08-24 11:00:38 +02:00
61fba107ef v0.2.9-pre.002-fix.001 2026-08-24 10:05:05 +02:00
835de48cb7 v0.2.9-pre.002 2026-08-24 09:54:14 +02:00
21de590aea v0.2.9-pre.001-fix.002 2026-08-24 09:23:03 +02:00
3081bd1085 v0.2.9-pre.001-fix.001 2026-08-24 09:17:30 +02:00
bdcab1e1a7 v0.2.9-pre.001 2026-08-24 08:37:36 +02:00
101 changed files with 18278 additions and 418 deletions

View File

@@ -1,5 +1,5 @@
# file: .env.example # file: .env.example
# version: 6 # version: 8
# 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.
@@ -34,6 +34,14 @@ KSP_PUBLIC_SOLANA_MAINNET_WS_URL=wss://api.mainnet-beta.solana.com
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON. # 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 # KSP_SECRET_SOLANA_HTTP_URL=https://provider.example/?api-key=replace-me
# PublicNode Mainnet personal token used as secret x-token metadata by the committed Mainnet Yellowstone gRPC profile.
# Keep the real value only in the process environment or local .env; do not assume that a Testnet token is authorized on Mainnet.
# KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN=replace-me
# PublicNode Testnet personal token used as secret x-token metadata by the committed Testnet Yellowstone gRPC profile.
# Keep the real value only in the process environment or local .env; do not assume that a Mainnet token is authorized on Testnet.
# KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN=replace-me
# Helius API key used by the LaserStream WebSocket endpoint in config/examples/std.transport.example.json. # Helius API key used by the LaserStream WebSocket endpoint in config/examples/std.transport.example.json.
# Keep the real credential only in the process environment or local .env; never commit it. # Keep the real credential only in the process environment or local .env; never commit it.
# KSP_SECRET_HELIUS_API_KEY=replace-me # KSP_SECRET_HELIUS_API_KEY=replace-me

View File

@@ -1,10 +1,18 @@
<!-- file: CHANGELOG.md --> <!-- file: CHANGELOG.md -->
<!-- version: 12 --> <!-- version: 13 -->
# Changelog KSP # Changelog KSP
Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`. Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`.
## 0.2.9 — Yellowstone gRPC standard/provider-neutral — 2026-08-24
`0.2.9` ajoute à `ksp-onchain-transport-lib` un moteur Yellowstone gRPC partagé construit directement sur Tonic/Protobuf, avec `yellowstone-grpc-proto` comme contrat wire upstream et sans `yellowstone-grpc-client` dans le runtime KSP. La surface Solana standard reste provider-neutral et couvre les sept unary retenues (`SubscribeReplayInfo`, `Ping`, `GetLatestBlockhash`, `GetBlockHeight`, `GetSlot`, `IsBlockhashValid`, `GetVersion`), `Subscribe` bidi, les familles accounts/slots/transactions/transaction status/blocks/block meta/entry, les neuf variantes d'update standard retenues, les filtres/common fields actuels, le backpressure, les limites de messages, le half-close/shutdown, ainsi qu'un reconnect/replay prudent depuis le dernier slot observé. Aucune garantie exactly-once, lossless ou d'ordre global gapless n'est introduite ; gaps et duplicates restent observables. `SubscribeDeshred` reste explicitement hors du standard KSP de cette release.
Config passe à `std.transport` V3, backward-readable avec V1 HTTP et V2 HTTP+WebSocket, et sépare les axes `provider` / `protocol` ainsi que `metadata` / `secret_metadata` sans créer de dépendance Transport -> Config/env. PublicNode constitue la première intégration provider : profils Yellowstone Mainnet et Testnet sur `https://solana-yellowstone-grpc.publicnode.com:443` et `https://solana-testnet-yellowstone-grpc.publicnode.com:443`, avec authentification `x-token` résolue par Config depuis des variables `KSP_SECRET_*`. Le smoke Transport pur reçoit les credentials uniquement par stdin opérateur, n'en journalise aucun et valide en live `Subscribe -> Slot` sur les deux réseaux ; le même personal token a été utilisé avec succès pour Mainnet et Testnet, tout en conservant deux entrées Config distinctes par flexibilité opérationnelle.
La clôture conserve les surfaces antérieures HTTP (**52 current typed + 14 historiques**), WebSocket Solana standard (**9 familles / 18 opérations**) et Helius LaserStream WebSocket. Le gate final comprend **383/383** tests unit Transport, **49/49** public API, **43/43** release completeness, **4/4** doctests, `cargo test --workspace`, les canaris de dépendances, le smoke PublicNode live **2/2 PASS** et l'inspection des graphes Cargo (`yellowstone-grpc-proto 12.6.0`, `tonic` / `tonic-prost 0.14.6`, `prost` / `prost-types 0.14.4`). `prompts/015-V0_2_10_START_PROMPT.md` ouvre ensuite `0.2.10 — OrbitFlare Yellowstone gRPC` uniquement depuis le tag stable `v0.2.9`, avec audit provider actuel, threat model et sizing en `pre.001` avant toute extension provider lourde.
## 0.2.8 — Helius LaserStream WebSocket — 2026-08-23 ## 0.2.8 — Helius LaserStream WebSocket — 2026-08-23
`0.2.8` étend `ksp-onchain-transport-lib` avec une façade `HeliusLaserStreamWsSession` dédiée qui réutilise le même `WsSession` physique/actor que le WebSocket Solana standard, sans second client, socket, registry ou scheduler. La surface stable Helius réutilise les sept familles standard actuellement retenues (`account`, `logs`, `program`, `root`, `signature`, `slot`, `slotsUpdates`) et ajoute lextension typée `transactionSubscribe` / `transactionUnsubscribe`; `block` et `vote` restent absents de la façade Helius et `slotsUpdates` conserve son statut unstable. Le heartbeat provider est possédé par lactor partagé et émet un WebSocket Ping control frame toutes les 60 secondes uniquement pour `WsProtocolKind::HeliusLaserStream`. `0.2.8` étend `ksp-onchain-transport-lib` avec une façade `HeliusLaserStreamWsSession` dédiée qui réutilise le même `WsSession` physique/actor que le WebSocket Solana standard, sans second client, socket, registry ou scheduler. La surface stable Helius réutilise les sept familles standard actuellement retenues (`account`, `logs`, `program`, `root`, `signature`, `slot`, `slotsUpdates`) et ajoute lextension typée `transactionSubscribe` / `transactionUnsubscribe`; `block` et `vote` restent absents de la façade Helius et `slotsUpdates` conserve son statut unstable. Le heartbeat provider est possédé par lactor partagé et émet un WebSocket Ping control frame toutes les 60 secondes uniquement pour `WsProtocolKind::HeliusLaserStream`.

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 236 # version: 261
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"] members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
[workspace.package] [workspace.package]
version = "0.2.8" version = "0.2.9"
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,31 +15,35 @@ publish = false
[workspace.dependencies] [workspace.dependencies]
argon2 = { version = "^0.5", default-features = false } argon2 = { version = "^0.5", default-features = false }
base64 = { version = "^0.23" }
chacha20poly1305 = { version = "^0.11", default-features = false } chacha20poly1305 = { version = "^0.11", default-features = false }
chrono = { version = "^0.4", default-features = false }
directories = { version = "^6.0" } directories = { version = "^6.0" }
ed25519-dalek = { version = "^3.0", default-features = false } ed25519-dalek = { version = "^3.0", default-features = false }
getrandom = { version = "^0.4", default-features = false }
base64 = { version = "^0.23" }
fs2 = { version = "^0.4" } fs2 = { version = "^0.4" }
futures-util = { version = "^0.3", default-features = false } futures-util = { version = "^0.3", default-features = false }
serde = { version = "^1.0" } getrandom = { version = "^0.4", default-features = false }
serde_json = { version = "^1.0" } http = { version = "^1.5", default-features = false }
jsonschema = { version = "^0.51", default-features = false } jsonschema = { version = "^0.51", default-features = false }
reqwest = { version = "^0.13", default-features = false } reqwest = { version = "^0.13", default-features = false }
serde = { version = "^1.0" }
serde_json = { version = "^1.0" }
solana-keypair = { version = "^3.1", default-features = false } solana-keypair = { version = "^3.1", default-features = false }
solana-pubkey = { version = "^4.3", default-features = false } solana-pubkey = { version = "^4.3", default-features = false }
tauri = { version = "^2.11" }
tauri-build = { version = "^2.6" }
tauri-plugin-dialog = { version = "^2.7" }
tauri-plugin-tracing = { version = "^0.3" }
tempfile = { version = "^3.27" }
tracing = { version = "^0.1", default-features = false } tracing = { version = "^0.1", default-features = false }
tracing-subscriber = { version = "^0.3", default-features = false } 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 } tokio = { version = "^1.53", default-features = false }
tokio-tungstenite = { version = "^0.30", default-features = false } tokio-tungstenite = { version = "^0.30", default-features = false }
tempfile = { version = "^3.27" } tonic = { version = "^0.14", default-features = false }
chrono = { version = "^0.4", default-features = false } tonic-prost = { version = "^0.14", default-features = false }
tauri = { version = "^2.11" }
tauri-build = { version = "^2.6" }
tauri-plugin-dialog = { version = "^2.7" }
tauri-plugin-tracing = { version = "^0.3" }
ts-rs = { version = "^12.0" } ts-rs = { version = "^12.0" }
yellowstone-grpc-proto = { version = "^12.6", default-features = false }
zeroize = { version = "^1.9" } zeroize = { version = "^1.9" }
[workspace.lints.rust] [workspace.lints.rust]

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md --> <!-- file: ROADMAP.md -->
<!-- version: 83 --> <!-- version: 86 -->
# Roadmap KSP # Roadmap KSP
@@ -53,11 +53,22 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
- [X] `0.2.6``ksp-app-wallet-desk` + `.kspwallet` V2 stables : composition Config/Wallet/HTTP/Logging, lifecycle VIEW/OWNER, balance, administration/import/export, wire binaire V2, APIs multi-version, migration V1 -> V2 explicite et runtime Tauri packagé user-writable validés ; bundles Linux `.deb`/`.rpm`/`.AppImage` produits avant publication. Plan clôturé : `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`. - [X] `0.2.6``ksp-app-wallet-desk` + `.kspwallet` V2 stables : composition Config/Wallet/HTTP/Logging, lifecycle VIEW/OWNER, balance, administration/import/export, wire binaire V2, APIs multi-version, migration V1 -> V2 explicite et runtime Tauri packagé user-writable validés ; bundles Linux `.deb`/`.rpm`/`.AppImage` produits avant publication. Plan clôturé : `docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md`.
- [X] `0.2.7` — WebSocket Solana standard stable : 9 familles subscribe/unsubscribe typées (18/18 opérations), sessions physiques multiples explicites, subscriptions logiques typées, lifecycle/reconnect/resubscribe/backpressure/shutdown bornés, Config V2, non-régression HTTP 52+14, compliance finale, smoke WebSocket Devnet et audit de dépendances validés ; publication `rel.001` et prompt `0.2.8` prêts. - [X] `0.2.7` — WebSocket Solana standard stable : 9 familles subscribe/unsubscribe typées (18/18 opérations), sessions physiques multiples explicites, subscriptions logiques typées, lifecycle/reconnect/resubscribe/backpressure/shutdown bornés, Config V2, non-régression HTTP 52+14, compliance finale, smoke WebSocket Devnet et audit de dépendances validés ; publication `rel.001` et prompt `0.2.8` prêts.
- [X] `0.2.8` — Helius LaserStream WebSocket stable : façade provider dédiée sur lactor WebSocket partagé, sept familles standard réutilisées (`account/logs/program/root/signature/slot/slotsUpdates`) + `transactionSubscribe`/`transactionUnsubscribe`, `block/vote` absents, heartbeat Ping 60 s Helius-only, Config V2/secrets redacted, lifecycle adversarial, compliance HTTP 52+14 / Standard WS 18/18 et graphes Cargo finaux validés ; prompt `0.2.9` prêt. - [X] `0.2.8` — Helius LaserStream WebSocket stable : façade provider dédiée sur lactor WebSocket partagé, sept familles standard réutilisées (`account/logs/program/root/signature/slot/slotsUpdates`) + `transactionSubscribe`/`transactionUnsubscribe`, `block/vote` absents, heartbeat Ping 60 s Helius-only, Config V2/secrets redacted, lifecycle adversarial, compliance HTTP 52+14 / Standard WS 18/18 et graphes Cargo finaux validés ; prompt `0.2.9` prêt.
- [ ] `0.2.9` Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle. - [X] `0.2.9` — Yellowstone gRPC standard/provider-neutral stable : moteur Tonic/Protobuf KSP partagé, sept unary standard retenues, `Subscribe` bidi et neuf variantes dupdate, lifecycle/backpressure/reconnect/replay bornés sans promesse lossless, Config Transport V3 backward V1/V2 avec provider/protocol séparés, profils PublicNode Mainnet/Testnet authentifiés par `x-token`, smoke live `Subscribe -> Slot` 2/2 PASS et graphes Cargo finaux inspectés ; `SubscribeDeshred` reste hors scope.
- [ ] `0.2.10`Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR. - [ ] `0.2.10`Ajouter lintégration Yellowstone gRPC OrbitFlare sur le moteur/standard acquis, avec audit explicite des auth/capabilities/restrictions/extensions et Devnet comme cible gratuite prioritaire.
- [ ] `0.2.11`Introduire une petite application desk de visualisation/validation des prix offchain, puis intégrer cette capacité dans `ksp-app-wallet-desk` sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé. - [ ] `0.2.11`Ajouter lintégration Helius LaserStream gRPC sur le moteur Yellowstone partagé, avec audit de compatibilité/replay/auth/extensions, sans confondre cette surface avec LaserStream WebSocket.
- [ ] `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.12` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR.
- [ ] `0.2.13` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet. - [ ] `0.2.13` — Introduire une petite application desk de visualisation/validation des prix offchain, puis intégrer cette capacité dans `ksp-app-wallet-desk` sans dupliquer la logique de récupération/normalisation possédée par le composant spécialisé.
- [ ] `0.2.14` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes.
- [ ] `0.2.15` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet.
### TODO/IDEAS — providers Yellowstone non planifiés
- [ ] **TODO** — eRPC : réauditer accès, auth/IP policy, capabilities et produits complémentaires avant toute décision dimplémentation.
- [ ] **TODO** — Triton : réauditer la frontière Yellowstone upstream / extensions Triton, notamment Deshred et futures extensions.
- [ ] **TODO** — Alchemy : réauditer auth, replay, limites et capabilities Yellowstone avant toute intégration.
- [ ] **TODO** — QuickNode : réauditer auth, compression, `from_slot`, filtres et limites de plan.
- [ ] **TODO** — Chainstack : réauditer networks, auth, add-on et capabilities Yellowstone.
- [ ] **IDEAS** — Tatum, Shyft, Solinfra, NodeFlare et autres providers : conserver comme candidats exploratoires sans numéro de release ni engagement dimplémentation.
### Règles Transport pour toute la série ### Règles Transport pour toute la série

View File

@@ -1,13 +1,16 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:ksp:schema:std.transport:v2", "$id": "urn:ksp:schema:std.transport:v3",
"title": "KSP standard HTTP + WebSocket Transport configuration", "title": "KSP standard HTTP + WebSocket + Yellowstone gRPC Transport configuration",
"oneOf": [ "oneOf": [
{ {
"$ref": "#/$defs/documentV1" "$ref": "#/$defs/documentV1"
}, },
{ {
"$ref": "#/$defs/documentV2" "$ref": "#/$defs/documentV2"
},
{
"$ref": "#/$defs/documentV3"
} }
], ],
"$defs": { "$defs": {
@@ -455,6 +458,275 @@
} }
} }
} }
},
"grpcPositiveMs": {
"type": "integer",
"minimum": 1,
"maximum": 300000
},
"grpcCapacity": {
"type": "integer",
"minimum": 1,
"maximum": 65536
},
"grpcMessageSize": {
"type": "integer",
"minimum": 1,
"maximum": 536870912
},
"grpcReconnect": {
"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/grpcPositiveMs"
},
"max_backoff_ms": {
"$ref": "#/$defs/grpcPositiveMs"
}
}
},
"grpcReconnectOverride": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"max_retries": {
"type": "integer",
"minimum": 0,
"maximum": 100
},
"initial_backoff_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"max_backoff_ms": {
"$ref": "#/$defs/grpcPositiveMs"
}
}
},
"grpcSession": {
"type": "object",
"additionalProperties": false,
"required": [
"connect_timeout_ms",
"unary_timeout_ms",
"close_timeout_ms",
"reconnect",
"request_channel_capacity",
"update_channel_capacity",
"max_inbound_message_size_bytes",
"max_outbound_message_size_bytes"
],
"properties": {
"connect_timeout_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"unary_timeout_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"close_timeout_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"reconnect": {
"$ref": "#/$defs/grpcReconnect"
},
"request_channel_capacity": {
"$ref": "#/$defs/grpcCapacity"
},
"update_channel_capacity": {
"$ref": "#/$defs/grpcCapacity"
},
"max_inbound_message_size_bytes": {
"$ref": "#/$defs/grpcMessageSize"
},
"max_outbound_message_size_bytes": {
"$ref": "#/$defs/grpcMessageSize"
}
}
},
"grpcSessionOverride": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"connect_timeout_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"unary_timeout_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"close_timeout_ms": {
"$ref": "#/$defs/grpcPositiveMs"
},
"reconnect": {
"$ref": "#/$defs/grpcReconnectOverride"
},
"request_channel_capacity": {
"$ref": "#/$defs/grpcCapacity"
},
"update_channel_capacity": {
"$ref": "#/$defs/grpcCapacity"
},
"max_inbound_message_size_bytes": {
"$ref": "#/$defs/grpcMessageSize"
},
"max_outbound_message_size_bytes": {
"$ref": "#/$defs/grpcMessageSize"
}
}
},
"grpcMetadata": {
"type": "object",
"additionalProperties": false,
"required": [
"key",
"value"
],
"properties": {
"key": {
"type": "string",
"minLength": 1,
"maxLength": 128,
"pattern": "^[0-9a-z_.-]+$"
},
"value": {
"type": "string",
"maxLength": 8192
}
}
},
"grpcEndpoint": {
"type": "object",
"additionalProperties": false,
"required": [
"name",
"enabled",
"provider",
"cluster",
"protocol",
"url"
],
"properties": {
"name": {
"$ref": "#/$defs/descriptor"
},
"enabled": {
"type": "boolean"
},
"provider": {
"$ref": "#/$defs/descriptor"
},
"cluster": {
"$ref": "#/$defs/descriptor"
},
"protocol": {
"const": "solana_yellowstone"
},
"url": {
"type": "string",
"minLength": 1,
"maxLength": 8192
},
"metadata": {
"type": "array",
"maxItems": 64,
"items": {
"$ref": "#/$defs/grpcMetadata"
}
},
"secret_metadata": {
"type": "array",
"maxItems": 64,
"items": {
"$ref": "#/$defs/grpcMetadata"
}
},
"session": {
"$ref": "#/$defs/grpcSessionOverride"
}
}
},
"profileV3": {
"type": "object",
"additionalProperties": false,
"required": [
"profile_id",
"endpoints",
"ws_endpoints"
],
"properties": {
"profile_id": {
"$ref": "#/$defs/profileId"
},
"endpoints": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/httpEndpoint"
}
},
"ws_endpoints": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/wsEndpoint"
}
},
"grpc_endpoints": {
"type": "array",
"minItems": 1,
"maxItems": 128,
"items": {
"$ref": "#/$defs/grpcEndpoint"
}
}
}
},
"documentV3": {
"type": "object",
"additionalProperties": false,
"required": [
"format_version",
"retry",
"ws_defaults",
"grpc_defaults",
"default_profile",
"profiles"
],
"properties": {
"format_version": {
"const": 3
},
"retry": {
"$ref": "#/$defs/retry"
},
"ws_defaults": {
"$ref": "#/$defs/wsSession"
},
"grpc_defaults": {
"$ref": "#/$defs/grpcSession"
},
"default_profile": {
"$ref": "#/$defs/profileId"
},
"profiles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/profileV3"
}
}
}
} }
} }
} }

View File

@@ -1,5 +1,5 @@
{ {
"format_version": 2, "format_version": 3,
"retry": { "retry": {
"max_retries": 2, "max_retries": 2,
"initial_backoff_ms": 100, "initial_backoff_ms": 100,
@@ -22,6 +22,20 @@
"max_frame_size_bytes": 16777216, "max_frame_size_bytes": 16777216,
"max_write_buffer_size_bytes": 1048576 "max_write_buffer_size_bytes": 1048576
}, },
"grpc_defaults": {
"connect_timeout_ms": 10000,
"unary_timeout_ms": 10000,
"close_timeout_ms": 5000,
"reconnect": {
"max_retries": 5,
"initial_backoff_ms": 250,
"max_backoff_ms": 5000
},
"request_channel_capacity": 128,
"update_channel_capacity": 256,
"max_inbound_message_size_bytes": 67108864,
"max_outbound_message_size_bytes": 67108864
},
"default_profile": "devnet_public", "default_profile": "devnet_public",
"profiles": [ "profiles": [
{ {
@@ -105,6 +119,120 @@
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}" "url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
} }
] ]
},
{
"profile_id": "publicnode_mainnet",
"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
}
}
]
}
],
"ws_endpoints": [
{
"name": "solana_mainnet_public_ws",
"enabled": true,
"provider": "solana-public",
"cluster": "mainnet-beta",
"kind": "solana_standard",
"url": "${KSP_PUBLIC_SOLANA_MAINNET_WS_URL:-wss://api.mainnet-beta.solana.com}"
}
],
"grpc_endpoints": [
{
"name": "publicnode_solana_mainnet_yellowstone",
"enabled": true,
"provider": "publicnode",
"cluster": "mainnet-beta",
"protocol": "solana_yellowstone",
"url": "https://solana-yellowstone-grpc.publicnode.com:443",
"secret_metadata": [
{
"key": "x-token",
"value": "${KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN}"
}
]
}
]
},
{
"profile_id": "publicnode_testnet",
"endpoints": [
{
"name": "solana_testnet_public",
"enabled": true,
"provider": "solana-public",
"cluster": "testnet",
"url": "https://api.testnet.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
}
}
]
}
],
"ws_endpoints": [
{
"name": "solana_testnet_public_ws",
"enabled": true,
"provider": "solana-public",
"cluster": "testnet",
"kind": "solana_standard",
"url": "wss://api.testnet.solana.com"
}
],
"grpc_endpoints": [
{
"name": "publicnode_solana_testnet_yellowstone",
"enabled": true,
"provider": "publicnode",
"cluster": "testnet",
"protocol": "solana_yellowstone",
"url": "https://solana-testnet-yellowstone-grpc.publicnode.com:443",
"secret_metadata": [
{
"key": "x-token",
"value": "${KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN}"
}
]
}
]
} }
] ]
} }

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-app-config-desk/Cargo.toml # file: crates/ksp-app-config-desk/Cargo.toml
# version: 8 # version: 9
[package] [package]
name = "ksp-app-config-desk" name = "ksp-app-config-desk"
@@ -22,6 +22,7 @@ path = "src/main.rs"
tauri-build.workspace = true tauri-build.workspace = true
[dependencies] [dependencies]
chrono = { workspace = true, features = ["std", "now"] }
fs2.workspace = true 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" }
@@ -30,7 +31,6 @@ 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, features = ["std", "now"] }
tokio = { workspace = true, features = ["time"] } tokio = { workspace = true, features = ["time"] }
ts-rs.workspace = true ts-rs.workspace = true

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-config-lib/Cargo.toml # file: crates/ksp-config-lib/Cargo.toml
# version: 7 # version: 8
[package] [package]
name = "ksp-config-lib" name = "ksp-config-lib"
@@ -9,12 +9,12 @@ repository.workspace = true
[dependencies] [dependencies]
directories.workspace = true directories.workspace = true
jsonschema.workspace = true
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" }
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" } ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
jsonschema.workspace = true
[dev-dependencies] [dev-dependencies]
tempfile.workspace = true tempfile.workspace = true

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/README.md --> <!-- file: crates/ksp-config-lib/README.md -->
<!-- version: 8 --> <!-- version: 9 -->
# ksp-config-lib # ksp-config-lib
@@ -81,7 +81,7 @@ Un secret reste accessible au runtime ou au management lorsqu'un consumer autori
Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées. Les méthodes `reveal_*` constituent un opt-in explicite au réel. L'authentification/autorisation de l'utilisateur humain appartient à l'application appelante et les valeurs retournées par ces méthodes ne doivent jamais être journalisées.
Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport les accepte pour les URLs HTTP et WebSocket : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON. Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configuration effective. Le document Transport accepte les valeurs secrètes pour les URLs HTTP/WebSocket et, en V3, pour `grpc_endpoints[].secret_metadata[]` : la valeur réelle est transmise au runtime légitime, tandis que la projection sûre et les `Debug` restent redacted. Les metadata gRPC publiques et secrètes sont séparées et leur provenance Config est contrôlée avant mapping. `std.wallet` refuse également toute sensibilité `Secret` pour `wallets_directory`/`wallets_subdirectory`; les passwords Wallet restent un autre flux Config et ne sont jamais stockés dans ce JSON.
## Documentation ## Documentation
@@ -89,7 +89,7 @@ Le document Logging refuse les valeurs de sensibilité `Secret` dans sa configur
- [`TODO.md`](TODO.md) — points explicitement différés ; - [`TODO.md`](TODO.md) — points explicitement différés ;
- [`../../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) — document standard Logging ; - [`../../config/std.logging.json`](../../config/std.logging.json) — document standard Logging ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — document standard Transport V2 HTTP + WebSocket, avec lecture backward du V1 HTTP-only ; - [`../../config/std.transport.json`](../../config/std.transport.json) — document standard Transport V3 HTTP + WebSocket + Yellowstone gRPC, avec lecture backward des V1/V2 ;
- [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ; - [`../../config/std.wallet.json`](../../config/std.wallet.json) — racine Wallet globale et sous-répertoire optionnel par profil ;
- [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Transport/Wallet de Wallet Desk ; - [`../../config/composite.ksp-app-wallet-desk.json`](../../config/composite.ksp-app-wallet-desk.json) — composition Logging/Transport/Wallet de Wallet Desk ;
- [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime. - [`../../.env.example`](../../.env.example) — inventaire versionné des variables d'environnement runtime.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-config-lib/USAGE.md --> <!-- file: crates/ksp-config-lib/USAGE.md -->
<!-- version: 11 --> <!-- version: 12 -->
# Utilisation de ksp-config-lib # Utilisation de ksp-config-lib
@@ -125,9 +125,9 @@ 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 + WebSocket depuis Config ### 4.1 Construire le Transport HTTP + WebSocket + Yellowstone gRPC depuis Config
Config possède également l'adapter du document `std.transport` vers le contrat runtime de `ksp-onchain-transport-lib` : Config possède l'adapter du document `std.transport` vers les contrats runtime de `ksp-onchain-transport-lib` :
```rust ```rust
let transport = match engine.load_resolved_transport_config(std::option::Option::None, &environment) { let transport = match engine.load_resolved_transport_config(std::option::Option::None, &environment) {
@@ -137,18 +137,30 @@ let transport = match engine.load_resolved_transport_config(std::option::Option:
let http_settings = transport.http_settings(); let http_settings = transport.http_settings();
let ws_settings = transport.ws_settings(); let ws_settings = transport.ws_settings();
let _ = (http_settings, ws_settings); let grpc_settings = transport.grpc_settings();
let _ = (http_settings, ws_settings, grpc_settings);
``` ```
`std.transport` V2 conserve `retry` et `profiles[].endpoints[]` pour HTTP, ajoute `ws_defaults` et `profiles[].ws_endpoints[]`, puis accepte les protocoles WebSocket `kind = "solana_standard"` et `kind = "helius_laserstream"`. Ce second discriminateur appartient exclusivement au namespace WebSocket et mappe vers `WsProtocolKind::HeliusLaserStream`; il ne préfigure aucun contrat LaserStream gRPC. Un `ws_endpoints[].session` optionnel surcharge seulement les paramètres génériques de `WsSessionSettings`. `std.transport` V3 conserve intégralement les shapes V1/V2, ajoute `grpc_defaults` au niveau global et permet `profiles[].grpc_endpoints[]`. Les profils V3 peuvent rester HTTP + WebSocket seulement : l'absence de `grpc_endpoints` mappe vers `None` et n'invente aucun endpoint. La lecture V1 HTTP-only et V2 HTTP + WebSocket reste stricte et backward-compatible.
Pour Helius LaserStream WebSocket, l'exemple versionné couvre explicitement les deux réseaux supportés par ce contrat : mainnet via `wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}` et devnet via `wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}`. Les deux réseaux vivent dans des profils Config distincts afin de ne pas mélanger des clusters dans un même profil logique. Config reste l'unique propriétaire de `KSP_SECRET_HELIUS_API_KEY` : il résout la clé dans l'URL effective et transmet au Transport un `WsEndpointUrl` utilisable au runtime. Dans `safe_value`, Config conserve les segments littéraux non sensibles d'une chaîne composée et remplace uniquement chaque segment secret par `********` ; les projections deviennent donc respectivement `wss://mainnet.helius-rpc.com/?api-key=********` et `wss://devnet.helius-rpc.com/?api-key=********`. Les représentations `Debug` restent sûres et n'exposent jamais la clé réelle. Transport ne lit jamais directement l'environnement. Un endpoint gRPC V3 sépare explicitement :
Le même schema enregistré conserve la lecture stricte du V1 historique : dans ce cas `http_settings()` reste disponible et `ws_settings()` retourne `None`. Aucun `WsTransportSettings` vide n'est inventé pour simuler l'absence de WebSocket. ```text
provider = description de l'opérateur/exécution, par exemple publicnode
protocol = solana_yellowstone
metadata = metadata non secrète
secret_metadata = metadata dont la valeur doit avoir une provenance KSP_SECRET_*/KSPB_SECRET_*
```
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. Les URLs HTTP et WebSocket peuvent provenir de `KSP_PUBLIC_*` ou de `KSP_SECRET_*`; dans ce dernier cas la valeur réelle reste disponible au runtime Transport, mais `ResolvedTransportConfig::effective().safe_value()` et les représentations `Debug` sont redacted. `protocol` n'est pas un nouveau `WsProtocolKind` et ne transforme pas PublicNode en protocole. Config résout les placeholders, vérifie la classe de sensibilité des metadata puis construit `YellowstoneGrpcTransportSettings`. Transport ne lit jamais l'environnement. Les valeurs de `secret_metadata` sont disponibles au runtime mais redacted dans `safe_value` et dans les représentations `Debug`.
La dépendance reste unidirectionnelle : Config connaît les contrats Transport pour les construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP. Le provider affiche `solana-yellowstone-grpc.publicnode.com:443` ; le profil versionné `publicnode_mainnet` le représente sous la forme URL TLS requise par Transport, `https://solana-yellowstone-grpc.publicnode.com:443`, sans credential. Aucun hostname Testnet n'est versionné tant que sa valeur exacte n'est pas confirmée depuis une source opérateur/officielle.
L'accesseur historique `into_transport_settings()` conserve volontairement son tuple `(HTTP, Option<WS>)`. Un consumer ayant besoin des trois backends utilise `into_all_transport_settings()` ou les accesseurs séparés afin de ne pas casser silencieusement les consumers V2.
Les protocoles WebSocket restent `kind = "solana_standard"` et `kind = "helius_laserstream"`. Helius LaserStream WebSocket conserve ses URLs Config-owned et sa clé `KSP_SECRET_HELIUS_API_KEY`; cette surface est indépendante de Yellowstone gRPC.
Les scalaires `*_ms` restent des valeurs Config et sont convertis en `std::time::Duration` par l'adapter. La dépendance reste unidirectionnelle : Config connaît les contrats Transport pour les construire ; Transport ne connaît ni Config, ni `.env`, ni les variables KSP.
### 4.2 Résoudre le répertoire Wallet depuis Config ### 4.2 Résoudre le répertoire Wallet depuis Config

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/lib.rs // file: crates/ksp-config-lib/src/lib.rs
// version: 17 // version: 18
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -9,8 +9,9 @@
//! //!
//! 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. Standard Logging, HTTP Transport and Wallet documents map explicitly to their runtime consumer contracts, while the management surface //! provenance. Standard Logging, on-chain Transport (HTTP/WebSocket/Yellowstone gRPC) and Wallet documents map explicitly to their runtime consumer
//! provides typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic JSON/`.env` persistence. //! contracts, while the management surface provides typed Logging mutation, safe environment reports, explicit privileged reveal calls and atomic
//! JSON/`.env` persistence.
mod bootstrap; mod bootstrap;
mod composite; mod composite;
@@ -154,9 +155,9 @@ 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 + WebSocket Transport configuration document. /// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME; pub use self::registry::DEFAULT_STD_TRANSPORT_FILENAME;
/// Default physical filename for the standard HTTP + WebSocket Transport JSON Schema document. /// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME; pub use self::registry::DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME;
/// Default physical filename for the standard Wallet configuration document. /// Default physical filename for the standard Wallet configuration document.
pub use self::registry::DEFAULT_STD_WALLET_FILENAME; pub use self::registry::DEFAULT_STD_WALLET_FILENAME;
@@ -168,13 +169,13 @@ pub use self::registry::FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK;
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 + WebSocket Transport JSON Schema document. /// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT; pub use self::registry::FILE_ID_SCHEMA_STD_TRANSPORT;
/// Logical file identifier for the standard Wallet JSON Schema document. /// Logical file identifier for the standard Wallet JSON Schema document.
pub use self::registry::FILE_ID_SCHEMA_STD_WALLET; pub use self::registry::FILE_ID_SCHEMA_STD_WALLET;
/// 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 + WebSocket Transport configuration document. /// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
pub use self::registry::FILE_ID_STD_TRANSPORT; pub use self::registry::FILE_ID_STD_TRANSPORT;
/// Logical file identifier for the standard Wallet configuration document. /// Logical file identifier for the standard Wallet configuration document.
pub use self::registry::FILE_ID_STD_WALLET; pub use self::registry::FILE_ID_STD_WALLET;
@@ -188,7 +189,7 @@ 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 Transport configuration mapped to HTTP and optional WebSocket runtime settings. /// Effective standard Transport configuration mapped to HTTP plus optional WebSocket and Yellowstone gRPC runtime settings.
pub use self::transport::ResolvedTransportConfig; pub use self::transport::ResolvedTransportConfig;
/// Effective standard Wallet configuration resolved to validated filesystem roots. /// Effective standard Wallet configuration resolved to validated filesystem roots.
pub use self::wallet::ResolvedWalletConfig; pub use self::wallet::ResolvedWalletConfig;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/registry.rs // file: crates/ksp-config-lib/src/registry.rs
// version: 9 // version: 10
/// 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";
@@ -11,9 +11,9 @@ pub const DEFAULT_COMPOSITE_SCHEMA_FILENAME: &str = "composite.schema.json";
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 + WebSocket Transport configuration document. /// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json"; pub const DEFAULT_STD_TRANSPORT_FILENAME: &str = "std.transport.json";
/// Default physical filename for the standard HTTP + WebSocket Transport JSON Schema document. /// Default physical filename for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json"; pub const DEFAULT_STD_TRANSPORT_SCHEMA_FILENAME: &str = "std.transport.schema.json";
/// Default physical filename for the standard Wallet configuration document. /// Default physical filename for the standard Wallet configuration document.
pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json"; pub const DEFAULT_STD_WALLET_FILENAME: &str = "std.wallet.json";
@@ -25,13 +25,13 @@ pub const FILE_ID_COMPOSITE_KSP_APP_WALLET_DESK: &str = "cfg.composite.ksp-app-w
pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite"; pub const FILE_ID_SCHEMA_COMPOSITE: &str = "schema.composite";
/// 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 + WebSocket Transport JSON Schema document. /// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport JSON Schema document.
pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport"; pub const FILE_ID_SCHEMA_STD_TRANSPORT: &str = "schema.std.transport";
/// Logical file identifier for the standard Wallet JSON Schema document. /// Logical file identifier for the standard Wallet JSON Schema document.
pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet"; pub const FILE_ID_SCHEMA_STD_WALLET: &str = "schema.std.wallet";
/// Logical file identifier for the standard Logging configuration document. /// Logical file identifier for the standard Logging configuration document.
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 HTTP + WebSocket Transport configuration document. /// Logical file identifier for the standard HTTP + WebSocket + Yellowstone gRPC Transport configuration document.
pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport"; pub const FILE_ID_STD_TRANSPORT: &str = "cfg.std.transport";
/// Logical file identifier for the standard Wallet configuration document. /// Logical file identifier for the standard Wallet configuration document.
pub const FILE_ID_STD_WALLET: &str = "cfg.std.wallet"; pub const FILE_ID_STD_WALLET: &str = "cfg.std.wallet";

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-config-lib/src/transport.rs // file: crates/ksp-config-lib/src/transport.rs
// version: 4 // version: 5
/// Effective standard on-chain Transport configuration resolved from Config and mapped to HTTP and optional WebSocket runtime contracts. /// Effective standard on-chain Transport configuration resolved from Config and mapped to HTTP plus optional WebSocket and Yellowstone gRPC runtime contracts.
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
pub struct ResolvedTransportConfig { pub struct ResolvedTransportConfig {
file_id: crate::ConfigFileId, file_id: crate::ConfigFileId,
@@ -11,6 +11,7 @@ pub struct ResolvedTransportConfig {
effective: crate::ResolvedConfigJson, effective: crate::ResolvedConfigJson,
settings: ksp_onchain_transport_lib::HttpTransportSettings, settings: ksp_onchain_transport_lib::HttpTransportSettings,
ws_settings: std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>, ws_settings: std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>,
grpc_settings: std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings>,
} }
impl ResolvedTransportConfig { impl ResolvedTransportConfig {
@@ -61,7 +62,7 @@ impl ResolvedTransportConfig {
return &self.settings; return &self.settings;
} }
/// Returns validated WebSocket Transport settings when the selected document uses format V2. /// Returns validated WebSocket Transport settings when the selected document uses format V2 or V3.
/// ///
/// Backward-compatible V1 HTTP-only documents return [`std::option::Option::None`]. /// Backward-compatible V1 HTTP-only documents return [`std::option::Option::None`].
#[must_use] #[must_use]
@@ -69,19 +70,42 @@ impl ResolvedTransportConfig {
return self.ws_settings.as_ref(); return self.ws_settings.as_ref();
} }
/// Returns validated Yellowstone gRPC Transport settings when the selected V3 profile declares gRPC endpoints.
///
/// V1/V2 documents and V3 profiles without `grpc_endpoints` return [`std::option::Option::None`].
#[must_use]
pub fn grpc_settings(&self) -> std::option::Option<&ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings> {
return self.grpc_settings.as_ref();
}
/// Consumes this resolved Config and returns the mapped runtime HTTP Transport settings. /// Consumes this resolved Config and returns the mapped runtime HTTP Transport settings.
#[must_use] #[must_use]
pub fn into_settings(self) -> ksp_onchain_transport_lib::HttpTransportSettings { pub fn into_settings(self) -> ksp_onchain_transport_lib::HttpTransportSettings {
return self.settings; return self.settings;
} }
/// Consumes this resolved Config and returns both HTTP and optional WebSocket runtime settings. /// Consumes this resolved Config and returns HTTP plus optional WebSocket runtime settings.
///
/// This compatibility accessor intentionally preserves the V2 tuple shape. Use [`Self::into_all_transport_settings`] when Yellowstone gRPC settings are
/// also required.
#[must_use] #[must_use]
pub fn into_transport_settings( pub fn into_transport_settings(
self, self,
) -> (ksp_onchain_transport_lib::HttpTransportSettings, std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>) { ) -> (ksp_onchain_transport_lib::HttpTransportSettings, std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>) {
return (self.settings, self.ws_settings); return (self.settings, self.ws_settings);
} }
/// Consumes this resolved Config and returns HTTP plus optional WebSocket and Yellowstone gRPC runtime settings.
#[must_use]
pub fn into_all_transport_settings(
self,
) -> (
ksp_onchain_transport_lib::HttpTransportSettings,
std::option::Option<ksp_onchain_transport_lib::WsTransportSettings>,
std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings>,
) {
return (self.settings, self.ws_settings, self.grpc_settings);
}
} }
impl std::fmt::Debug for ResolvedTransportConfig { impl std::fmt::Debug for ResolvedTransportConfig {
@@ -94,15 +118,17 @@ impl std::fmt::Debug for ResolvedTransportConfig {
.field("selection_source", &self.selection_source) .field("selection_source", &self.selection_source)
.field("effective", &self.effective) .field("effective", &self.effective)
.field("has_ws_settings", &self.ws_settings.is_some()) .field("has_ws_settings", &self.ws_settings.is_some())
.field("has_grpc_settings", &self.grpc_settings.is_some())
.finish_non_exhaustive(); .finish_non_exhaustive();
} }
} }
impl crate::ConfigDocumentEngine { impl crate::ConfigDocumentEngine {
/// Loads the standard Transport document, selects a profile, resolves environment placeholders and maps HTTP plus optional WebSocket runtime settings. /// Loads the standard Transport document, selects a profile, resolves environment placeholders and maps HTTP plus optional WebSocket and Yellowstone
/// gRPC runtime settings.
/// ///
/// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. Secret endpoint URLs are allowed /// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. Secret endpoint URLs are allowed
/// because Transport URL wrappers own runtime redaction. V1 documents remain HTTP-only; V2 documents require WebSocket defaults and endpoints. /// because Transport URL wrappers own runtime redaction. V1 remains HTTP-only, V2 adds WebSocket and V3 can additionally map Yellowstone gRPC.
pub fn load_resolved_transport_config( pub fn load_resolved_transport_config(
&self, &self,
requested_profile: std::option::Option<&str>, requested_profile: std::option::Option<&str>,
@@ -121,7 +147,8 @@ impl crate::ConfigDocumentEngine {
return resolve_transport_profile(&profile, environment); return resolve_transport_profile(&profile, environment);
} }
/// Maps an already resolved standard Transport profile to HTTP plus optional WebSocket runtime adapters while preserving selection provenance. /// Maps an already resolved standard Transport profile to HTTP plus optional WebSocket and Yellowstone gRPC runtime adapters while preserving
/// selection provenance.
/// ///
/// This entry point is intended for profiles selected by a composite. The profile must reference `cfg.std.transport`. /// This entry point is intended for profiles selected by a composite. The profile must reference `cfg.std.transport`.
pub fn resolve_transport_config_profile( pub fn resolve_transport_config_profile(
@@ -148,9 +175,13 @@ struct EffectiveTransportSource {
retry: EffectiveRetrySource, retry: EffectiveRetrySource,
#[serde(default)] #[serde(default)]
ws_defaults: std::option::Option<EffectiveWsSessionSource>, ws_defaults: std::option::Option<EffectiveWsSessionSource>,
#[serde(default)]
grpc_defaults: std::option::Option<EffectiveGrpcSessionSource>,
endpoints: std::vec::Vec<EffectiveEndpointSource>, endpoints: std::vec::Vec<EffectiveEndpointSource>,
#[serde(default)] #[serde(default)]
ws_endpoints: std::option::Option<std::vec::Vec<EffectiveWsEndpointSource>>, ws_endpoints: std::option::Option<std::vec::Vec<EffectiveWsEndpointSource>>,
#[serde(default)]
grpc_endpoints: std::option::Option<std::vec::Vec<EffectiveGrpcEndpointSource>>,
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@@ -255,6 +286,72 @@ struct EffectiveWsReconnectOverrideSource {
max_backoff_ms: std::option::Option<u64>, max_backoff_ms: std::option::Option<u64>,
} }
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveGrpcReconnectSource {
max_retries: u32,
initial_backoff_ms: u64,
max_backoff_ms: u64,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveGrpcReconnectOverrideSource {
max_retries: std::option::Option<u32>,
initial_backoff_ms: std::option::Option<u64>,
max_backoff_ms: std::option::Option<u64>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveGrpcSessionSource {
connect_timeout_ms: u64,
unary_timeout_ms: u64,
close_timeout_ms: u64,
reconnect: EffectiveGrpcReconnectSource,
request_channel_capacity: usize,
update_channel_capacity: usize,
max_inbound_message_size_bytes: usize,
max_outbound_message_size_bytes: usize,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveGrpcSessionOverrideSource {
connect_timeout_ms: std::option::Option<u64>,
unary_timeout_ms: std::option::Option<u64>,
close_timeout_ms: std::option::Option<u64>,
reconnect: std::option::Option<EffectiveGrpcReconnectOverrideSource>,
request_channel_capacity: std::option::Option<usize>,
update_channel_capacity: std::option::Option<usize>,
max_inbound_message_size_bytes: std::option::Option<usize>,
max_outbound_message_size_bytes: std::option::Option<usize>,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveGrpcMetadataSource {
key: String,
value: String,
}
#[derive(serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct EffectiveGrpcEndpointSource {
name: String,
enabled: bool,
provider: String,
cluster: String,
protocol: String,
url: String,
#[serde(default)]
metadata: std::vec::Vec<EffectiveGrpcMetadataSource>,
#[serde(default)]
secret_metadata: std::vec::Vec<EffectiveGrpcMetadataSource>,
#[serde(default)]
session: std::option::Option<EffectiveGrpcSessionOverrideSource>,
}
fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedTransportConfig> { fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result<ResolvedTransportConfig> {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapping standard Transport Config profile"); ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapping standard Transport Config profile");
let effective = profile.resolve_effective_environment_detailed(environment); let effective = profile.resolve_effective_environment_detailed(environment);
@@ -294,16 +391,26 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
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 grpc_settings = map_optional_grpc_settings(format_version, source.grpc_defaults, source.grpc_endpoints, &effective, profile);
let grpc_settings = match grpc_settings {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let ws_endpoint_count = match ws_settings.as_ref() { let ws_endpoint_count = match ws_settings.as_ref() {
std::option::Option::Some(value) => value.endpoints().len(), std::option::Option::Some(value) => value.endpoints().len(),
std::option::Option::None => 0_usize, std::option::Option::None => 0_usize,
}; };
let grpc_endpoint_count = match grpc_settings.as_ref() {
std::option::Option::Some(value) => value.endpoints().len(),
std::option::Option::None => 0_usize,
};
ksp_logging_lib::debug!( ksp_logging_lib::debug!(
target: crate::TRACING_TARGET, target: crate::TRACING_TARGET,
profile_id = profile.profile_id(), profile_id = profile.profile_id(),
format_version, format_version,
http_endpoint_count = settings.endpoints().len(), http_endpoint_count = settings.endpoints().len(),
ws_endpoint_count, ws_endpoint_count,
grpc_endpoint_count,
"mapped standard Transport Config to runtime settings" "mapped standard Transport Config to runtime settings"
); );
return std::result::Result::Ok(ResolvedTransportConfig { return std::result::Result::Ok(ResolvedTransportConfig {
@@ -314,6 +421,7 @@ fn resolve_transport_profile(profile: &crate::ResolvedConfigProfile, environment
effective, effective,
settings, settings,
ws_settings, ws_settings,
grpc_settings,
}); });
} }
@@ -328,18 +436,22 @@ fn map_optional_ws_settings(
if defaults.is_some() || sources.is_some() { if defaults.is_some() || sources.is_some() {
std::result::Result::Err(effective_error(profile, "Transport V1 must remain HTTP-only")) std::result::Result::Err(effective_error(profile, "Transport V1 must remain HTTP-only"))
} else { } else {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, profile_id = profile.profile_id(), "mapped backward-compatible Transport V1 without WebSocket settings"); ksp_logging_lib::trace!(
target: crate::TRACING_TARGET,
profile_id = profile.profile_id(),
"mapped backward-compatible Transport V1 without WebSocket settings"
);
std::result::Result::Ok(std::option::Option::None) std::result::Result::Ok(std::option::Option::None)
} }
}, },
2 => { 2 | 3 => {
let defaults = match defaults { let defaults = match defaults {
std::option::Option::Some(value) => value, std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2 requires ws_defaults")), std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2/V3 requires ws_defaults")),
}; };
let sources = match sources { let sources = match sources {
std::option::Option::Some(value) => value, std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2 profile requires ws_endpoints")), std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V2/V3 profile requires ws_endpoints")),
}; };
let endpoints = map_ws_endpoints(sources, &defaults, profile); let endpoints = map_ws_endpoints(sources, &defaults, profile);
let endpoints = match endpoints { let endpoints = match endpoints {
@@ -360,6 +472,283 @@ fn map_optional_ws_settings(
}; };
} }
fn map_optional_grpc_settings(
format_version: u32,
defaults: std::option::Option<EffectiveGrpcSessionSource>,
sources: std::option::Option<std::vec::Vec<EffectiveGrpcEndpointSource>>,
effective: &crate::ResolvedConfigJson,
profile: &crate::ResolvedConfigProfile,
) -> ksp_core_lib::Result<std::option::Option<ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings>> {
return match format_version {
1 | 2 => {
if defaults.is_some() || sources.is_some() {
std::result::Result::Err(effective_error(profile, "Transport V1/V2 must not define Yellowstone gRPC settings"))
} else {
std::result::Result::Ok(std::option::Option::None)
}
},
3 => {
let defaults = match defaults {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(effective_error(profile, "Transport V3 requires grpc_defaults")),
};
let sources = match sources {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let endpoints = map_grpc_endpoints(sources, &defaults, effective, 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::YellowstoneGrpcTransportSettings::new(endpoints);
if let std::result::Result::Err(error) = settings.validate() {
return std::result::Result::Err(transport_contract_error(
profile,
"effective Yellowstone gRPC Transport settings fail the Transport runtime contract",
&error,
));
}
std::result::Result::Ok(std::option::Option::Some(settings))
},
_ => std::result::Result::Err(effective_error(profile, "effective Transport format_version is unsupported")),
};
}
fn map_grpc_endpoints(
sources: std::vec::Vec<EffectiveGrpcEndpointSource>,
defaults: &EffectiveGrpcSessionSource,
effective: &crate::ResolvedConfigJson,
profile: &crate::ResolvedConfigProfile,
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings>> {
let mut endpoints = std::vec::Vec::<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings>::with_capacity(sources.len());
for (endpoint_index, source) in sources.into_iter().enumerate() {
let endpoint_name = source.name.clone();
if source.protocol != "solana_yellowstone" {
return std::result::Result::Err(
effective_error(profile, "effective gRPC protocol is unsupported")
.with_context("endpoint_name", endpoint_name)
.with_context("grpc_protocol", source.protocol),
);
}
let url = ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::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 Yellowstone gRPC endpoint URL is invalid", &error)
.with_context("endpoint_name", endpoint_name),
);
},
};
let session = map_grpc_session_settings(defaults, source.session.as_ref(), profile, endpoint_name.as_str());
let session = match session {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let metadata = map_grpc_metadata(source.metadata, source.secret_metadata, effective, profile, endpoint_name.as_str(), endpoint_index);
let metadata = match metadata {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let endpoint = ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
source.name,
source.enabled,
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new(source.provider),
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new(source.cluster),
url,
session,
);
let endpoint = endpoint.with_metadata(metadata);
let endpoint = match endpoint {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
transport_contract_error(profile, "effective Yellowstone gRPC metadata fail the Transport runtime contract", &error)
.with_context("endpoint_name", endpoint_name),
);
},
};
endpoints.push(endpoint);
}
return std::result::Result::Ok(endpoints);
}
fn map_grpc_session_settings(
defaults: &EffectiveGrpcSessionSource,
overrides: std::option::Option<&EffectiveGrpcSessionOverrideSource>,
profile: &crate::ResolvedConfigProfile,
endpoint_name: &str,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings> {
let mut connect_timeout_ms = defaults.connect_timeout_ms;
let mut unary_timeout_ms = defaults.unary_timeout_ms;
let mut close_timeout_ms = defaults.close_timeout_ms;
let mut reconnect_max_retries = defaults.reconnect.max_retries;
let mut reconnect_initial_backoff_ms = defaults.reconnect.initial_backoff_ms;
let mut reconnect_max_backoff_ms = defaults.reconnect.max_backoff_ms;
let mut request_channel_capacity = defaults.request_channel_capacity;
let mut update_channel_capacity = defaults.update_channel_capacity;
let mut max_inbound_message_size_bytes = defaults.max_inbound_message_size_bytes;
let mut max_outbound_message_size_bytes = defaults.max_outbound_message_size_bytes;
if let std::option::Option::Some(overrides) = overrides {
if let std::option::Option::Some(value) = overrides.connect_timeout_ms {
connect_timeout_ms = value;
}
if let std::option::Option::Some(value) = overrides.unary_timeout_ms {
unary_timeout_ms = value;
}
if let std::option::Option::Some(value) = overrides.close_timeout_ms {
close_timeout_ms = value;
}
if let std::option::Option::Some(reconnect) = overrides.reconnect.as_ref() {
if let std::option::Option::Some(value) = reconnect.max_retries {
reconnect_max_retries = value;
}
if let std::option::Option::Some(value) = reconnect.initial_backoff_ms {
reconnect_initial_backoff_ms = value;
}
if let std::option::Option::Some(value) = reconnect.max_backoff_ms {
reconnect_max_backoff_ms = value;
}
}
if let std::option::Option::Some(value) = overrides.request_channel_capacity {
request_channel_capacity = value;
}
if let std::option::Option::Some(value) = overrides.update_channel_capacity {
update_channel_capacity = value;
}
if let std::option::Option::Some(value) = overrides.max_inbound_message_size_bytes {
max_inbound_message_size_bytes = value;
}
if let std::option::Option::Some(value) = overrides.max_outbound_message_size_bytes {
max_outbound_message_size_bytes = value;
}
}
let reconnect = ksp_onchain_transport_lib::YellowstoneGrpcReconnectSettings::new(
reconnect_max_retries,
std::time::Duration::from_millis(reconnect_initial_backoff_ms),
std::time::Duration::from_millis(reconnect_max_backoff_ms),
);
let settings = ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::new(
std::time::Duration::from_millis(connect_timeout_ms),
std::time::Duration::from_millis(unary_timeout_ms),
std::time::Duration::from_millis(close_timeout_ms),
reconnect,
request_channel_capacity,
update_channel_capacity,
max_inbound_message_size_bytes,
max_outbound_message_size_bytes,
);
if let std::result::Result::Err(error) = settings.validate() {
return std::result::Result::Err(
transport_contract_error(profile, "effective Yellowstone gRPC session settings fail the Transport runtime contract", &error)
.with_context("endpoint_name", endpoint_name),
);
}
return std::result::Result::Ok(settings);
}
fn map_grpc_metadata(
public_sources: std::vec::Vec<EffectiveGrpcMetadataSource>,
secret_sources: std::vec::Vec<EffectiveGrpcMetadataSource>,
effective: &crate::ResolvedConfigJson,
profile: &crate::ResolvedConfigProfile,
endpoint_name: &str,
endpoint_index: usize,
) -> ksp_core_lib::Result<std::vec::Vec<ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry>> {
let mut metadata = std::vec::Vec::<ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry>::with_capacity(public_sources.len() + secret_sources.len());
for (metadata_index, source) in public_sources.into_iter().enumerate() {
let pointer = format!("/grpc_endpoints/{endpoint_index}/metadata/{metadata_index}/value");
if let std::result::Result::Err(error) = validate_grpc_metadata_provenance(effective, pointer.as_str(), false, profile, endpoint_name) {
return std::result::Result::Err(error);
}
let entry = ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::public(source.key, source.value);
let entry = match entry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
transport_contract_error(profile, "effective public Yellowstone gRPC metadata are invalid", &error)
.with_context("endpoint_name", endpoint_name),
);
},
};
metadata.push(entry);
}
for (metadata_index, source) in secret_sources.into_iter().enumerate() {
let pointer = format!("/grpc_endpoints/{endpoint_index}/secret_metadata/{metadata_index}/value");
if let std::result::Result::Err(error) = validate_grpc_metadata_provenance(effective, pointer.as_str(), true, profile, endpoint_name) {
return std::result::Result::Err(error);
}
let entry = ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::secret(source.key, source.value);
let entry = match entry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
transport_contract_error(profile, "effective secret Yellowstone gRPC metadata are invalid", &error)
.with_context("endpoint_name", endpoint_name),
);
},
};
metadata.push(entry);
}
return std::result::Result::Ok(metadata);
}
fn validate_grpc_metadata_provenance(
effective: &crate::ResolvedConfigJson,
pointer: &str,
secret_class: bool,
profile: &crate::ResolvedConfigProfile,
endpoint_name: &str,
) -> ksp_core_lib::Result<()> {
let provenance = match effective.provenance_at(pointer) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
effective_error(profile, "Yellowstone gRPC metadata provenance is unavailable")
.with_context("endpoint_name", endpoint_name)
.with_context("field", pointer),
);
},
};
let mut has_secret_environment = false;
for item in provenance {
let variable_name = match item.variable_name() {
std::option::Option::Some(value) => value,
std::option::Option::None => continue,
};
let sensitivity = crate::ConfigSensitivity::from_variable_name(variable_name);
let sensitivity = match sensitivity {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if sensitivity.is_secret() {
has_secret_environment = true;
if !secret_class {
return std::result::Result::Err(
effective_error(profile, "public Yellowstone gRPC metadata must not derive from a secret environment variable")
.with_context("endpoint_name", endpoint_name)
.with_context("field", pointer),
);
}
} else if secret_class {
return std::result::Result::Err(
effective_error(profile, "secret Yellowstone gRPC metadata may reference only secret environment variables")
.with_context("endpoint_name", endpoint_name)
.with_context("field", pointer),
);
}
}
if secret_class && !has_secret_environment {
return std::result::Result::Err(
effective_error(profile, "secret Yellowstone gRPC metadata require secret environment provenance")
.with_context("endpoint_name", endpoint_name)
.with_context("field", pointer),
);
}
return std::result::Result::Ok(());
}
fn map_endpoints( fn map_endpoints(
sources: std::vec::Vec<EffectiveEndpointSource>, sources: std::vec::Vec<EffectiveEndpointSource>,
profile: &crate::ResolvedConfigProfile, profile: &crate::ResolvedConfigProfile,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/public_api.rs // file: crates/ksp-config-lib/tests/public_api.rs
// version: 22 // version: 23
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity, //! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
//! Logging/Transport adapters and management contracts. //! Logging/Transport adapters and management contracts.
@@ -254,7 +254,9 @@ fn transport_adapter_contract_is_available_from_crate_root() {
assert!(std::mem::size_of::<ksp_config_lib::ResolvedTransportConfig>() > 0); assert!(std::mem::size_of::<ksp_config_lib::ResolvedTransportConfig>() > 0);
let _http_settings = ksp_config_lib::ResolvedTransportConfig::http_settings; let _http_settings = ksp_config_lib::ResolvedTransportConfig::http_settings;
let _ws_settings = ksp_config_lib::ResolvedTransportConfig::ws_settings; let _ws_settings = ksp_config_lib::ResolvedTransportConfig::ws_settings;
let _grpc_settings = ksp_config_lib::ResolvedTransportConfig::grpc_settings;
let _into_transport_settings = ksp_config_lib::ResolvedTransportConfig::into_transport_settings; let _into_transport_settings = ksp_config_lib::ResolvedTransportConfig::into_transport_settings;
let _into_all_transport_settings = ksp_config_lib::ResolvedTransportConfig::into_all_transport_settings;
assert_eq!(ksp_config_lib::FILE_ID_STD_TRANSPORT, "cfg.std.transport"); 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::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_FILENAME, "std.transport.json");

View File

@@ -0,0 +1,242 @@
{
"format_version": 3,
"retry": {
"max_retries": 2,
"initial_backoff_ms": 100,
"max_backoff_ms": 2000
},
"ws_defaults": {
"command_timeout_ms": 8000,
"close_timeout_ms": 4000,
"reconnect": {
"max_retries": 5,
"initial_backoff_ms": 250,
"max_backoff_ms": 5000
},
"resubscribe": "active_subscriptions",
"command_queue_capacity": 64,
"notification_queue_capacity": 96,
"max_active_subscriptions": 256,
"max_pending_requests": 48,
"max_message_size_bytes": 33554432,
"max_frame_size_bytes": 8388608,
"max_write_buffer_size_bytes": 524288
},
"grpc_defaults": {
"connect_timeout_ms": 6000,
"unary_timeout_ms": 7000,
"close_timeout_ms": 4000,
"reconnect": {
"max_retries": 4,
"initial_backoff_ms": 200,
"max_backoff_ms": 3000
},
"request_channel_capacity": 64,
"update_channel_capacity": 96,
"max_inbound_message_size_bytes": 33554432,
"max_outbound_message_size_bytes": 16777216
},
"default_profile": "grpc_secret_ok",
"profiles": [
{
"profile_id": "grpc_secret_ok",
"endpoints": [
{
"name": "fixture_http",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"url": "https://fallback.invalid",
"connect_timeout_ms": 1000,
"request_timeout_ms": 3000,
"max_idle_connections_per_host": 2,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 1,
"limits": {
"requests_per_second": 5,
"burst_capacity": 5,
"max_concurrent_requests": 2,
"pause_after_rate_limit_ms": 500
}
}
]
}
],
"ws_endpoints": [
{
"name": "fixture_ws",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"kind": "solana_standard",
"url": "wss://fallback.invalid"
}
],
"grpc_endpoints": [
{
"name": "fixture_grpc",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"protocol": "solana_yellowstone",
"url": "https://grpc.invalid",
"metadata": [
{
"key": "x-ksp-label",
"value": "${KSP_PUBLIC_GRPC_LABEL:-fixture-public}"
}
],
"secret_metadata": [
{
"key": "x-ksp-token",
"value": "Bearer ${KSP_SECRET_GRPC_TOKEN:-fixture-secret}"
}
],
"session": {
"connect_timeout_ms": 4500,
"reconnect": {
"max_retries": 7
},
"update_channel_capacity": 32
}
}
]
},
{
"profile_id": "grpc_public_from_secret_invalid",
"endpoints": [
{
"name": "fixture_http",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"url": "https://fallback.invalid",
"connect_timeout_ms": 1000,
"request_timeout_ms": 3000,
"max_idle_connections_per_host": 2,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 1,
"limits": {
"requests_per_second": 5,
"burst_capacity": 5,
"max_concurrent_requests": 2,
"pause_after_rate_limit_ms": 500
}
}
]
}
],
"ws_endpoints": [
{
"name": "fixture_ws",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"kind": "solana_standard",
"url": "wss://fallback.invalid"
}
],
"grpc_endpoints": [
{
"name": "fixture_grpc",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"protocol": "solana_yellowstone",
"url": "https://grpc.invalid",
"metadata": [
{
"key": "x-ksp-public",
"value": "${KSP_SECRET_GRPC_TOKEN:-fixture-secret}"
}
],
"secret_metadata": [],
"session": {
"connect_timeout_ms": 4500,
"reconnect": {
"max_retries": 7
},
"update_channel_capacity": 32
}
}
]
},
{
"profile_id": "grpc_secret_from_public_invalid",
"endpoints": [
{
"name": "fixture_http",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"url": "https://fallback.invalid",
"connect_timeout_ms": 1000,
"request_timeout_ms": 3000,
"max_idle_connections_per_host": 2,
"roles": [
{
"role": "default",
"enabled": true,
"request_kinds": [
"*"
],
"priority": 1,
"limits": {
"requests_per_second": 5,
"burst_capacity": 5,
"max_concurrent_requests": 2,
"pause_after_rate_limit_ms": 500
}
}
]
}
],
"ws_endpoints": [
{
"name": "fixture_ws",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"kind": "solana_standard",
"url": "wss://fallback.invalid"
}
],
"grpc_endpoints": [
{
"name": "fixture_grpc",
"enabled": true,
"provider": "fixture-provider",
"cluster": "mainnet-beta",
"protocol": "solana_yellowstone",
"url": "https://grpc.invalid",
"metadata": [],
"secret_metadata": [
{
"key": "x-ksp-secret",
"value": "${KSP_PUBLIC_GRPC_LABEL:-fixture-public}"
}
],
"session": {
"connect_timeout_ms": 4500,
"reconnect": {
"max_retries": 7
},
"update_channel_capacity": 32
}
}
]
}
]
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/transport.rs // file: crates/ksp-config-lib/unit_tests/transport.rs
// version: 6 // version: 10
#[test] #[test]
fn fixture_transport_profile_maps_complete_runtime_contract() { fn fixture_transport_profile_maps_complete_runtime_contract() {
@@ -41,6 +41,7 @@ fn fixture_transport_profile_maps_complete_runtime_contract() {
assert_eq!(role.limits().burst_capacity().map(std::num::NonZeroU32::get), std::option::Option::Some(12)); assert_eq!(role.limits().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().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))); assert_eq!(role.limits().pause_after_rate_limit(), std::option::Option::Some(std::time::Duration::from_millis(650)));
assert!(resolved.grpc_settings().is_none(), "V2 fixture must remain Yellowstone gRPC-free");
let ws = resolved.ws_settings(); let ws = resolved.ws_settings();
assert!(ws.is_some(), "V2 fixture should expose WebSocket settings"); assert!(ws.is_some(), "V2 fixture should expose WebSocket settings");
if let std::option::Option::Some(ws) = ws { if let std::option::Option::Some(ws) = ws {
@@ -90,6 +91,7 @@ fn v1_transport_fixture_remains_backward_readable_and_http_only() {
assert_eq!(resolved.settings().endpoints().len(), 1); assert_eq!(resolved.settings().endpoints().len(), 1);
assert_eq!(resolved.settings().endpoints()[0].url().as_str(), "https://legacy.invalid"); assert_eq!(resolved.settings().endpoints()[0].url().as_str(), "https://legacy.invalid");
assert!(resolved.ws_settings().is_none(), "V1 must not invent WebSocket runtime settings"); assert!(resolved.ws_settings().is_none(), "V1 must not invent WebSocket runtime settings");
assert!(resolved.grpc_settings().is_none(), "V1 must not invent Yellowstone gRPC runtime settings");
} }
} }
@@ -109,8 +111,9 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
assert_eq!(default.profile_id(), "devnet_public"); assert_eq!(default.profile_id(), "devnet_public");
assert_eq!(default.settings().endpoints()[0].cluster().as_str(), "devnet"); assert_eq!(default.settings().endpoints()[0].cluster().as_str(), "devnet");
assert_eq!(default.settings().endpoints()[0].url().as_str(), "https://api.devnet.solana.com"); assert_eq!(default.settings().endpoints()[0].url().as_str(), "https://api.devnet.solana.com");
assert!(default.grpc_settings().is_none(), "Devnet profile must not invent a Yellowstone gRPC endpoint");
let ws = default.ws_settings(); let ws = default.ws_settings();
assert!(ws.is_some(), "committed V2 Devnet profile should expose WebSocket settings"); assert!(ws.is_some(), "committed V3 Devnet profile should expose WebSocket settings");
if let std::option::Option::Some(ws) = ws { if let std::option::Option::Some(ws) = ws {
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.devnet.solana.com"); assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.devnet.solana.com");
assert_eq!(ws.endpoints()[0].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard); assert_eq!(ws.endpoints()[0].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
@@ -121,8 +124,9 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
assert_eq!(mainnet.selection_source(), crate::ConfigProfileSelectionSource::Explicit); 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].cluster().as_str(), "mainnet-beta");
assert_eq!(mainnet.settings().endpoints()[0].url().as_str(), "https://api.mainnet-beta.solana.com"); assert_eq!(mainnet.settings().endpoints()[0].url().as_str(), "https://api.mainnet-beta.solana.com");
assert!(mainnet.grpc_settings().is_none(), "generic Mainnet profile must stay distinct from the PublicNode gRPC profile");
let ws = mainnet.ws_settings(); let ws = mainnet.ws_settings();
assert!(ws.is_some(), "committed V2 Mainnet profile should expose WebSocket settings"); assert!(ws.is_some(), "committed V3 Mainnet profile should expose WebSocket settings");
if let std::option::Option::Some(ws) = ws { if let std::option::Option::Some(ws) = ws {
assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.mainnet-beta.solana.com"); assert_eq!(ws.endpoints()[0].url().as_str(), "wss://api.mainnet-beta.solana.com");
} }
@@ -130,7 +134,147 @@ fn committed_transport_document_maps_default_and_explicit_profiles() {
} }
#[test] #[test]
fn committed_v2_websocket_endpoint_composes_with_public_session_constructor_without_polling() { fn committed_v3_publicnode_profiles_map_provider_neutral_yellowstone_grpc_with_network_scoped_secret_x_tokens() {
let engine = committed_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
for (profile_id, endpoint_name, cluster, url, environment_name, canary) in [
(
"publicnode_mainnet",
"publicnode_solana_mainnet_yellowstone",
"mainnet-beta",
"https://solana-yellowstone-grpc.publicnode.com:443",
"KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN",
"PUBLICNODE-MAINNET-GRPC-X-TOKEN-CANARY",
),
(
"publicnode_testnet",
"publicnode_solana_testnet_yellowstone",
"testnet",
"https://solana-testnet-yellowstone-grpc.publicnode.com:443",
"KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN",
"PUBLICNODE-TESTNET-GRPC-X-TOKEN-CANARY",
),
] {
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert(environment_name.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::Some(profile_id), &environment);
assert!(resolved.is_ok(), "committed PublicNode V3 profile {profile_id} should map from its network-scoped token: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
assert_eq!(resolved.profile_id(), profile_id);
assert!(resolved.ws_settings().is_some());
let grpc = resolved.grpc_settings();
assert!(grpc.is_some(), "PublicNode profile {profile_id} should expose Yellowstone gRPC settings");
if let std::option::Option::Some(grpc) = grpc {
assert_eq!(grpc.endpoints().len(), 1);
let endpoint = &grpc.endpoints()[0];
assert_eq!(endpoint.name(), endpoint_name);
assert_eq!(endpoint.provider().as_str(), "publicnode");
assert_eq!(endpoint.cluster().as_str(), cluster);
assert_eq!(endpoint.url().as_str(), url);
assert!(endpoint.url().uses_tls());
assert_eq!(endpoint.metadata().len(), 1);
assert_eq!(endpoint.metadata()[0].key(), "x-token");
assert!(endpoint.metadata()[0].is_secret());
let endpoint_debug = format!("{endpoint:?}");
assert!(!endpoint_debug.contains(canary), "PublicNode {cluster} x-token must stay redacted from endpoint Debug");
assert_eq!(endpoint.session().connect_timeout(), std::time::Duration::from_millis(10_000));
assert_eq!(endpoint.session().unary_timeout(), std::time::Duration::from_millis(10_000));
assert_eq!(endpoint.session().close_timeout(), std::time::Duration::from_millis(5_000));
assert_eq!(endpoint.session().reconnect().max_retries(), 5);
assert!(grpc.validate().is_ok(), "Config-produced Yellowstone gRPC settings should satisfy Transport validation");
let debug = format!("{grpc:?}");
assert!(!debug.contains("publicnode.com"), "Transport settings Debug must not expose gRPC endpoint URLs");
assert!(!debug.contains(canary), "Transport settings Debug must not expose the network-scoped PublicNode x-token");
}
}
}
#[test]
fn v3_grpc_metadata_separates_public_and_secret_provenance() {
let engine = v3_fixture_engine();
let engine = match engine {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let canary = "grpc-secret-canary";
let mut process = std::collections::BTreeMap::<String, String>::new();
process.insert("KSP_SECRET_GRPC_TOKEN".to_owned(), canary.to_owned());
process.insert("KSP_PUBLIC_GRPC_LABEL".to_owned(), "operator-public".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(), "V3 gRPC metadata fixture should map: {resolved:?}");
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let grpc = resolved.grpc_settings();
assert!(grpc.is_some());
if let std::option::Option::Some(grpc) = grpc {
let endpoint = &grpc.endpoints()[0];
assert_eq!(endpoint.metadata().len(), 2);
assert_eq!(endpoint.metadata()[0].key(), "x-ksp-label");
assert!(!endpoint.metadata()[0].is_secret());
assert_eq!(endpoint.metadata()[1].key(), "x-ksp-token");
assert!(endpoint.metadata()[1].is_secret());
assert_eq!(endpoint.session().connect_timeout(), std::time::Duration::from_millis(4_500));
assert_eq!(endpoint.session().unary_timeout(), std::time::Duration::from_millis(7_000));
assert_eq!(endpoint.session().reconnect().max_retries(), 7);
assert_eq!(endpoint.session().update_channel_capacity(), 32);
}
assert_eq!(
resolved.effective().safe_value().pointer("/grpc_endpoints/0/metadata/0/value").and_then(serde_json::Value::as_str),
std::option::Option::Some("operator-public")
);
assert_eq!(
resolved.effective().safe_value().pointer("/grpc_endpoints/0/secret_metadata/0/value").and_then(serde_json::Value::as_str),
std::option::Option::Some("Bearer ********")
);
let secret_provenance = resolved.effective().provenance_at("/grpc_endpoints/0/secret_metadata/0/value");
assert!(secret_provenance.is_some());
if let std::option::Option::Some(provenance) = secret_provenance {
assert_eq!(provenance.len(), 2);
assert_eq!(provenance[0], crate::ConfigValueProvenance::DocumentLiteral);
assert_eq!(provenance[1].variable_name(), std::option::Option::Some("KSP_SECRET_GRPC_TOKEN"));
}
let debug = format!("{resolved:?}");
assert!(!debug.contains(canary));
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
}
#[test]
fn v3_grpc_metadata_rejects_crossed_sensitivity_classes() {
let engine = v3_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_GRPC_TOKEN".to_owned(), "secret-canary".to_owned());
process.insert("KSP_PUBLIC_GRPC_LABEL".to_owned(), "public-canary".to_owned());
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
let public_from_secret = engine.load_resolved_transport_config(std::option::Option::Some("grpc_public_from_secret_invalid"), &environment);
let secret_from_public = engine.load_resolved_transport_config(std::option::Option::Some("grpc_secret_from_public_invalid"), &environment);
assert!(public_from_secret.is_err(), "public metadata must reject KSP_SECRET provenance");
assert!(secret_from_public.is_err(), "secret_metadata must reject non-secret environment provenance");
if let std::result::Result::Err(error) = public_from_secret {
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
assert!(!format!("{error:?}").contains("secret-canary"));
}
if let std::result::Result::Err(error) = secret_from_public {
assert_eq!(error.code(), crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID);
assert!(!format!("{error:?}").contains("public-canary"));
}
}
#[test]
fn committed_v3_websocket_endpoint_preserves_v2_compatibility_constructor_without_polling() {
let engine = committed_engine(); let engine = committed_engine();
let engine = match engine { let engine = match engine {
std::result::Result::Ok(value) => value, std::result::Result::Ok(value) => value,
@@ -138,11 +282,11 @@ fn committed_v2_websocket_endpoint_composes_with_public_session_constructor_with
}; };
let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new()); let environment = crate::ConfigEnvironment::from_maps(std::collections::BTreeMap::new(), std::collections::BTreeMap::new());
let resolved = engine.load_resolved_transport_config(std::option::Option::Some("devnet_public"), &environment); let resolved = engine.load_resolved_transport_config(std::option::Option::Some("devnet_public"), &environment);
assert!(resolved.is_ok(), "committed V2 Transport profile should map: {resolved:?}"); assert!(resolved.is_ok(), "committed V3 Transport profile should map: {resolved:?}");
if let std::result::Result::Ok(resolved) = resolved { if let std::result::Result::Ok(resolved) = resolved {
let (http, ws) = resolved.into_transport_settings(); let (http, ws) = resolved.into_transport_settings();
assert_eq!(http.endpoints().len(), 1); assert_eq!(http.endpoints().len(), 1);
assert!(ws.is_some(), "committed V2 Transport profile should expose WebSocket settings"); assert!(ws.is_some(), "committed V3 Transport profile should expose WebSocket settings");
if let std::option::Option::Some(ws) = ws { if let std::option::Option::Some(ws) = ws {
assert!(ws.validate().is_ok(), "Config-produced WebSocket settings should satisfy Transport validation"); assert!(ws.validate().is_ok(), "Config-produced WebSocket settings should satisfy Transport validation");
assert_eq!(ws.endpoints().len(), 1); assert_eq!(ws.endpoints().len(), 1);
@@ -170,6 +314,7 @@ fn transport_profile_preserves_global_and_profile_origin() {
if let std::result::Result::Ok(profile) = 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("retry"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
assert_eq!(profile.origin("ws_defaults"), std::option::Option::Some(crate::ConfigValueOrigin::Global)); assert_eq!(profile.origin("ws_defaults"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
assert_eq!(profile.origin("grpc_defaults"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
assert_eq!(profile.origin("endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile)); assert_eq!(profile.origin("endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
assert_eq!(profile.origin("ws_endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile)); assert_eq!(profile.origin("ws_endpoints"), std::option::Option::Some(crate::ConfigValueOrigin::Profile));
assert_eq!(profile.origin("format_version"), std::option::Option::Some(crate::ConfigValueOrigin::Global)); assert_eq!(profile.origin("format_version"), std::option::Option::Some(crate::ConfigValueOrigin::Global));
@@ -421,6 +566,22 @@ fn v1_fixture_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry)); return std::result::Result::Ok(crate::ConfigDocumentEngine::new(bootstrap, registry));
} }
fn v3_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_v3");
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> { fn committed_engine() -> ksp_core_lib::Result<crate::ConfigDocumentEngine> {
let workspace = workspace_root(); let workspace = workspace_root();
let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas")); let bootstrap = crate::ConfigBootstrapOptions::from_paths(workspace.join("config"), workspace.join("config/schemas"));

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-core-lib/tests/workspace_dependencies.rs // file: crates/ksp-core-lib/tests/workspace_dependencies.rs
// version: 5 // version: 7
//! Workspace-level dependency policy canaries owned by the foundational KSP test surface. //! Workspace-level dependency policy canaries owned by the foundational KSP test surface.
@@ -62,11 +62,17 @@ fn transport_manifest_preserves_ksp_dependency_firewall() {
assert!(manifest.contains("ksp-core-lib")); assert!(manifest.contains("ksp-core-lib"));
assert!(manifest.contains("ksp-logging-lib")); assert!(manifest.contains("ksp-logging-lib"));
assert!(manifest.contains("futures-util = { workspace = true, features = [\"sink\", \"std\"] }")); assert!(manifest.contains("futures-util = { workspace = true, features = [\"sink\", \"std\"] }"));
assert!(manifest.contains("http.workspace = true"));
assert!(manifest.contains("reqwest = { workspace = true, features = [\"rustls\"] }")); assert!(manifest.contains("reqwest = { workspace = true, features = [\"rustls\"] }"));
assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"net\", \"rt\", \"sync\", \"time\"] }")); assert!(manifest.contains("tokio = { workspace = true, features = [\"macros\", \"net\", \"rt\", \"sync\", \"time\"] }"));
assert!(manifest.contains("tokio-tungstenite = { workspace = true, features = [\"connect\", \"rustls-tls-webpki-roots\"] }")); assert!(manifest.contains("tokio-tungstenite = { workspace = true, features = [\"connect\", \"rustls-tls-webpki-roots\"] }"));
assert!(manifest.contains("tonic = { workspace = true, features = [\"channel\", \"tls-aws-lc\", \"tls-webpki-roots\"] }"));
assert!(manifest.contains("tonic-prost.workspace = true"));
assert!(manifest.contains("yellowstone-grpc-proto.workspace = true"));
assert!(manifest.contains("[dev-dependencies]")); assert!(manifest.contains("[dev-dependencies]"));
assert!(manifest.contains("tokio = { workspace = true, features = [\"io-util\", \"net\", \"rt\", \"test-util\"] }")); assert!(manifest.contains("tokio = { workspace = true, features = [\"io-util\", \"net\", \"rt\", \"test-util\"] }"));
assert!(manifest.contains("tonic = { workspace = true, features = [\"codegen\", \"server\"] }"));
assert!(manifest.contains("yellowstone-grpc-proto = { workspace = true, features = [\"tonic\"] }"));
} }
#[test] #[test]
@@ -86,7 +92,20 @@ fn transport_manifest_runtime_and_dev_dependency_names_are_exact() {
let dependency_names = manifest_dependency_names(dependencies); let dependency_names = manifest_dependency_names(dependencies);
assert_eq!( assert_eq!(
dependency_names, dependency_names,
std::vec!["futures-util", "ksp-core-lib", "ksp-logging-lib", "reqwest", "serde", "serde_json", "tokio", "tokio-tungstenite"] std::vec![
"futures-util",
"http",
"ksp-core-lib",
"ksp-logging-lib",
"reqwest",
"serde",
"serde_json",
"tokio",
"tokio-tungstenite",
"tonic",
"tonic-prost",
"yellowstone-grpc-proto",
]
); );
let dev_dependencies_tail = manifest.split("[dev-dependencies]").nth(1); let dev_dependencies_tail = manifest.split("[dev-dependencies]").nth(1);
assert!(dev_dependencies_tail.is_some(), "transport dev-dependencies section must exist"); assert!(dev_dependencies_tail.is_some(), "transport dev-dependencies section must exist");
@@ -98,7 +117,7 @@ fn transport_manifest_runtime_and_dev_dependency_names_are_exact() {
std::option::Option::Some(value) => value, std::option::Option::Some(value) => value,
std::option::Option::None => return, std::option::Option::None => return,
}; };
assert_eq!(manifest_dependency_names(dev_dependencies), std::vec!["tokio"]); assert_eq!(manifest_dependency_names(dev_dependencies), std::vec!["tokio", "tonic", "yellowstone-grpc-proto"]);
} }
fn manifest_dependency_names(section: &str) -> std::vec::Vec<&str> { fn manifest_dependency_names(section: &str) -> std::vec::Vec<&str> {

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-logging-lib/Cargo.toml # file: crates/ksp-logging-lib/Cargo.toml
# version: 5 # version: 6
[package] [package]
name = "ksp-logging-lib" name = "ksp-logging-lib"
@@ -10,8 +10,8 @@ repository.workspace = true
[dependencies] [dependencies]
ksp-core-lib = { path = "../ksp-core-lib" } ksp-core-lib = { path = "../ksp-core-lib" }
tracing = { workspace = true, features = ["std"] } tracing = { workspace = true, features = ["std"] }
tracing-subscriber = { workspace = true, features = ["fmt", "json", "ansi"] }
tracing-appender.workspace = true tracing-appender.workspace = true
tracing-subscriber = { workspace = true, features = ["fmt", "json", "ansi"] }
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] } tokio = { workspace = true, features = ["macros", "rt", "rt-multi-thread"] }

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-onchain-transport-lib/Cargo.toml # file: crates/ksp-onchain-transport-lib/Cargo.toml
# version: 6 # version: 9
[package] [package]
name = "ksp-onchain-transport-lib" name = "ksp-onchain-transport-lib"
@@ -8,17 +8,23 @@ edition.workspace = true
repository.workspace = true repository.workspace = true
[dependencies] [dependencies]
futures-util = { workspace = true, features = ["sink", "std"] }
http.workspace = true
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" }
futures-util = { workspace = true, features = ["sink", "std"] }
reqwest = { workspace = true, features = ["rustls"] } reqwest = { workspace = true, features = ["rustls"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] } tokio = { workspace = true, features = ["macros", "net", "rt", "sync", "time"] }
tokio-tungstenite = { workspace = true, features = ["connect", "rustls-tls-webpki-roots"] } tokio-tungstenite = { workspace = true, features = ["connect", "rustls-tls-webpki-roots"] }
tonic = { workspace = true, features = ["channel", "tls-aws-lc", "tls-webpki-roots"] }
tonic-prost.workspace = true
yellowstone-grpc-proto.workspace = true
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true, features = ["io-util", "net", "rt", "test-util"] } tokio = { workspace = true, features = ["io-util", "net", "rt", "test-util"] }
tonic = { workspace = true, features = ["codegen", "server"] }
yellowstone-grpc-proto = { workspace = true, features = ["tonic"] }
[lints] [lints]
workspace = true workspace = true

View File

@@ -1,9 +1,9 @@
<!-- file: crates/ksp-onchain-transport-lib/README.md --> <!-- file: crates/ksp-onchain-transport-lib/README.md -->
<!-- version: 20 --> <!-- version: 22 -->
# `ksp-onchain-transport-lib` # `ksp-onchain-transport-lib`
`ksp-onchain-transport-lib` est la bibliothèque KSP propriétaire du transport on-chain Solana. Elle fournit le transport HTTP JSON-RPC complet et le moteur WebSocket Solana standard ; les extensions provider-specific et gRPC sont ajoutées séparément lorsquune release les cible. `ksp-onchain-transport-lib` est la bibliothèque KSP propriétaire du transport on-chain Solana. Elle fournit le transport HTTP JSON-RPC complet, le moteur WebSocket Solana standard et la foundation Yellowstone gRPC standard/provider-neutral. Les extensions provider-specific restent ajoutées séparément lorsquune release les cible.
## Responsabilités ## Responsabilités
@@ -21,6 +21,8 @@ La crate possède :
- l'exécution générique des méthodes standard supportées ; - l'exécution générique des méthodes standard supportées ;
- les wrappers typés HTTP et WebSocket explicitement livrés par KSP ; - les wrappers typés HTTP et WebSocket explicitement livrés par KSP ;
- les sessions physiques WebSocket, subscriptions logiques, reconnect/resubscribe et backpressure bornés ; - les sessions physiques WebSocket, subscriptions logiques, reconnect/resubscribe et backpressure bornés ;
- les settings, channels, unary et sessions `Subscribe` Yellowstone gRPC standard ;
- le reconnect/replay Yellowstone prudent avec observabilité de gaps/duplicates sans promesse lossless ;
- les snapshots runtime sûrs ; - les snapshots runtime sûrs ;
- l'observabilité Transport via `ksp-logging-lib`. - l'observabilité Transport via `ksp-logging-lib`.
@@ -37,6 +39,7 @@ ksp-config-lib
-> ksp-logging-lib -> ksp-logging-lib
-> reqwest / tokio / serde -> reqwest / tokio / serde
-> tokio-tungstenite / futures-util -> tokio-tungstenite / futures-util
-> tonic / tonic-prost / yellowstone-grpc-proto
``` ```
La direction inverse est interdite : La direction inverse est interdite :
@@ -217,6 +220,32 @@ Pour Helius, lactor envoie automatiquement un control frame WebSocket `Ping`
LaserStream **gRPC** reste un backend distinct, hors de cette façade, de `WsProtocolKind` et de la Config WebSocket `helius_laserstream`. LaserStream **gRPC** reste un backend distinct, hors de cette façade, de `WsProtocolKind` et de la Config WebSocket `helius_laserstream`.
## Yellowstone gRPC standard
La foundation `0.2.9` ajoute un troisième backend réseau distinct de HTTP et WebSocket. Le moteur est KSP-owned : `yellowstone-grpc-proto` fournit le wire publié, tandis que Tonic reste encapsulé derrière les types crate-root KSP. Aucun client Tonic brut ni type protobuf upstream nest réexporté.
La surface publique principale comprend :
```text
YellowstoneGrpcEndpointUrl / YellowstoneGrpcEndpointSettings
YellowstoneGrpcSessionSettings / YellowstoneGrpcReconnectSettings
YellowstoneGrpcTransportSettings
YellowstoneGrpcChannel
SolanaYellowstoneGrpcUnaryClient
YellowstoneSubscribeRequest
SolanaYellowstoneGrpcSubscribeSession
YellowstoneGrpcSubscribeSnapshot
```
Les sept unary standards retenus sont `SubscribeReplayInfo`, `Ping`, `GetLatestBlockhash`, `GetBlockHeight`, `GetSlot`, `IsBlockhashValid` et `GetVersion`. `Subscribe` couvre accounts, slots, transactions, transaction status, blocks, block metadata et entries, avec `commitment`, `accounts_data_slice`, `ping` et `from_slot`. `SubscribeDeshred` reste hors scope de la foundation standard.
Le stream bidirectionnel est borné : request/update queues, tailles inbound/outbound, half-close, close timeout et reconnect budget sont explicites. Après reconnect, KSP rejoue la dernière requête complète acceptée et avance prudemment `from_slot` selon le dernier slot observé et `SubscribeReplayInfo.first_available`. Les compteurs de gap et duplicate sont de lobservabilité ; ils ne constituent jamais une garantie exactly-once ou lossless.
Config Transport V3 peut mapper des `grpc_endpoints` vers ces settings sans inverser la dépendance. `protocol = solana_yellowstone` décrit le wire standard, tandis que `provider` reste un descripteur distinct. Les profils committés `publicnode_mainnet` et `publicnode_testnet` utilisent le standard sans façade PublicNode spécifique. Leur personal token est injecté comme metadata secrète `x-token` depuis Config ; Transport ne lit jamais les variables KSP.
Le smoke live opt-in ouvre un `Subscribe` slots authentifié sur PublicNode Mainnet et Testnet, attend un update `Slot` non nul puis ferme de manière bornée. Les deux endpoints sont versionnés. Le harness reçoit ses personal tokens sur stdin ; aucun secret nest committé ni placé dans lURL. Le même token a été validé par lopérateur sur les deux réseaux, même si Config conserve deux variables distinctes pour laisser cette policy flexible.
## Résilience ## Résilience
L'admission est calculée par couple endpoint/rôle. Le pool applique : L'admission est calculée par couple endpoint/rôle. Le pool applique :
@@ -253,7 +282,7 @@ La configuration Logging de référence conserve un fichier dédié Transport à
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. 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.
Trois smokes Devnet opt-in sont séparés par responsabilité : Quatre smokes réseau opt-in sont séparés par responsabilité :
```text ```text
Transport HTTP pur : settings programmatiques -> HttpTransportPool Transport HTTP pur : settings programmatiques -> HttpTransportPool
@@ -270,13 +299,17 @@ Transport WebSocket pur : settings programmatiques -> WsSession
Composition historique : Config -> std.transport/devnet_public -> HttpTransportPool Composition historique : Config -> std.transport/devnet_public -> HttpTransportPool
-> getHealth/getGenesisHash/getVersion/getBalance -> getHealth/getGenesisHash/getVersion/getBalance
Transport Yellowstone gRPC : settings programmatiques + x-token secret
-> PublicNode Mainnet + Testnet
-> TLS -> Subscribe slots -> Slot non nul -> close borné
``` ```
Le smoke HTTP 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. Le smoke HTTP 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.
Le smoke WebSocket Transport cible uniquement la famille stable `slotSubscribe` sur l'endpoint public Devnet `wss://api.devnet.solana.com`. Il borne connexion, attente de notification, unsubscribe et fermeture ; il ne transforme aucune famille unstable en gate live. Le smoke WebSocket Transport cible uniquement la famille stable `slotSubscribe` sur l'endpoint public Devnet `wss://api.devnet.solana.com`. Il borne connexion, attente de notification, unsubscribe et fermeture ; il ne transforme aucune famille unstable en gate live.
Les trois tests sont `ignored` par défaut. Les deux smokes Transport appartiennent 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. Un rate-limit, refus externe ou incident Devnet n'est pas assimilé automatiquement à une régression locale. Les quatre tests sont `ignored` par défaut ; le test Yellowstone contient deux cas live Mainnet/Testnet. Les trois familles de smokes Transport appartiennent 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. Un rate-limit, refus externe ou incident Devnet n'est pas assimilé automatiquement à une régression locale.
Aucun smoke Helius live supplémentaire nest committé en `0.2.8-pre.010`. Un tel test devrait à la fois obtenir `KSP_SECRET_HELIUS_API_KEY` via Config et exercer Transport ; lajouter dans Transport violerait lownership environnement/secret, tandis que lajouter dans Config étendrait lexception cross-crates que le projet veut au contraire résorber. La première surface KSP dintégration/orchestration dédiée devra héberger ce smoke. Le scénario live recommandé est alors `helius_devnet -> HeliusLaserStreamWsSession -> slotSubscribe -> notification -> unsubscribe -> close`; `transactionSubscribe` reste un smoke optionnel dépendant des droits provider et ne devient pas un gate stable de release. Aucun smoke Helius live supplémentaire nest committé en `0.2.8-pre.010`. Un tel test devrait à la fois obtenir `KSP_SECRET_HELIUS_API_KEY` via Config et exercer Transport ; lajouter dans Transport violerait lownership environnement/secret, tandis que lajouter dans Config étendrait lexception cross-crates que le projet veut au contraire résorber. La première surface KSP dintégration/orchestration dédiée devra héberger ce smoke. Le scénario live recommandé est alors `helius_devnet -> HeliusLaserStreamWsSession -> slotSubscribe -> notification -> unsubscribe -> close`; `transactionSubscribe` reste un smoke optionnel dépendant des droits provider et ne devient pas un gate stable de release.
@@ -290,4 +323,6 @@ Aucun smoke Helius live supplémentaire nest committé en `0.2.8-pre.010`. Un
- [`../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md) — matrice finale validée Transactions ; - [`../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md) — matrice finale validée Transactions ;
- [`../../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/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 ; - [`../../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 + WebSocket V2, avec lecture backward V1 HTTP-only. - [`../../docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md`](../../docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md) — plan Yellowstone gRPC standard/provider-neutral et PublicNode ;
- [`../../docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md`](../../docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md) — matrice de compliance Yellowstone ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — configuration standard Transport V3 HTTP + WebSocket + gRPC, avec lecture backward V1/V2.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md --> <!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
<!-- version: 20 --> <!-- version: 22 -->
# Utilisation de `ksp-onchain-transport-lib` # Utilisation de `ksp-onchain-transport-lib`
@@ -257,7 +257,111 @@ Les limites de taille et de capacité sont des policies KSP configurables par `W
Le snapshot expose seulement l'identité locale, les metadata logiques de l'endpoint, l'état, les compteurs sûrs et les projections locales de subscriptions. L'URL et les remote subscription IDs ne sont jamais projetés. La disparition de tous les handles de session déclenche le cleanup actor best-effort ; `close().await` reste la voie normale de shutdown. Le snapshot expose seulement l'identité locale, les metadata logiques de l'endpoint, l'état, les compteurs sûrs et les projections locales de subscriptions. L'URL et les remote subscription IDs ne sont jamais projetés. La disparition de tous les handles de session déclenche le cleanup actor best-effort ; `close().await` reste la voie normale de shutdown.
## 4. Appels typés ## 4. Yellowstone gRPC standard
### Construction programmatique et unary
Transport peut ouvrir directement un endpoint Yellowstone sans Config :
```rust
let grpc_url = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse(
"https://solana-yellowstone-grpc.publicnode.com:443",
) {
Ok(value) => value,
Err(error) => return Err(error),
};
let x_token_metadata = match ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::secret(
"x-token",
x_token,
) {
Ok(value) => value,
Err(error) => return Err(error),
};
let grpc_endpoint = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
"publicnode_mainnet_yellowstone",
true,
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("publicnode"),
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new("mainnet-beta"),
grpc_url,
ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::default(),
)
.with_metadata(vec![x_token_metadata])
{
Ok(value) => value,
Err(error) => return Err(error),
};
let grpc_channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(&grpc_endpoint).await {
Ok(value) => value,
Err(error) => return Err(error),
};
let grpc = grpc_channel.standard_unary_client();
let version = grpc.get_version().await;
let slot = grpc
.get_slot(Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed))
.await;
```
LURL reste sensible : `Debug`, erreurs KSP et snapshots nen exposent pas la valeur. Les metadata publiques/secrètes se construisent avec `YellowstoneGrpcMetadataEntry`; Transport ne lit jamais lenvironnement. PublicNode requiert actuellement une metadata secrète `x-token` pour les endpoints Yellowstone validés ; ici `x_token` représente une valeur déjà obtenue par un caller sécurisé. En usage normal, Config construit cette metadata depuis les variables `KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN` ou `KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN`. Les appels unary montrés ci-dessus illustrent la surface KSP standard ; le smoke PublicNode de release ne prétend pas valider leur entitlement provider et gate uniquement `Subscribe` slots.
### Config Transport V3
Avec `ksp-config-lib`, un profil V3 peut exposer les trois transports sans casser laccesseur historique HTTP + WS :
```rust
let resolved = match engine.load_resolved_transport_config(Some("publicnode_mainnet"), &environment) {
Ok(value) => value,
Err(error) => return Err(error),
};
let grpc_settings = match resolved.grpc_settings() {
Some(value) => value,
None => return Err(ksp_core_lib::Error::new(
ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS,
"selected profile has no Yellowstone gRPC endpoint",
)),
};
let endpoint = match grpc_settings.endpoints().iter().find(|candidate| candidate.enabled()) {
Some(value) => value,
None => return Err(ksp_core_lib::Error::new(
ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS,
"selected profile has no enabled Yellowstone gRPC endpoint",
)),
};
let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(endpoint).await;
```
`protocol = solana_yellowstone` est validé par Config et reste distinct du descripteur `provider`. Une valeur provider nautorise pas Transport à introduire une API provider-specific sans divergence réelle.
### Subscribe bidirectionnel
Une session standard part dune requête typed complète :
```rust
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
let name = match ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("slots") {
Ok(value) => value,
Err(error) => return Err(error),
};
if let Err(error) = request.insert_slot_filter(
name,
ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter::new(),
) {
return Err(error);
}
request.set_commitment(Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
let mut stream = match grpc_channel.open_standard_subscribe(request).await {
Ok(value) => value,
Err(error) => return Err(error),
};
let update = stream.next_update().await;
let snapshot = stream.snapshot();
let closed = stream.close().await;
```
`try_update()` remplace dynamiquement la requête complète tant que la session est `Active`. Une mutation pendant `Reconnecting` est refusée pour éviter une application ambiguë. Le snapshot expose reconnects, replay attempts, gaps, duplicates, dernier `from_slot` demandé et dernier slot observé, sans endpoint ni payload arbitraire.
Le reconnect réutilise la dernière requête acceptée et peut avancer `from_slot`, mais le consumer doit traiter cette reprise comme best-effort. KSP ne promet ni exactly-once, ni replay historique complet, ni absence de fork/equivocation entre nœuds.
## 5. Appels typés
Les wrappers typés se trouvent directement sur `HttpTransportPool`. Les wrappers typés se trouvent directement sur `HttpTransportPool`.
@@ -317,7 +421,7 @@ let stake_minimum = pool.get_stake_minimum_delegation(&role, Some(&context)).awa
`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. `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.
## 5. Exécution JSON-RPC standard générique ## 6. Exécution JSON-RPC standard générique
Une méthode courante auditée peut être appelée via son descriptor : Une méthode courante auditée peut être appelée via son descriptor :
@@ -331,7 +435,7 @@ Cette API retourne un `serde_json::Value`. Elle reste utile pour les extensions
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. 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.
## 6. Sélection et admission sans exécuter la requête ## 7. Sélection et admission sans exécuter la requête
Pour inspecter le routing : Pour inspecter le routing :
@@ -346,13 +450,13 @@ Dans le même bloc, `acquire_for_method()` réserve réellement la capacité RPS
`HttpRequestPermit` détient la capacité de concurrence jusqu'à sa destruction. Aucun verrou synchrone n'est conservé pendant l'attente réseau. `HttpRequestPermit` détient la capacité de concurrence jusqu'à sa destruction. Aucun verrou synchrone n'est conservé pendant l'attente réseau.
## 7. Snapshots runtime ## 8. Snapshots runtime
`HttpTransportPool::snapshot()` fournit une vue sûre des endpoints/rôles : disponibilité, limites, requêtes en vol, cooldown restant et compteurs 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. Les URLs d'endpoint n'y apparaissent jamais.
## 8. Retry et write submissions ## 9. Retry et write submissions
La policy de retry est portée par la metadata des méthodes et `evaluate_transport_retry()`. La policy de retry est portée par la metadata des méthodes et `evaluate_transport_retry()`.
@@ -360,7 +464,7 @@ Les reads/simulations classés `RetrySafe` peuvent être réessayés dans le bud
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. 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.
## 9. Logging ## 10. Logging
Les événements Transport utilisent le target : Les événements Transport utilisent le target :
@@ -372,7 +476,7 @@ Ne jamais journaliser l'URL complète, un token provider, un body massif, une tr
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. 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.
## 10. Smokes Devnet opt-in ## 11. Smokes réseau opt-in
Le smoke **Transport HTTP pur** construit ses settings programmatiquement et exerce un sous-ensemble représentatif d'Accounts/Tokens/Cluster, trois reads Transactions, puis des reads Blocks/Economics : Le smoke **Transport HTTP pur** construit ses settings programmatiquement et exerce un sous-ensemble représentatif d'Accounts/Tokens/Cluster, trois reads Transactions, puis des reads Blocks/Economics :
@@ -390,6 +494,31 @@ cargo test -p ksp-onchain-transport-lib --test websocket_devnet_smoke -- --ignor
Il n'utilise ni `blockSubscribe`, ni `slotsUpdatesSubscribe`, ni `voteSubscribe` : ces familles restent unstable et leur disponibilité dépend des capabilities du validator. Le smoke live n'est donc pas un gate de disponibilité de ces extensions. Il n'utilise ni `blockSubscribe`, ni `slotsUpdatesSubscribe`, ni `voteSubscribe` : ces familles restent unstable et leur disponibilité dépend des capabilities du validator. Le smoke live n'est donc pas un gate de disponibilité de ces extensions.
Le smoke **Transport Yellowstone gRPC PublicNode** reste indépendant de Config mais nécessite un personal token opérateur. Il teste Mainnet et Testnet en ouvrant `Subscribe`, en demandant les updates `slots`, en attendant un `YellowstoneSubscribeUpdate::Slot` non nul puis en fermant la session de manière bornée.
Pour éviter de placer les secrets dans les arguments ou l'URL, le harness lit deux lignes sur stdin : Mainnet puis Testnet. Elles peuvent contenir la même valeur ; l'opérateur a validé un même personal token sur les deux réseaux.
```bash
read -rsp 'PublicNode Mainnet Yellowstone x-token: ' PUBLICNODE_MAINNET_TOKEN
echo
read -rsp 'PublicNode Testnet Yellowstone x-token: ' PUBLICNODE_TESTNET_TOKEN
echo
printf '%s\n%s\n' "$PUBLICNODE_MAINNET_TOKEN" "$PUBLICNODE_TESTNET_TOKEN" \
| cargo test -p ksp-onchain-transport-lib \
--test yellowstone_publicnode_smoke \
-- --ignored --nocapture
unset PUBLICNODE_MAINNET_TOKEN PUBLICNODE_TESTNET_TOKEN
```
Endpoints validés :
```text
Mainnet https://solana-yellowstone-grpc.publicnode.com:443
Testnet https://solana-testnet-yellowstone-grpc.publicnode.com:443
```
Les profils Config conservent deux variables secrètes distinctes afin d'autoriser des credentials différents si nécessaire ; cette séparation ne signifie pas que PublicNode impose actuellement un token différent par réseau. Un timeout KSP de half-close après réception du slot est accepté par le smoke comme fermeture bornée du provider ; aucune absence de slot ni autre erreur n'est masquée.
Le smoke de **composition Config -> Transport** reste également disponible : Le smoke de **composition Config -> Transport** reste également disponible :
```bash ```bash
@@ -400,7 +529,7 @@ Il valide le profil committé `devnet_public` et les quatre canaris foundation.
### Smoke Helius live ### Smoke Helius live
Aucun nouveau test Helius live nest committé en `0.2.8-pre.010`. La raison est architecturale : Transport ne peut pas lire `KSP_SECRET_HELIUS_API_KEY` ni dépendre de Config, et Config ne doit pas devenir la destination générale des futurs smokes `Config + autre crate`. Créer un quatrième smoke dans lune de ces deux crates contournerait donc une frontière déjà documentée. Aucun nouveau test Helius live nest committé en `0.2.8-pre.010`. La raison est architecturale : Transport ne peut pas lire `KSP_SECRET_HELIUS_API_KEY` ni dépendre de Config, et Config ne doit pas devenir la destination générale des futurs smokes `Config + autre crate`. Créer un smoke Helius supplémentaire dans lune de ces deux crates contournerait donc une frontière déjà documentée.
Lorsque la surface KSP dintégration/orchestration dédiée existera, le smoke live minimal recommandé sera : Lorsque la surface KSP dintégration/orchestration dédiée existera, le smoke live minimal recommandé sera :
@@ -416,7 +545,7 @@ Config helius_devnet
Ce scénario utilise une méthode standard stable sur lendpoint Helius et teste donc auth + façade provider + actor + unsubscribe sans dépendre dune entitlement particulière de `transactionSubscribe`. Un smoke `transactionSubscribe` pourra être ajouté séparément comme opt-in provider-specific si lenvironnement opérateur possède les droits nécessaires ; il ne doit pas devenir un gate réseau obligatoire de la release. Ce scénario utilise une méthode standard stable sur lendpoint Helius et teste donc auth + façade provider + actor + unsubscribe sans dépendre dune entitlement particulière de `transactionSubscribe`. Un smoke `transactionSubscribe` pourra être ajouté séparément comme opt-in provider-specific si lenvironnement opérateur possède les droits nécessaires ; il ne doit pas devenir un gate réseau obligatoire de la release.
Les endpoints publics/provider sont des dépendances externes. Un rate-limit, refus dauth, entitlement absente ou incident réseau nest pas assimilé automatiquement à une régression locale ; les fixtures HTTP/WebSocket locales et les gates déterministes restent autoritaires. Les endpoints publics/provider sont des dépendances externes. Un rate-limit, refus dauth, entitlement absente ou incident réseau nest pas assimilé automatiquement à une régression locale ; les fixtures HTTP/WebSocket/gRPC locales et les gates déterministes restent autoritaires.
Pour auditer les dépendances, inspecter également le graphe effectif après résolution Cargo : Pour auditer les dépendances, inspecter également le graphe effectif après résolution Cargo :

View File

@@ -1,8 +1,16 @@
// file: crates/ksp-onchain-transport-lib/src/error.rs // file: crates/ksp-onchain-transport-lib/src/error.rs
// version: 4 // version: 7
/// Error code used when no logical endpoint can satisfy a request. /// Error code used when no logical endpoint can satisfy a request.
pub const ERROR_CODE_ENDPOINT_SELECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "endpoint_selection_failed"); pub const ERROR_CODE_ENDPOINT_SELECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "endpoint_selection_failed");
/// Error code used when a bounded Yellowstone gRPC request/update queue is exhausted.
pub const ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "grpc_backpressure_overflow");
/// Error code used when a Yellowstone gRPC channel cannot be prepared safely.
pub const ERROR_CODE_GRPC_CHANNEL_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "grpc_channel_failed");
/// Error code used when a Yellowstone gRPC subscribe session is no longer available to the caller.
pub const ERROR_CODE_GRPC_SESSION_CLOSED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "grpc_session_closed");
/// Error code used when a Yellowstone gRPC endpoint returns a remote gRPC status.
pub const ERROR_CODE_GRPC_STATUS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "grpc_status");
/// Error code used when an HTTP connection cannot be established. /// Error code used when an HTTP connection cannot be established.
pub const ERROR_CODE_HTTP_CONNECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "http_connection_failed"); pub const ERROR_CODE_HTTP_CONNECTION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("onchain_transport", "http_connection_failed");
/// Error code used when an HTTP request fails after a connection exists. /// Error code used when an HTTP request fails after a connection exists.

View File

@@ -0,0 +1,191 @@
// file: crates/ksp-onchain-transport-lib/src/grpc_channel.rs
// version: 4
/// Prepared or connected Yellowstone gRPC channel owned by KSP Transport.
///
/// The underlying Tonic channel, endpoint URL and request metadata remain private. Callers use the KSP-owned typed Yellowstone surfaces layered on this
/// physical channel instead of receiving a raw Tonic escape hatch.
#[derive(Clone)]
pub struct YellowstoneGrpcChannel {
endpoint_name: std::string::String,
provider: crate::YellowstoneGrpcProviderName,
cluster: crate::YellowstoneGrpcClusterName,
channel: tonic::transport::Channel,
metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>,
session: crate::YellowstoneGrpcSessionSettings,
}
impl YellowstoneGrpcChannel {
/// Prepares one lazy HTTP/2 channel without establishing a network connection.
///
/// HTTPS endpoints receive the KSP TLS configuration immediately, so invalid local TLS setup is rejected before a typed client is created. Tonic lazy
/// channels require an active Tokio runtime even though no socket is opened yet.
pub fn prepare(settings: &crate::YellowstoneGrpcEndpointSettings) -> ksp_core_lib::Result<Self> {
if let std::result::Result::Err(error) = settings.validate() {
return std::result::Result::Err(error);
}
if !settings.enabled() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "disabled Yellowstone gRPC endpoint cannot prepare a channel")
.with_context("endpoint_name", settings.name()),
);
}
if tokio::runtime::Handle::try_current().is_err() {
return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC channel preparation requires an active Tokio runtime"));
}
let endpoint = match build_tonic_endpoint(settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let channel = endpoint.connect_lazy();
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
endpoint_name = settings.name(),
provider = settings.provider().as_str(),
cluster = settings.cluster().as_str(),
tls = settings.url().uses_tls(),
metadata_count = settings.metadata().len(),
"prepared lazy Yellowstone gRPC channel"
);
return std::result::Result::Ok(Self::from_parts(settings, channel));
}
/// Establishes one Yellowstone gRPC HTTP/2 channel with bounded connect timeout and configured TLS roots.
pub async fn connect(settings: &crate::YellowstoneGrpcEndpointSettings) -> ksp_core_lib::Result<Self> {
if let std::result::Result::Err(error) = settings.validate() {
return std::result::Result::Err(error);
}
if !settings.enabled() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "disabled Yellowstone gRPC endpoint cannot connect")
.with_context("endpoint_name", settings.name()),
);
}
if tokio::runtime::Handle::try_current().is_err() {
return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC connection requires an active Tokio runtime"));
}
let endpoint = match build_tonic_endpoint(settings) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let channel = match endpoint.connect().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
endpoint_name = settings.name(),
provider = settings.provider().as_str(),
cluster = settings.cluster().as_str(),
tls = settings.url().uses_tls(),
"Yellowstone gRPC channel connection failed"
);
return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC channel connection failed"));
},
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
endpoint_name = settings.name(),
provider = settings.provider().as_str(),
cluster = settings.cluster().as_str(),
tls = settings.url().uses_tls(),
"connected Yellowstone gRPC channel"
);
return std::result::Result::Ok(Self::from_parts(settings, channel));
}
/// Returns the safe logical endpoint name.
#[must_use]
pub fn endpoint_name(&self) -> &str {
return self.endpoint_name.as_str();
}
/// Returns the open provider descriptor.
#[must_use]
pub const fn provider(&self) -> &crate::YellowstoneGrpcProviderName {
return &self.provider;
}
/// Returns the open cluster descriptor.
#[must_use]
pub const fn cluster(&self) -> &crate::YellowstoneGrpcClusterName {
return &self.cluster;
}
/// Creates the standard Solana Yellowstone unary facade over this physical channel.
#[must_use]
pub fn standard_unary_client(&self) -> crate::SolanaYellowstoneGrpcUnaryClient {
return crate::SolanaYellowstoneGrpcUnaryClient::new(self.channel.clone(), self.metadata.clone(), self.session.clone());
}
/// Opens one standard Solana Yellowstone bidirectional `Subscribe` session over this physical channel.
pub async fn open_standard_subscribe(
&self,
initial_request: crate::YellowstoneSubscribeRequest,
) -> ksp_core_lib::Result<crate::SolanaYellowstoneGrpcSubscribeSession> {
return crate::open_yellowstone_subscribe_session(
self.channel.clone(),
self.metadata.clone(),
self.session.clone(),
self.endpoint_name.clone(),
self.provider.clone(),
self.cluster.clone(),
initial_request,
)
.await;
}
fn from_parts(settings: &crate::YellowstoneGrpcEndpointSettings, channel: tonic::transport::Channel) -> Self {
return Self {
endpoint_name: settings.name().to_owned(),
provider: settings.provider().clone(),
cluster: settings.cluster().clone(),
channel,
metadata: settings.metadata().to_vec(),
session: settings.session().clone(),
};
}
}
impl std::fmt::Debug for YellowstoneGrpcChannel {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("YellowstoneGrpcChannel")
.field("endpoint_name", &self.endpoint_name)
.field("provider", &self.provider)
.field("cluster", &self.cluster)
.field("metadata_count", &self.metadata.len())
.field("channel", &"<private>")
.finish();
}
}
fn build_tonic_endpoint(settings: &crate::YellowstoneGrpcEndpointSettings) -> ksp_core_lib::Result<tonic::transport::Endpoint> {
let endpoint = match tonic::transport::Endpoint::from_shared(settings.url().as_str().to_owned()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC endpoint URI cannot be prepared by the HTTP/2 transport"));
},
};
let endpoint = endpoint.connect_timeout(settings.session().connect_timeout()).buffer_size(settings.session().request_channel_capacity());
if settings.url().uses_tls() {
let tls = tonic::transport::ClientTlsConfig::new().with_webpki_roots().timeout(settings.session().connect_timeout());
return match endpoint.tls_config(tls) {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => {
std::result::Result::Err(grpc_channel_error(settings, "Yellowstone gRPC TLS configuration failed before connection"))
},
};
}
return std::result::Result::Ok(endpoint);
}
fn grpc_channel_error(settings: &crate::YellowstoneGrpcEndpointSettings, message: &str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, message)
.with_context("endpoint_name", settings.name())
.with_context("provider", settings.provider().as_str())
.with_context("cluster", settings.cluster().as_str());
}
#[cfg(test)]
#[path = "../unit_tests/grpc_channel.rs"]
mod tests;

View File

@@ -0,0 +1,666 @@
// file: crates/ksp-onchain-transport-lib/src/grpc_settings.rs
// version: 2
const DEFAULT_GRPC_CLOSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
const DEFAULT_GRPC_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const DEFAULT_GRPC_MAX_INBOUND_MESSAGE_SIZE_BYTES: usize = 64 * 1024 * 1024;
const DEFAULT_GRPC_MAX_OUTBOUND_MESSAGE_SIZE_BYTES: usize = 64 * 1024 * 1024;
const DEFAULT_GRPC_RECONNECT_INITIAL_BACKOFF: std::time::Duration = std::time::Duration::from_millis(250);
const DEFAULT_GRPC_RECONNECT_MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(5);
const DEFAULT_GRPC_RECONNECT_MAX_RETRIES: u32 = 5;
const DEFAULT_GRPC_REQUEST_CHANNEL_CAPACITY: usize = 128;
const DEFAULT_GRPC_UNARY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
const DEFAULT_GRPC_UPDATE_CHANNEL_CAPACITY: usize = 256;
const MAX_GRPC_CHANNEL_CAPACITY: usize = 65_536;
const MAX_GRPC_DESCRIPTOR_LENGTH_BYTES: usize = 128;
const MAX_GRPC_ENDPOINT_COUNT: usize = 128;
const MAX_GRPC_ENDPOINT_URL_LENGTH_BYTES: usize = 8 * 1024;
const MAX_GRPC_MESSAGE_SIZE_BYTES: usize = 512 * 1024 * 1024;
const MAX_GRPC_METADATA_ENTRY_COUNT: usize = 64;
const MAX_GRPC_METADATA_KEY_LENGTH_BYTES: usize = 128;
const MAX_GRPC_METADATA_VALUE_LENGTH_BYTES: usize = 8 * 1024;
const MAX_GRPC_RECONNECT_RETRIES: u32 = 100;
const MAX_GRPC_RUNTIME_DURATION: std::time::Duration = std::time::Duration::from_secs(300);
/// Runtime Yellowstone gRPC endpoint URL owned by Transport.
///
/// The actual URL can contain provider credentials. Its [`std::fmt::Debug`] implementation is intentionally redacted.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneGrpcEndpointUrl {
value: std::string::String,
uses_tls: bool,
}
impl YellowstoneGrpcEndpointUrl {
/// Parses and validates one Yellowstone gRPC endpoint URL.
pub fn parse(value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, "validating Yellowstone gRPC endpoint URL");
let value = value.into();
if value.len() > MAX_GRPC_ENDPOINT_URL_LENGTH_BYTES {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint URL exceeds the KSP length bound")
.with_context("field", "grpc_endpoints.url"),
);
}
let parsed = match reqwest::Url::parse(value.as_str()) {
std::result::Result::Ok(parsed) => parsed,
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = "grpc_endpoints.url", "rejected invalid Yellowstone gRPC endpoint URL");
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint URL is invalid")
.with_context("field", "grpc_endpoints.url")
.with_source(error),
);
},
};
if parsed.scheme() != "http" && parsed.scheme() != "https" {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
field = "grpc_endpoints.url",
scheme = parsed.scheme(),
"rejected Yellowstone gRPC endpoint URL with unsupported scheme"
);
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint URL must use http or https")
.with_context("field", "grpc_endpoints.url")
.with_context("scheme", parsed.scheme()),
);
}
if parsed.host_str().is_none() {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = "grpc_endpoints.url", "rejected Yellowstone gRPC endpoint URL without host");
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint URL must contain a host")
.with_context("field", "grpc_endpoints.url"),
);
}
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, scheme = parsed.scheme(), "validated Yellowstone gRPC endpoint URL syntax");
return std::result::Result::Ok(Self { value, uses_tls: parsed.scheme() == "https" });
}
/// Returns the sensitive runtime URL text.
///
/// Callers must not write this value to logs, generic diagnostics or snapshots.
#[must_use]
pub fn as_str(&self) -> &str {
return self.value.as_str();
}
/// Returns whether this endpoint URL requires TLS.
#[must_use]
pub const fn uses_tls(&self) -> bool {
return self.uses_tls;
}
}
impl std::fmt::Debug for YellowstoneGrpcEndpointUrl {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("YellowstoneGrpcEndpointUrl(<redacted>)");
}
}
/// One validated ASCII metadata entry attached to Yellowstone gRPC requests.
///
/// Metadata values are intentionally omitted from [`std::fmt::Debug`] for both public and secret entries. Secret entries are additionally marked sensitive on
/// the Tonic metadata value before transmission so the HTTP/2 stack avoids indexing them where supported. Binary `*-bin` metadata is not part of the
/// `0.2.9-pre.003` contract.
#[derive(Clone, Eq, PartialEq)]
pub struct YellowstoneGrpcMetadataEntry {
key: std::string::String,
value: std::string::String,
secret: bool,
}
impl YellowstoneGrpcMetadataEntry {
/// Creates one non-secret ASCII metadata entry.
pub fn public(key: impl std::convert::Into<std::string::String>, value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
return Self::new(key.into(), value.into(), false);
}
/// Creates one secret ASCII metadata entry with redacted diagnostics.
pub fn secret(key: impl std::convert::Into<std::string::String>, value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
return Self::new(key.into(), value.into(), true);
}
/// Returns the validated metadata key.
#[must_use]
pub fn key(&self) -> &str {
return self.key.as_str();
}
/// Returns whether the value must be treated as secret by Transport.
#[must_use]
pub const fn is_secret(&self) -> bool {
return self.secret;
}
/// Appends this validated value to an internal Tonic metadata map while preserving its sensitivity marker.
pub(crate) fn append_to(&self, metadata: &mut tonic::metadata::MetadataMap) -> ksp_core_lib::Result<()> {
let key = match tonic::metadata::MetadataKey::<tonic::metadata::Ascii>::from_bytes(self.key.as_bytes()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata key is invalid")
.with_context("field", "grpc_endpoint.metadata.key"),
);
},
};
let mut value = match tonic::metadata::AsciiMetadataValue::try_from(self.value.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata value is invalid")
.with_context("field", "grpc_endpoint.metadata.value")
.with_context("metadata_key", self.key.as_str()),
);
},
};
value.set_sensitive(self.secret);
metadata.append(key, value);
return std::result::Result::Ok(());
}
fn new(key: std::string::String, value: std::string::String, secret: bool) -> ksp_core_lib::Result<Self> {
if key.is_empty()
|| key.len() > MAX_GRPC_METADATA_KEY_LENGTH_BYTES
|| key != key.to_ascii_lowercase()
|| key.starts_with("grpc-")
|| key.ends_with("-bin")
{
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata key violates the KSP ASCII metadata contract")
.with_context("field", "grpc_endpoint.metadata.key"),
);
}
if tonic::metadata::MetadataKey::<tonic::metadata::Ascii>::from_bytes(key.as_bytes()).is_err() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata key is invalid")
.with_context("field", "grpc_endpoint.metadata.key"),
);
}
if value.len() > MAX_GRPC_METADATA_VALUE_LENGTH_BYTES || tonic::metadata::AsciiMetadataValue::try_from(value.as_str()).is_err() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC metadata value violates the KSP ASCII metadata contract")
.with_context("field", "grpc_endpoint.metadata.value")
.with_context("metadata_key", key.as_str()),
);
}
return std::result::Result::Ok(Self { key, value, secret });
}
}
impl std::fmt::Debug for YellowstoneGrpcMetadataEntry {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("YellowstoneGrpcMetadataEntry")
.field("key", &self.key)
.field("secret", &self.secret)
.field("value", &"<redacted>")
.finish();
}
}
/// Open provider descriptor used by Yellowstone gRPC endpoint settings.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct YellowstoneGrpcProviderName {
value: std::string::String,
}
impl YellowstoneGrpcProviderName {
/// Creates an open provider descriptor. Validation is performed by [`YellowstoneGrpcTransportSettings::validate`].
#[must_use]
pub fn new(value: impl std::convert::Into<std::string::String>) -> Self {
return Self { value: value.into() };
}
/// Returns the provider descriptor text.
#[must_use]
pub fn as_str(&self) -> &str {
return self.value.as_str();
}
}
/// Open cluster or network descriptor used by Yellowstone gRPC endpoint settings.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct YellowstoneGrpcClusterName {
value: std::string::String,
}
impl YellowstoneGrpcClusterName {
/// Creates an open cluster descriptor. Validation is performed by [`YellowstoneGrpcTransportSettings::validate`].
#[must_use]
pub fn new(value: impl std::convert::Into<std::string::String>) -> Self {
return Self { value: value.into() };
}
/// Returns the cluster descriptor text.
#[must_use]
pub fn as_str(&self) -> &str {
return self.value.as_str();
}
}
/// Bounded reconnect settings owned by the Yellowstone gRPC runtime.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct YellowstoneGrpcReconnectSettings {
max_retries: u32,
initial_backoff: std::time::Duration,
max_backoff: std::time::Duration,
}
impl YellowstoneGrpcReconnectSettings {
/// Creates bounded reconnect settings.
#[must_use]
pub const fn new(max_retries: u32, initial_backoff: std::time::Duration, max_backoff: std::time::Duration) -> Self {
return Self { max_retries, initial_backoff, max_backoff };
}
/// Returns the number of reconnect attempts allowed after one live channel or stream is lost.
#[must_use]
pub const fn max_retries(&self) -> u32 {
return self.max_retries;
}
/// Returns the initial reconnect backoff.
#[must_use]
pub const fn initial_backoff(&self) -> std::time::Duration {
return self.initial_backoff;
}
/// Returns the maximum reconnect backoff.
#[must_use]
pub const fn max_backoff(&self) -> std::time::Duration {
return self.max_backoff;
}
}
impl std::default::Default for YellowstoneGrpcReconnectSettings {
fn default() -> Self {
return Self::new(DEFAULT_GRPC_RECONNECT_MAX_RETRIES, DEFAULT_GRPC_RECONNECT_INITIAL_BACKOFF, DEFAULT_GRPC_RECONNECT_MAX_BACKOFF);
}
}
/// Runtime limits and lifecycle settings for one Yellowstone gRPC physical channel/session path.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct YellowstoneGrpcSessionSettings {
connect_timeout: std::time::Duration,
unary_timeout: std::time::Duration,
close_timeout: std::time::Duration,
reconnect: crate::YellowstoneGrpcReconnectSettings,
request_channel_capacity: usize,
update_channel_capacity: usize,
max_inbound_message_size_bytes: usize,
max_outbound_message_size_bytes: usize,
}
impl YellowstoneGrpcSessionSettings {
/// Creates complete runtime settings for one physical Yellowstone gRPC channel/session path.
#[must_use]
#[allow(clippy::too_many_arguments)]
pub const fn new(
connect_timeout: std::time::Duration,
unary_timeout: std::time::Duration,
close_timeout: std::time::Duration,
reconnect: crate::YellowstoneGrpcReconnectSettings,
request_channel_capacity: usize,
update_channel_capacity: usize,
max_inbound_message_size_bytes: usize,
max_outbound_message_size_bytes: usize,
) -> Self {
return Self {
connect_timeout,
unary_timeout,
close_timeout,
reconnect,
request_channel_capacity,
update_channel_capacity,
max_inbound_message_size_bytes,
max_outbound_message_size_bytes,
};
}
/// Returns the connection establishment timeout.
#[must_use]
pub const fn connect_timeout(&self) -> std::time::Duration {
return self.connect_timeout;
}
/// Returns the default unary request timeout.
#[must_use]
pub const fn unary_timeout(&self) -> std::time::Duration {
return self.unary_timeout;
}
/// Returns the bounded graceful-close timeout.
#[must_use]
pub const fn close_timeout(&self) -> std::time::Duration {
return self.close_timeout;
}
/// Returns the reconnect policy.
#[must_use]
pub const fn reconnect(&self) -> &crate::YellowstoneGrpcReconnectSettings {
return &self.reconnect;
}
/// Returns the bounded outgoing Subscribe request channel capacity.
#[must_use]
pub const fn request_channel_capacity(&self) -> usize {
return self.request_channel_capacity;
}
/// Returns the bounded incoming update delivery channel capacity.
#[must_use]
pub const fn update_channel_capacity(&self) -> usize {
return self.update_channel_capacity;
}
/// Returns the maximum inbound protobuf message size accepted by KSP.
#[must_use]
pub const fn max_inbound_message_size_bytes(&self) -> usize {
return self.max_inbound_message_size_bytes;
}
/// Returns the maximum outbound protobuf message size accepted by KSP.
#[must_use]
pub const fn max_outbound_message_size_bytes(&self) -> usize {
return self.max_outbound_message_size_bytes;
}
/// Validates KSP-owned runtime bounds without reading Config or environment state.
pub fn validate(&self) -> ksp_core_lib::Result<()> {
if let std::result::Result::Err(error) = validate_grpc_duration(self.connect_timeout, "grpc_session.connect_timeout") {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = validate_grpc_duration(self.unary_timeout, "grpc_session.unary_timeout") {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = validate_grpc_duration(self.close_timeout, "grpc_session.close_timeout") {
return std::result::Result::Err(error);
}
if self.reconnect.max_retries() > MAX_GRPC_RECONNECT_RETRIES {
return grpc_invalid_settings("Yellowstone gRPC reconnect retry count exceeds the KSP runtime bound", "grpc_session.reconnect.max_retries");
}
if let std::result::Result::Err(error) = validate_grpc_duration(self.reconnect.initial_backoff(), "grpc_session.reconnect.initial_backoff") {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = validate_grpc_duration(self.reconnect.max_backoff(), "grpc_session.reconnect.max_backoff") {
return std::result::Result::Err(error);
}
if self.reconnect.initial_backoff() > self.reconnect.max_backoff() {
return grpc_invalid_settings("Yellowstone gRPC reconnect initial backoff must not exceed maximum backoff", "grpc_session.reconnect");
}
if let std::result::Result::Err(error) = validate_grpc_capacity(self.request_channel_capacity, "grpc_session.request_channel_capacity") {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = validate_grpc_capacity(self.update_channel_capacity, "grpc_session.update_channel_capacity") {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = validate_grpc_message_size(self.max_inbound_message_size_bytes, "grpc_session.max_inbound_message_size_bytes")
{
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) =
validate_grpc_message_size(self.max_outbound_message_size_bytes, "grpc_session.max_outbound_message_size_bytes")
{
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
request_channel_capacity = self.request_channel_capacity,
update_channel_capacity = self.update_channel_capacity,
max_inbound_message_size_bytes = self.max_inbound_message_size_bytes,
max_outbound_message_size_bytes = self.max_outbound_message_size_bytes,
reconnect_max_retries = self.reconnect.max_retries(),
"validated Yellowstone gRPC session settings"
);
return std::result::Result::Ok(());
}
}
impl std::default::Default for YellowstoneGrpcSessionSettings {
fn default() -> Self {
return Self::new(
DEFAULT_GRPC_CONNECT_TIMEOUT,
DEFAULT_GRPC_UNARY_TIMEOUT,
DEFAULT_GRPC_CLOSE_TIMEOUT,
crate::YellowstoneGrpcReconnectSettings::default(),
DEFAULT_GRPC_REQUEST_CHANNEL_CAPACITY,
DEFAULT_GRPC_UPDATE_CHANNEL_CAPACITY,
DEFAULT_GRPC_MAX_INBOUND_MESSAGE_SIZE_BYTES,
DEFAULT_GRPC_MAX_OUTBOUND_MESSAGE_SIZE_BYTES,
);
}
}
/// Runtime settings for one named Yellowstone gRPC endpoint.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct YellowstoneGrpcEndpointSettings {
name: std::string::String,
enabled: bool,
provider: crate::YellowstoneGrpcProviderName,
cluster: crate::YellowstoneGrpcClusterName,
url: crate::YellowstoneGrpcEndpointUrl,
session: crate::YellowstoneGrpcSessionSettings,
metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>,
}
impl YellowstoneGrpcEndpointSettings {
/// Creates explicit settings for one logical Yellowstone gRPC endpoint.
#[must_use]
pub fn new(
name: impl std::convert::Into<std::string::String>,
enabled: bool,
provider: crate::YellowstoneGrpcProviderName,
cluster: crate::YellowstoneGrpcClusterName,
url: crate::YellowstoneGrpcEndpointUrl,
session: crate::YellowstoneGrpcSessionSettings,
) -> Self {
return Self { name: name.into(), enabled, provider, cluster, url, session, metadata: std::vec::Vec::new() };
}
/// Returns the logical endpoint name.
#[must_use]
pub fn name(&self) -> &str {
return self.name.as_str();
}
/// Returns whether this endpoint can be used to prepare a physical channel.
#[must_use]
pub const fn enabled(&self) -> bool {
return self.enabled;
}
/// Returns the open provider descriptor.
#[must_use]
pub const fn provider(&self) -> &crate::YellowstoneGrpcProviderName {
return &self.provider;
}
/// Returns the open cluster descriptor.
#[must_use]
pub const fn cluster(&self) -> &crate::YellowstoneGrpcClusterName {
return &self.cluster;
}
/// Returns the sensitive Yellowstone gRPC endpoint URL wrapper.
#[must_use]
pub const fn url(&self) -> &crate::YellowstoneGrpcEndpointUrl {
return &self.url;
}
/// Returns the effective runtime settings for the channel/session path.
#[must_use]
pub const fn session(&self) -> &crate::YellowstoneGrpcSessionSettings {
return &self.session;
}
/// Returns metadata entries in declaration order without exposing their values.
#[must_use]
pub fn metadata(&self) -> &[crate::YellowstoneGrpcMetadataEntry] {
return self.metadata.as_slice();
}
/// Replaces request metadata after validating KSP bounds and ASCII metadata rules.
pub fn with_metadata(mut self, metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>) -> ksp_core_lib::Result<Self> {
if metadata.len() > MAX_GRPC_METADATA_ENTRY_COUNT {
return grpc_invalid_settings_value("Yellowstone gRPC metadata entry count exceeds the KSP bound", "grpc_endpoint.metadata");
}
self.metadata = metadata;
if let std::result::Result::Err(error) = self.validate() {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(self);
}
/// Validates this endpoint without performing network I/O.
pub fn validate(&self) -> ksp_core_lib::Result<()> {
return validate_grpc_endpoint(self, "grpc_endpoint");
}
}
/// Complete runtime settings consumed by the KSP Yellowstone gRPC transport engine.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct YellowstoneGrpcTransportSettings {
endpoints: std::vec::Vec<crate::YellowstoneGrpcEndpointSettings>,
}
impl YellowstoneGrpcTransportSettings {
/// Creates complete Yellowstone gRPC runtime settings.
#[must_use]
pub fn new(endpoints: std::vec::Vec<crate::YellowstoneGrpcEndpointSettings>) -> Self {
return Self { endpoints };
}
/// Returns configured Yellowstone gRPC endpoints in declaration order.
#[must_use]
pub fn endpoints(&self) -> &[crate::YellowstoneGrpcEndpointSettings] {
return self.endpoints.as_slice();
}
/// Validates structural runtime invariants without reading Config or environment state.
pub fn validate(&self) -> ksp_core_lib::Result<()> {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, endpoint_count = self.endpoints.len(), "validating Yellowstone gRPC transport settings");
if self.endpoints.is_empty() {
return grpc_invalid_settings("at least one Yellowstone gRPC endpoint must be configured", "grpc_endpoints");
}
if self.endpoints.len() > MAX_GRPC_ENDPOINT_COUNT {
return grpc_invalid_settings("Yellowstone gRPC endpoint count exceeds the KSP bound", "grpc_endpoints");
}
let mut enabled_endpoint_count = 0_usize;
for (endpoint_index, endpoint) in self.endpoints.iter().enumerate() {
let field_prefix = format!("grpc_endpoints[{endpoint_index}]");
if let std::result::Result::Err(error) = validate_grpc_endpoint(endpoint, field_prefix.as_str()) {
return std::result::Result::Err(error);
}
if endpoint.enabled() {
enabled_endpoint_count += 1;
}
for previous in &self.endpoints[..endpoint_index] {
if previous.name() == endpoint.name() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "Yellowstone gRPC endpoint names must be unique")
.with_context("field", format!("grpc_endpoints[{endpoint_index}].name"))
.with_context("endpoint_name", endpoint.name()),
);
}
}
}
if enabled_endpoint_count == 0 {
return grpc_invalid_settings("at least one Yellowstone gRPC endpoint must be enabled", "grpc_endpoints.enabled");
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
endpoint_count = self.endpoints.len(),
enabled_endpoint_count,
"validated Yellowstone gRPC transport settings"
);
return std::result::Result::Ok(());
}
}
fn validate_grpc_endpoint(endpoint: &crate::YellowstoneGrpcEndpointSettings, field_prefix: &str) -> ksp_core_lib::Result<()> {
let name_field = format!("{field_prefix}.name");
if let std::result::Result::Err(error) = validate_grpc_descriptor(endpoint.name(), name_field.as_str()) {
return std::result::Result::Err(error);
}
let provider_field = format!("{field_prefix}.provider");
if let std::result::Result::Err(error) = validate_grpc_descriptor(endpoint.provider().as_str(), provider_field.as_str()) {
return std::result::Result::Err(error);
}
let cluster_field = format!("{field_prefix}.cluster");
if let std::result::Result::Err(error) = validate_grpc_descriptor(endpoint.cluster().as_str(), cluster_field.as_str()) {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = endpoint.session().validate() {
return std::result::Result::Err(error);
}
if endpoint.metadata().len() > MAX_GRPC_METADATA_ENTRY_COUNT {
return grpc_invalid_settings("Yellowstone gRPC metadata entry count exceeds the KSP bound", "grpc_endpoint.metadata");
}
ksp_logging_lib::trace!(
target: crate::TRACING_TARGET,
endpoint_name = endpoint.name(),
provider = endpoint.provider().as_str(),
cluster = endpoint.cluster().as_str(),
enabled = endpoint.enabled(),
"validated Yellowstone gRPC endpoint settings"
);
return std::result::Result::Ok(());
}
fn validate_grpc_descriptor(value: &str, field: &str) -> ksp_core_lib::Result<()> {
if value.trim().is_empty() {
return grpc_invalid_settings("Yellowstone gRPC transport descriptor must not be empty", field);
}
if value.trim() != value {
return grpc_invalid_settings("Yellowstone gRPC transport descriptor must not contain leading or trailing whitespace", field);
}
if value.len() > MAX_GRPC_DESCRIPTOR_LENGTH_BYTES {
return grpc_invalid_settings("Yellowstone gRPC transport descriptor exceeds the KSP length bound", field);
}
return std::result::Result::Ok(());
}
fn validate_grpc_duration(value: std::time::Duration, field: &str) -> ksp_core_lib::Result<()> {
if value.is_zero() {
return grpc_invalid_settings("Yellowstone gRPC runtime duration must be greater than zero", field);
}
if value > MAX_GRPC_RUNTIME_DURATION {
return grpc_invalid_settings("Yellowstone gRPC runtime duration exceeds the KSP bound", field);
}
return std::result::Result::Ok(());
}
fn validate_grpc_capacity(value: usize, field: &str) -> ksp_core_lib::Result<()> {
if value == 0 {
return grpc_invalid_settings("Yellowstone gRPC channel capacity must be greater than zero", field);
}
if value > MAX_GRPC_CHANNEL_CAPACITY {
return grpc_invalid_settings("Yellowstone gRPC channel capacity exceeds the KSP bound", field);
}
return std::result::Result::Ok(());
}
fn validate_grpc_message_size(value: usize, field: &str) -> ksp_core_lib::Result<()> {
if value == 0 {
return grpc_invalid_settings("Yellowstone gRPC message size must be greater than zero", field);
}
if value > MAX_GRPC_MESSAGE_SIZE_BYTES {
return grpc_invalid_settings("Yellowstone gRPC message size exceeds the KSP bound", field);
}
return std::result::Result::Ok(());
}
fn grpc_invalid_settings(message: &str, field: &str) -> ksp_core_lib::Result<()> {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = field, reason = message, "rejected Yellowstone gRPC transport settings");
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, message).with_context("field", field));
}
fn grpc_invalid_settings_value<T>(message: &str, field: &str) -> ksp_core_lib::Result<T> {
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = field, reason = message, "rejected Yellowstone gRPC transport settings");
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, message).with_context("field", field));
}
#[cfg(test)]
#[path = "../unit_tests/grpc_settings.rs"]
mod tests;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,398 @@
// file: crates/ksp-onchain-transport-lib/src/grpc_unary.rs
// version: 1
const MAX_YELLOWSTONE_BLOCKHASH_LENGTH_BYTES: usize = 256;
const MAX_YELLOWSTONE_VERSION_LENGTH_BYTES: usize = 512;
const PATH_GET_BLOCK_HEIGHT: &str = "/geyser.Geyser/GetBlockHeight";
const PATH_GET_LATEST_BLOCKHASH: &str = "/geyser.Geyser/GetLatestBlockhash";
const PATH_GET_SLOT: &str = "/geyser.Geyser/GetSlot";
const PATH_GET_VERSION: &str = "/geyser.Geyser/GetVersion";
const PATH_IS_BLOCKHASH_VALID: &str = "/geyser.Geyser/IsBlockhashValid";
const PATH_PING: &str = "/geyser.Geyser/Ping";
const PATH_SUBSCRIBE_REPLAY_INFO: &str = "/geyser.Geyser/SubscribeReplayInfo";
/// Standard Solana Yellowstone unary client layered on one KSP-owned physical gRPC channel.
///
/// This N2 facade deliberately exposes no raw Tonic client and no upstream protobuf types. Streaming `Subscribe` is not part of `0.2.9-pre.003`.
#[derive(Clone)]
pub struct SolanaYellowstoneGrpcUnaryClient {
channel: tonic::transport::Channel,
metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>,
unary_timeout: std::time::Duration,
max_inbound_message_size_bytes: usize,
max_outbound_message_size_bytes: usize,
}
impl SolanaYellowstoneGrpcUnaryClient {
/// Creates the internal unary facade state from a KSP-owned physical channel and validated endpoint settings.
pub(crate) fn new(
channel: tonic::transport::Channel,
metadata: std::vec::Vec<crate::YellowstoneGrpcMetadataEntry>,
settings: crate::YellowstoneGrpcSessionSettings,
) -> Self {
return Self {
channel,
metadata,
unary_timeout: settings.unary_timeout(),
max_inbound_message_size_bytes: settings.max_inbound_message_size_bytes(),
max_outbound_message_size_bytes: settings.max_outbound_message_size_bytes(),
};
}
/// Returns the first slot retained by the endpoint for replay when advertised.
pub async fn subscribe_replay_info(&self) -> ksp_core_lib::Result<crate::YellowstoneReplayInfo> {
let response: yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse =
match self.unary("SubscribeReplayInfo", PATH_SUBSCRIBE_REPLAY_INFO, yellowstone_grpc_proto::geyser::SubscribeReplayInfoRequest {}).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::YellowstoneReplayInfo::new(response.first_available));
}
/// Executes the standard Yellowstone unary `Ping` RPC and verifies the echoed count.
pub async fn ping(&self, count: i32) -> ksp_core_lib::Result<crate::YellowstonePong> {
let response: yellowstone_grpc_proto::geyser::PongResponse =
match self.unary("Ping", PATH_PING, yellowstone_grpc_proto::geyser::PingRequest { count }).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if response.count != count {
return invalid_unary_response("Ping", "Yellowstone Ping response count does not match the request");
}
return std::result::Result::Ok(crate::YellowstonePong::new(response.count));
}
/// Returns the latest blockhash advertised by the Yellowstone endpoint at an optional commitment.
pub async fn get_latest_blockhash(
&self,
commitment: std::option::Option<crate::SolanaCommitment>,
) -> ksp_core_lib::Result<crate::YellowstoneLatestBlockhash> {
let response: yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse = match self
.unary(
"GetLatestBlockhash",
PATH_GET_LATEST_BLOCKHASH,
yellowstone_grpc_proto::geyser::GetLatestBlockhashRequest { commitment: commitment_to_wire(commitment) },
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if response.blockhash.is_empty() || response.blockhash.len() > MAX_YELLOWSTONE_BLOCKHASH_LENGTH_BYTES {
return invalid_unary_response("GetLatestBlockhash", "Yellowstone latest blockhash response violates the KSP text bound");
}
return std::result::Result::Ok(crate::YellowstoneLatestBlockhash::new(response.slot, response.blockhash, response.last_valid_block_height));
}
/// Returns the current block height at an optional commitment.
pub async fn get_block_height(&self, commitment: std::option::Option<crate::SolanaCommitment>) -> ksp_core_lib::Result<crate::YellowstoneBlockHeight> {
let response: yellowstone_grpc_proto::geyser::GetBlockHeightResponse = match self
.unary(
"GetBlockHeight",
PATH_GET_BLOCK_HEIGHT,
yellowstone_grpc_proto::geyser::GetBlockHeightRequest { commitment: commitment_to_wire(commitment) },
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::YellowstoneBlockHeight::new(response.block_height));
}
/// Returns the current slot at an optional commitment.
pub async fn get_slot(&self, commitment: std::option::Option<crate::SolanaCommitment>) -> ksp_core_lib::Result<crate::YellowstoneSlot> {
let response: yellowstone_grpc_proto::geyser::GetSlotResponse =
match self.unary("GetSlot", PATH_GET_SLOT, yellowstone_grpc_proto::geyser::GetSlotRequest { commitment: commitment_to_wire(commitment) }).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::YellowstoneSlot::new(response.slot));
}
/// Checks one opaque Solana blockhash at an optional commitment.
pub async fn is_blockhash_valid(
&self,
blockhash: impl std::convert::Into<std::string::String>,
commitment: std::option::Option<crate::SolanaCommitment>,
) -> ksp_core_lib::Result<crate::YellowstoneBlockhashValidity> {
let blockhash = blockhash.into();
if blockhash.is_empty() || blockhash.len() > MAX_YELLOWSTONE_BLOCKHASH_LENGTH_BYTES || blockhash.trim() != blockhash {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Yellowstone blockhash must be non-empty, bounded and untrimmed")
.with_context("grpc_operation", "IsBlockhashValid")
.with_context("field", "blockhash"),
);
}
let response: yellowstone_grpc_proto::geyser::IsBlockhashValidResponse = match self
.unary(
"IsBlockhashValid",
PATH_IS_BLOCKHASH_VALID,
yellowstone_grpc_proto::geyser::IsBlockhashValidRequest { blockhash, commitment: commitment_to_wire(commitment) },
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::YellowstoneBlockhashValidity::new(response.slot, response.valid));
}
/// Returns the endpoint's Yellowstone/validator version string after applying a deterministic KSP text bound.
pub async fn get_version(&self) -> ksp_core_lib::Result<crate::YellowstoneVersionInfo> {
let response: yellowstone_grpc_proto::geyser::GetVersionResponse =
match self.unary("GetVersion", PATH_GET_VERSION, yellowstone_grpc_proto::geyser::GetVersionRequest {}).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if response.version.is_empty() || response.version.len() > MAX_YELLOWSTONE_VERSION_LENGTH_BYTES {
return invalid_unary_response("GetVersion", "Yellowstone version response violates the KSP text bound");
}
return std::result::Result::Ok(crate::YellowstoneVersionInfo::new(response.version));
}
async fn unary<Request, Response>(&self, operation: &'static str, path: &'static str, message: Request) -> ksp_core_lib::Result<Response>
where
Request: tonic_prost::prost::Message + Send + Sync + 'static,
Response: tonic_prost::prost::Message + std::default::Default + Send + Sync + 'static,
{
let path = match path.parse::<http::uri::PathAndQuery>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "internal Yellowstone gRPC method path is invalid")
.with_context("grpc_operation", operation),
);
},
};
let mut grpc = tonic::client::Grpc::new(self.channel.clone())
.max_decoding_message_size(self.max_inbound_message_size_bytes)
.max_encoding_message_size(self.max_outbound_message_size_bytes);
let future = async {
if grpc.ready().await.is_err() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_CHANNEL_FAILED, "Yellowstone gRPC channel is not ready for unary dispatch")
.with_context("grpc_operation", operation),
);
}
let mut request = tonic::Request::new(message);
for entry in &self.metadata {
if let std::result::Result::Err(error) = entry.append_to(request.metadata_mut()) {
return std::result::Result::Err(error);
}
}
let response = match grpc.unary(request, path, tonic_prost::ProstCodec::<Request, Response>::default()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(status) => return std::result::Result::Err(grpc_status_error(operation, status)),
};
return std::result::Result::Ok(response.into_inner());
};
return match tokio::time::timeout(self.unary_timeout, future).await {
std::result::Result::Ok(result) => result,
std::result::Result::Err(_) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TIMEOUT, "Yellowstone gRPC unary operation exceeded the KSP deadline")
.with_context("grpc_operation", operation),
),
};
}
}
impl std::fmt::Debug for SolanaYellowstoneGrpcUnaryClient {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("SolanaYellowstoneGrpcUnaryClient")
.field("metadata_count", &self.metadata.len())
.field("unary_timeout", &self.unary_timeout)
.field("max_inbound_message_size_bytes", &self.max_inbound_message_size_bytes)
.field("max_outbound_message_size_bytes", &self.max_outbound_message_size_bytes)
.field("channel", &"<private>")
.finish();
}
}
/// Replay availability advertised by `SubscribeReplayInfo`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct YellowstoneReplayInfo {
first_available: std::option::Option<u64>,
}
impl YellowstoneReplayInfo {
/// Creates a replay-info projection.
#[must_use]
pub const fn new(first_available: std::option::Option<u64>) -> Self {
return Self { first_available };
}
/// Returns the first replayable slot when the endpoint advertises one.
#[must_use]
pub const fn first_available(&self) -> std::option::Option<u64> {
return self.first_available;
}
}
/// Echo returned by the Yellowstone unary `Ping` RPC.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct YellowstonePong {
count: i32,
}
impl YellowstonePong {
/// Creates a ping response projection.
#[must_use]
pub const fn new(count: i32) -> Self {
return Self { count };
}
/// Returns the echoed ping count.
#[must_use]
pub const fn count(&self) -> i32 {
return self.count;
}
}
/// Latest blockhash response returned by Yellowstone.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct YellowstoneLatestBlockhash {
slot: u64,
blockhash: std::string::String,
last_valid_block_height: u64,
}
impl YellowstoneLatestBlockhash {
/// Creates a latest-blockhash projection.
#[must_use]
pub fn new(slot: u64, blockhash: std::string::String, last_valid_block_height: u64) -> Self {
return Self { slot, blockhash, last_valid_block_height };
}
/// Returns the response slot.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
/// Returns the opaque Solana blockhash text.
#[must_use]
pub fn blockhash(&self) -> &str {
return self.blockhash.as_str();
}
/// Returns the last valid block height associated with the blockhash.
#[must_use]
pub const fn last_valid_block_height(&self) -> u64 {
return self.last_valid_block_height;
}
}
/// Current block height returned by Yellowstone.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct YellowstoneBlockHeight {
block_height: u64,
}
impl YellowstoneBlockHeight {
/// Creates a block-height projection.
#[must_use]
pub const fn new(block_height: u64) -> Self {
return Self { block_height };
}
/// Returns the block height.
#[must_use]
pub const fn block_height(&self) -> u64 {
return self.block_height;
}
}
/// Current slot returned by Yellowstone.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct YellowstoneSlot {
slot: u64,
}
impl YellowstoneSlot {
/// Creates a slot projection.
#[must_use]
pub const fn new(slot: u64) -> Self {
return Self { slot };
}
/// Returns the slot.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
}
/// Result of checking one blockhash with Yellowstone.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct YellowstoneBlockhashValidity {
slot: u64,
valid: bool,
}
impl YellowstoneBlockhashValidity {
/// Creates a blockhash-validity projection.
#[must_use]
pub const fn new(slot: u64, valid: bool) -> Self {
return Self { slot, valid };
}
/// Returns the response slot.
#[must_use]
pub const fn slot(&self) -> u64 {
return self.slot;
}
/// Returns whether the blockhash is valid at the requested commitment.
#[must_use]
pub const fn valid(&self) -> bool {
return self.valid;
}
}
/// Bounded Yellowstone endpoint version information.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct YellowstoneVersionInfo {
version: std::string::String,
}
impl YellowstoneVersionInfo {
/// Creates a version projection.
#[must_use]
pub fn new(version: std::string::String) -> Self {
return Self { version };
}
/// Returns the bounded endpoint version string.
#[must_use]
pub fn version(&self) -> &str {
return self.version.as_str();
}
}
fn commitment_to_wire(commitment: std::option::Option<crate::SolanaCommitment>) -> std::option::Option<i32> {
return commitment.map(|value| {
return match value {
crate::SolanaCommitment::Processed => yellowstone_grpc_proto::geyser::CommitmentLevel::Processed as i32,
crate::SolanaCommitment::Confirmed => yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed as i32,
crate::SolanaCommitment::Finalized => yellowstone_grpc_proto::geyser::CommitmentLevel::Finalized as i32,
};
});
}
fn grpc_status_error(operation: &'static str, status: tonic::Status) -> ksp_core_lib::Error {
let code = status.code().to_string();
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, grpc_operation = operation, grpc_code = code.as_str(), "Yellowstone gRPC unary endpoint returned a status");
return ksp_core_lib::Error::new(crate::ERROR_CODE_GRPC_STATUS, "Yellowstone gRPC endpoint returned a gRPC status")
.with_context("grpc_operation", operation)
.with_context("grpc_code", code);
}
fn invalid_unary_response<T>(operation: &'static str, message: &str) -> ksp_core_lib::Result<T> {
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RESPONSE, message).with_context("grpc_operation", operation));
}
#[cfg(test)]
#[path = "../unit_tests/grpc_unary.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/client.rs // file: crates/ksp-onchain-transport-lib/src/http_client.rs
// version: 7 // version: 8
/// Passive runtime availability reported for one logical HTTP endpoint or role. /// Passive runtime availability reported for one logical HTTP endpoint or role.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -510,5 +510,5 @@ fn build_reqwest_client(settings: &crate::HttpEndpointSettings) -> std::result::
} }
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/client.rs"] #[path = "../unit_tests/http_client.rs"]
mod tests; mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/executor.rs // file: crates/ksp-onchain-transport-lib/src/http_executor.rs
// version: 3 // version: 4
const HTTP_BAD_GATEWAY: u16 = 502; const HTTP_BAD_GATEWAY: u16 = 502;
const HTTP_GATEWAY_TIMEOUT: u16 = 504; const HTTP_GATEWAY_TIMEOUT: u16 = 504;
@@ -228,5 +228,5 @@ fn http_status_error(method: &crate::HttpRpcMethodDescriptor, status: u16) -> ks
} }
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/executor.rs"] #[path = "../unit_tests/http_executor.rs"]
mod tests; mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/pool.rs // file: crates/ksp-onchain-transport-lib/src/http_pool.rs
// version: 7 // version: 8
/// Safe snapshot of the logical HTTP endpoint pool. /// Safe snapshot of the logical HTTP endpoint pool.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@@ -616,5 +616,5 @@ fn request_timeout(role: &crate::HttpRoleName, request_kind: &crate::HttpRequest
} }
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/pool.rs"] #[path = "../unit_tests/http_pool.rs"]
mod tests; mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/resilience.rs // file: crates/ksp-onchain-transport-lib/src/http_resilience.rs
// version: 3 // version: 4
const DEFAULT_RATE_LIMIT_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(1); const DEFAULT_RATE_LIMIT_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(1);
const MAX_PROVIDER_RETRY_AFTER: std::time::Duration = std::time::Duration::from_secs(60); const MAX_PROVIDER_RETRY_AFTER: std::time::Duration = std::time::Duration::from_secs(60);
@@ -401,5 +401,5 @@ fn retry_backoff(settings: &crate::HttpRetrySettings, retry_number: u32) -> std:
} }
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/resilience.rs"] #[path = "../unit_tests/http_resilience.rs"]
mod tests; mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/settings.rs // file: crates/ksp-onchain-transport-lib/src/http_settings.rs
// version: 6 // version: 7
/// Runtime HTTP endpoint URL owned by Transport. /// Runtime HTTP endpoint URL owned by Transport.
/// ///
@@ -582,5 +582,5 @@ fn invalid_settings(message: &str, field: &str) -> ksp_core_lib::Result<()> {
} }
#[cfg(test)] #[cfg(test)]
#[path = "../unit_tests/settings.rs"] #[path = "../unit_tests/http_settings.rs"]
mod tests; mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/lib.rs // file: crates/ksp-onchain-transport-lib/src/lib.rs
// version: 34 // version: 44
#![warn(missing_docs)] #![warn(missing_docs)]
#![deny(unreachable_pub)] #![deny(unreachable_pub)]
@@ -31,14 +31,34 @@
//! `0.2.8-pre.005` adds the typed Helius `transactionSubscribe` request contract and provider filter/options validation. `0.2.8-pre.006` integrates the live //! `0.2.8-pre.005` adds the typed Helius `transactionSubscribe` request contract and provider filter/options validation. `0.2.8-pre.006` integrates the live
//! transaction handle and typed `transactionNotification` union into the same actor-owned registry, remote-ID remap, unsubscribe-race handling and //! transaction handle and typed `transactionNotification` union into the same actor-owned registry, remote-ID remap, unsubscribe-race handling and
//! per-subscription backpressure path. //! per-subscription backpressure path.
//! `0.2.9-pre.002` opens the Yellowstone gRPC N1 engine foundation with Transport-owned redacted settings, bounded reconnect/channel/message policies, the
//! published Yellowstone protobuf dependency and a lazy Tonic HTTP/2 channel wrapper that exposes no raw Tonic or upstream protobuf types.
//! `0.2.9-pre.003` adds bounded TLS/WebPKI connection establishment, generic redacted ASCII request metadata and the seven standard Yellowstone unary RPCs
//! through KSP-owned DTOs.
//! `0.2.9-pre.004` materializes the provider-neutral standard `SubscribeRequest` foundation: all seven named filter maps, global filter-name bounds/uniqueness,
//! commitment, ordered account-data slices, ping and `from_slot`. Family-specific account/slot filters land in `pre.005`; transaction/block filters remain
//! staged for `pre.007008`.
//! `0.2.9-pre.006` normalizes the five unambiguously HTTP-owned private implementation modules with an `http_` prefix while preserving shared `rpc_*`,
//! JSON-RPC, error and constants modules.
//! `0.2.9-pre.007` completes the standard transaction/transaction-status filters and storage-wire projections; `pre.008` completes Blocks, block-meta and
//! entry projections. `0.2.9-pre.009` promotes those protobuf bridges into runtime and opens one KSP-owned bounded bidirectional `Subscribe` session with
//! request mutation, automatic server-Ping reply, observable Pong, normal server half-close, terminal backpressure and bounded graceful shutdown.
//! `0.2.9-pre.010` activates the bounded KSP-owned reconnect policy, deterministic replay from the latest accepted request and highest observed slot,
//! conservative ReplayInfo-based continuity-gap observability and bounded duplicate observation without claiming exactly-once or lossless delivery.
mod client;
mod constants; mod constants;
mod error; mod error;
mod executor; mod grpc_channel;
mod grpc_settings;
mod grpc_stream;
mod grpc_subscribe;
mod grpc_unary;
mod http_client;
mod http_executor;
mod http_pool;
mod http_resilience;
mod http_settings;
mod json_rpc; mod json_rpc;
mod pool;
mod resilience;
mod rpc_accounts; mod rpc_accounts;
mod rpc_blocks; mod rpc_blocks;
mod rpc_canary; mod rpc_canary;
@@ -48,7 +68,6 @@ mod rpc_economics;
mod rpc_method; mod rpc_method;
mod rpc_tokens; mod rpc_tokens;
mod rpc_transactions; mod rpc_transactions;
mod settings;
mod ws_accounts; mod ws_accounts;
mod ws_blocks; mod ws_blocks;
mod ws_cluster; mod ws_cluster;
@@ -60,16 +79,16 @@ mod ws_settings;
mod ws_subscription; mod ws_subscription;
mod ws_transactions; mod ws_transactions;
/// Passive runtime availability reported for one logical HTTP endpoint.
pub use self::client::HttpEndpointAvailability;
/// Shareable logical HTTP endpoint client owned by KSP Transport.
pub use self::client::HttpEndpointClient;
/// Safe routing snapshot for one configured endpoint role.
pub use self::client::HttpEndpointRoleSnapshot;
/// Safe metadata snapshot for one logical HTTP endpoint.
pub use self::client::HttpEndpointSnapshot;
/// Error code used when no logical endpoint can satisfy a request. /// Error code used when no logical endpoint can satisfy a request.
pub use self::error::ERROR_CODE_ENDPOINT_SELECTION_FAILED; pub use self::error::ERROR_CODE_ENDPOINT_SELECTION_FAILED;
/// Error code used when bounded Yellowstone gRPC runtime capacity is exhausted.
pub use self::error::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW;
/// Error code used when a Yellowstone gRPC channel cannot be prepared safely.
pub use self::error::ERROR_CODE_GRPC_CHANNEL_FAILED;
/// Error code used when a Yellowstone gRPC subscribe session is no longer available.
pub use self::error::ERROR_CODE_GRPC_SESSION_CLOSED;
/// Error code used when a Yellowstone gRPC endpoint returns a remote status.
pub use self::error::ERROR_CODE_GRPC_STATUS;
/// Error code used when an HTTP connection cannot be established. /// Error code used when an HTTP connection cannot be established.
pub use self::error::ERROR_CODE_HTTP_CONNECTION_FAILED; pub use self::error::ERROR_CODE_HTTP_CONNECTION_FAILED;
/// Error code used when an HTTP request fails after connection establishment. /// Error code used when an HTTP request fails after connection establishment.
@@ -102,6 +121,192 @@ pub use self::error::ERROR_CODE_WS_CONNECTION_FAILED;
pub use self::error::ERROR_CODE_WS_PROTOCOL_ERROR; pub use self::error::ERROR_CODE_WS_PROTOCOL_ERROR;
/// Error code used when a WebSocket session is no longer available. /// Error code used when a WebSocket session is no longer available.
pub use self::error::ERROR_CODE_WS_SESSION_CLOSED; pub use self::error::ERROR_CODE_WS_SESSION_CLOSED;
/// Yellowstone gRPC channel owned by KSP Transport.
pub use self::grpc_channel::YellowstoneGrpcChannel;
/// Open cluster or network descriptor used by Yellowstone gRPC endpoint settings.
pub use self::grpc_settings::YellowstoneGrpcClusterName;
/// Runtime settings for one named Yellowstone gRPC endpoint.
pub use self::grpc_settings::YellowstoneGrpcEndpointSettings;
/// Runtime Yellowstone gRPC endpoint URL with redacted diagnostics.
pub use self::grpc_settings::YellowstoneGrpcEndpointUrl;
/// Validated public or secret ASCII metadata attached to Yellowstone gRPC requests.
pub use self::grpc_settings::YellowstoneGrpcMetadataEntry;
/// Open provider descriptor used by Yellowstone gRPC endpoint settings.
pub use self::grpc_settings::YellowstoneGrpcProviderName;
/// Bounded reconnect settings owned by the Yellowstone gRPC runtime.
pub use self::grpc_settings::YellowstoneGrpcReconnectSettings;
/// Runtime limits and lifecycle settings for one Yellowstone gRPC channel/session path.
pub use self::grpc_settings::YellowstoneGrpcSessionSettings;
/// Complete runtime settings consumed by the KSP Yellowstone gRPC transport engine.
pub use self::grpc_settings::YellowstoneGrpcTransportSettings;
/// Standard Yellowstone bidirectional Subscribe session.
pub use self::grpc_stream::SolanaYellowstoneGrpcSubscribeSession;
/// Safe Yellowstone reconnect/replay continuity snapshot.
pub use self::grpc_stream::YellowstoneGrpcSubscribeSnapshot;
/// Safe Yellowstone bidirectional Subscribe lifecycle state.
pub use self::grpc_stream::YellowstoneGrpcSubscribeState;
/// One validated standard Yellowstone account predicate.
pub use self::grpc_subscribe::YellowstoneAccountFilterPredicate;
/// Typed account payload carried by one standard Yellowstone account update.
pub use self::grpc_subscribe::YellowstoneAccountInfo;
/// Lamport comparison used by standard Yellowstone account filters.
pub use self::grpc_subscribe::YellowstoneAccountLamportsFilter;
/// Validated standard Yellowstone account memcmp predicate.
pub use self::grpc_subscribe::YellowstoneAccountMemcmp;
/// Encoding selected by one Yellowstone account memcmp predicate.
pub use self::grpc_subscribe::YellowstoneAccountMemcmpEncoding;
/// Standard Yellowstone account-update projection owned by KSP.
pub use self::grpc_subscribe::YellowstoneAccountUpdate;
/// One standard Yellowstone account-data slice.
pub use self::grpc_subscribe::YellowstoneAccountsDataSlice;
/// Metadata-only standard Yellowstone block update.
pub use self::grpc_subscribe::YellowstoneBlockMetaUpdate;
/// Rewards container carried by Yellowstone block and block-meta updates.
pub use self::grpc_subscribe::YellowstoneBlockRewards;
/// Full standard Yellowstone block update.
pub use self::grpc_subscribe::YellowstoneBlockUpdate;
/// One compiled instruction from the Yellowstone Solana-storage transaction wire.
pub use self::grpc_subscribe::YellowstoneCompiledInstruction;
/// Wire-preserving KSP representation of a standard Yellowstone Cuckoo filter.
pub use self::grpc_subscribe::YellowstoneCuckooFilter;
/// Hash algorithm carried by a standard Yellowstone Cuckoo filter.
pub use self::grpc_subscribe::YellowstoneCuckooHashAlgorithm;
/// One Yellowstone block-entry payload reused by block and standalone entry updates.
pub use self::grpc_subscribe::YellowstoneEntryInfo;
/// Standalone standard Yellowstone entry update.
pub use self::grpc_subscribe::YellowstoneEntryUpdate;
/// Fixed-width 32-byte hash from the Yellowstone Solana-storage transaction wire.
pub use self::grpc_subscribe::YellowstoneHashBytes;
/// One inner instruction from Yellowstone transaction status metadata.
pub use self::grpc_subscribe::YellowstoneInnerInstruction;
/// One indexed inner-instruction group from Yellowstone transaction status metadata.
pub use self::grpc_subscribe::YellowstoneInnerInstructions;
/// One address-table lookup from a Yellowstone transaction message.
pub use self::grpc_subscribe::YellowstoneMessageAddressTableLookup;
/// Return-data payload from Yellowstone transaction status metadata.
pub use self::grpc_subscribe::YellowstoneReturnData;
/// One reward entry from Yellowstone transaction status metadata.
pub use self::grpc_subscribe::YellowstoneReward;
/// Reward classification from the Yellowstone Solana-storage wire.
pub use self::grpc_subscribe::YellowstoneRewardType;
/// Current standard Yellowstone slot status.
pub use self::grpc_subscribe::YellowstoneSlotStatus;
/// Standard Yellowstone slot-update projection owned by KSP.
pub use self::grpc_subscribe::YellowstoneSlotUpdate;
/// Solana transaction body carried by Yellowstone storage protobuf messages.
pub use self::grpc_subscribe::YellowstoneStoredTransaction;
/// Complete account-family filter group for standard Yellowstone Subscribe.
pub use self::grpc_subscribe::YellowstoneSubscribeAccountFilter;
/// Complete block-family filter group for standard Yellowstone Subscribe.
pub use self::grpc_subscribe::YellowstoneSubscribeBlockFilter;
/// Empty filter marker activating the standard Yellowstone blocks-meta family.
pub use self::grpc_subscribe::YellowstoneSubscribeBlocksMetaFilter;
/// Empty filter marker activating the standard Yellowstone entry family.
pub use self::grpc_subscribe::YellowstoneSubscribeEntryFilter;
/// Validated globally unique logical filter name for standard Yellowstone Subscribe maps.
pub use self::grpc_subscribe::YellowstoneSubscribeFilterName;
/// Optional ping mutation carried by the standard Yellowstone Subscribe request.
pub use self::grpc_subscribe::YellowstoneSubscribePing;
/// Standard Yellowstone server Ping update.
pub use self::grpc_subscribe::YellowstoneSubscribePingUpdate;
/// Standard Yellowstone server Pong update.
pub use self::grpc_subscribe::YellowstoneSubscribePongUpdate;
/// Provider-neutral standard Yellowstone Subscribe request.
pub use self::grpc_subscribe::YellowstoneSubscribeRequest;
/// Complete slot-family filter group for standard Yellowstone Subscribe.
pub use self::grpc_subscribe::YellowstoneSubscribeSlotFilter;
/// Complete transaction-family filter shared by transactions and transaction-status maps.
pub use self::grpc_subscribe::YellowstoneSubscribeTransactionFilter;
/// Any standard Yellowstone Subscribe update.
pub use self::grpc_subscribe::YellowstoneSubscribeUpdate;
/// Optional token-account owner expansion for current Yellowstone transaction filters.
pub use self::grpc_subscribe::YellowstoneTokenAccountExpansion;
/// One pre/post token balance from Yellowstone transaction status metadata.
pub use self::grpc_subscribe::YellowstoneTokenBalance;
/// Optional Transaction V1 inline budget configuration from Yellowstone Solana-storage.
pub use self::grpc_subscribe::YellowstoneTransactionConfig;
/// Opaque runtime transaction error bytes from Yellowstone Solana-storage.
pub use self::grpc_subscribe::YellowstoneTransactionError;
/// Complete transaction info carried by Yellowstone transaction and block updates.
pub use self::grpc_subscribe::YellowstoneTransactionInfo;
/// Complete current Yellowstone transaction message.
pub use self::grpc_subscribe::YellowstoneTransactionMessage;
/// Solana transaction message header from Yellowstone Solana-storage.
pub use self::grpc_subscribe::YellowstoneTransactionMessageHeader;
/// Fixed-width transaction signature attached to Yellowstone updates.
pub use self::grpc_subscribe::YellowstoneTransactionSignature;
/// Validated base58 transaction-signature selector for Yellowstone transaction filters.
pub use self::grpc_subscribe::YellowstoneTransactionSignatureSelector;
/// Complete Yellowstone transaction status metadata.
pub use self::grpc_subscribe::YellowstoneTransactionStatusMeta;
/// Lightweight Yellowstone transaction-status update.
pub use self::grpc_subscribe::YellowstoneTransactionStatusUpdate;
/// Full Yellowstone transaction update.
pub use self::grpc_subscribe::YellowstoneTransactionUpdate;
/// UI token amount from Yellowstone transaction status metadata.
pub use self::grpc_subscribe::YellowstoneUiTokenAmount;
/// Timestamp attached to standard Yellowstone update envelopes.
pub use self::grpc_subscribe::YellowstoneUpdateTimestamp;
/// Standard Solana Yellowstone unary facade over one KSP-owned physical gRPC channel.
pub use self::grpc_unary::SolanaYellowstoneGrpcUnaryClient;
/// Block height returned by the standard Yellowstone unary surface.
pub use self::grpc_unary::YellowstoneBlockHeight;
/// Result of a standard Yellowstone blockhash-validity check.
pub use self::grpc_unary::YellowstoneBlockhashValidity;
/// Latest blockhash returned by the standard Yellowstone unary surface.
pub use self::grpc_unary::YellowstoneLatestBlockhash;
/// Echo returned by the standard Yellowstone unary Ping RPC.
pub use self::grpc_unary::YellowstonePong;
/// Replay availability advertised by the standard Yellowstone unary surface.
pub use self::grpc_unary::YellowstoneReplayInfo;
/// Current slot returned by the standard Yellowstone unary surface.
pub use self::grpc_unary::YellowstoneSlot;
/// Bounded endpoint version returned by the standard Yellowstone unary surface.
pub use self::grpc_unary::YellowstoneVersionInfo;
/// Passive runtime availability reported for one logical HTTP endpoint.
pub use self::http_client::HttpEndpointAvailability;
/// Shareable logical HTTP endpoint client owned by KSP Transport.
pub use self::http_client::HttpEndpointClient;
/// Safe routing snapshot for one configured endpoint role.
pub use self::http_client::HttpEndpointRoleSnapshot;
/// Safe metadata snapshot for one logical HTTP endpoint.
pub use self::http_client::HttpEndpointSnapshot;
/// Result of one logical endpoint selection.
pub use self::http_pool::HttpEndpointSelection;
/// Runtime admission permit for one HTTP request.
pub use self::http_pool::HttpRequestPermit;
/// Shareable logical HTTP endpoint pool with priority routing, admission limits and bounded deadlines.
pub use self::http_pool::HttpTransportPool;
/// Safe snapshot of the logical HTTP endpoint pool.
pub use self::http_pool::HttpTransportPoolSnapshot;
/// Dispatch knowledge used to prevent ambiguous automatic resubmission.
pub use self::http_resilience::HttpDispatchState;
/// Transport-level cause considered by the bounded retry policy.
pub use self::http_resilience::HttpRetryCause;
/// Result of evaluating one bounded transport retry opportunity.
pub use self::http_resilience::HttpRetryDecision;
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method.
pub use self::http_resilience::evaluate_transport_retry;
/// Open cluster or network descriptor used by HTTP endpoint settings.
pub use self::http_settings::HttpClusterName;
/// Runtime settings for one role declared by an HTTP endpoint.
pub use self::http_settings::HttpEndpointRoleSettings;
/// Runtime settings for one named Solana HTTP endpoint.
pub use self::http_settings::HttpEndpointSettings;
/// Runtime HTTP endpoint URL with redacted diagnostics.
pub use self::http_settings::HttpEndpointUrl;
/// Open provider descriptor used by HTTP endpoint settings.
pub use self::http_settings::HttpProviderName;
/// Open request-kind descriptor used by logical endpoint capabilities.
pub use self::http_settings::HttpRequestKind;
/// Bounded retry settings owned by the HTTP transport runtime.
pub use self::http_settings::HttpRetrySettings;
/// Local limits attached to one logical HTTP endpoint role.
pub use self::http_settings::HttpRoleLimits;
/// Open logical endpoint role descriptor.
pub use self::http_settings::HttpRoleName;
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
pub use self::http_settings::HttpTransportSettings;
/// JSON-RPC 2.0 error payload returned by a remote Solana endpoint. /// JSON-RPC 2.0 error payload returned by a remote Solana endpoint.
pub use self::json_rpc::JsonRpcErrorObject; pub use self::json_rpc::JsonRpcErrorObject;
/// Validated JSON-RPC 2.0 error response. /// Validated JSON-RPC 2.0 error response.
@@ -116,22 +321,6 @@ pub use self::json_rpc::JsonRpcSuccessResponse;
pub use self::json_rpc::parse_json_rpc_response_text; pub use self::json_rpc::parse_json_rpc_response_text;
/// Validates a decoded JSON value as one JSON-RPC HTTP response. /// Validates a decoded JSON value as one JSON-RPC HTTP response.
pub use self::json_rpc::parse_json_rpc_response_value; pub use self::json_rpc::parse_json_rpc_response_value;
/// Result of one logical endpoint selection.
pub use self::pool::HttpEndpointSelection;
/// Runtime admission permit for one HTTP request.
pub use self::pool::HttpRequestPermit;
/// Shareable logical HTTP endpoint pool with priority routing, admission limits and bounded deadlines.
pub use self::pool::HttpTransportPool;
/// Safe snapshot of the logical HTTP endpoint pool.
pub use self::pool::HttpTransportPoolSnapshot;
/// Dispatch knowledge used to prevent ambiguous automatic resubmission.
pub use self::resilience::HttpDispatchState;
/// Transport-level cause considered by the bounded retry policy.
pub use self::resilience::HttpRetryCause;
/// Result of evaluating one bounded transport retry opportunity.
pub use self::resilience::HttpRetryDecision;
/// Evaluates the centralized bounded HTTP retry policy for one audited RPC method.
pub use self::resilience::evaluate_transport_retry;
/// Typed transport-level Solana account without Program/SPL decoding. /// Typed transport-level Solana account without Program/SPL decoding.
pub use self::rpc_accounts::SolanaAccount; pub use self::rpc_accounts::SolanaAccount;
/// Address and lamport balance returned by `getLargestAccounts`. /// Address and lamport balance returned by `getLargestAccounts`.
@@ -304,26 +493,6 @@ pub use self::rpc_transactions::SolanaTransactionEncoding;
pub use self::rpc_transactions::SolanaTransactionVersion; pub use self::rpc_transactions::SolanaTransactionVersion;
/// Three-state wire field used when Solana distinguishes omission from an explicit JSON `null`. /// Three-state wire field used when Solana distinguishes omission from an explicit JSON `null`.
pub use self::rpc_transactions::SolanaWireField; pub use self::rpc_transactions::SolanaWireField;
/// Open cluster or network descriptor used by HTTP endpoint settings.
pub use self::settings::HttpClusterName;
/// Runtime settings for one role declared by an HTTP endpoint.
pub use self::settings::HttpEndpointRoleSettings;
/// Runtime settings for one named Solana HTTP endpoint.
pub use self::settings::HttpEndpointSettings;
/// Runtime HTTP endpoint URL with redacted diagnostics.
pub use self::settings::HttpEndpointUrl;
/// Open provider descriptor used by HTTP endpoint settings.
pub use self::settings::HttpProviderName;
/// Open request-kind descriptor used by logical endpoint capabilities.
pub use self::settings::HttpRequestKind;
/// Bounded retry settings owned by the HTTP transport runtime.
pub use self::settings::HttpRetrySettings;
/// Local limits attached to one logical HTTP endpoint role.
pub use self::settings::HttpRoleLimits;
/// Open logical endpoint role descriptor.
pub use self::settings::HttpRoleName;
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
pub use self::settings::HttpTransportSettings;
/// Configuration accepted by the standard Solana `accountSubscribe` WebSocket method. /// Configuration accepted by the standard Solana `accountSubscribe` WebSocket method.
pub use self::ws_accounts::SolanaAccountSubscribeConfig; pub use self::ws_accounts::SolanaAccountSubscribeConfig;
/// One `programNotification` payload preserving contextual and non-contextual upstream forms. /// One `programNotification` payload preserving contextual and non-contextual upstream forms.
@@ -411,18 +580,24 @@ pub use self::ws_transactions::SolanaSignatureSubscribeConfig;
/// Owning tracing target for events emitted by the on-chain transport crate. /// Owning tracing target for events emitted by the on-chain transport crate.
pub(crate) use self::constants::TRACING_TARGET; pub(crate) use self::constants::TRACING_TARGET;
/// Internal Yellowstone Subscribe session opener used by the physical channel.
pub(crate) use self::grpc_stream::open_yellowstone_subscribe_session;
/// Internal Yellowstone Subscribe request wire conversion shared with the stream engine.
pub(crate) use self::grpc_subscribe::yellowstone_subscribe_request_to_wire;
/// Internal Yellowstone Subscribe update decoder shared with the stream engine.
pub(crate) use self::grpc_subscribe::yellowstone_subscribe_update_from_wire;
/// Crate-internal `HttpConcurrencyPermit` state shared across the owning crate. /// Crate-internal `HttpConcurrencyPermit` state shared across the owning crate.
pub(crate) use self::resilience::HttpConcurrencyPermit; pub(crate) use self::http_resilience::HttpConcurrencyPermit;
/// Crate-internal `HttpRoleRuntime` state shared across the owning crate. /// Crate-internal `HttpRoleRuntime` state shared across the owning crate.
pub(crate) use self::resilience::HttpRoleRuntime; pub(crate) use self::http_resilience::HttpRoleRuntime;
/// Crate-internal `RoleAdmissionAttempt` variants used by the owning crate. /// Crate-internal `RoleAdmissionAttempt` variants used by the owning crate.
pub(crate) use self::resilience::RoleAdmissionAttempt; pub(crate) use self::http_resilience::RoleAdmissionAttempt;
/// Validates endpoint settings.
pub(crate) use self::http_settings::validate_endpoint_settings;
/// Decodes one private serde wire type into the shared Transport error domain for typed RPC adapters. /// Decodes one private serde wire type into the shared Transport error domain for typed RPC adapters.
pub(crate) use self::rpc_common::decode_wire_json; pub(crate) use self::rpc_common::decode_wire_json;
/// Parses a base58 public key without echoing its wire value into diagnostics for typed RPC adapters. /// Parses a base58 public key without echoing its wire value into diagnostics for typed RPC adapters.
pub(crate) use self::rpc_common::parse_wire_pubkey; pub(crate) use self::rpc_common::parse_wire_pubkey;
/// Validates endpoint settings.
pub(crate) use self::settings::validate_endpoint_settings;
/// Crate-internal command surface shared by the physical session and typed subscription handle. /// Crate-internal command surface shared by the physical session and typed subscription handle.
pub(crate) use self::ws_session::WsSessionCommand; pub(crate) use self::ws_session::WsSessionCommand;
/// Crate-internal notification dispatch result. /// Crate-internal notification dispatch result.

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-onchain-transport-lib/src/rpc_common.rs // file: crates/ksp-onchain-transport-lib/src/rpc_common.rs
// version: 6 // version: 7
/// Commitment level accepted by typed Solana HTTP and WebSocket adapters. /// Commitment level accepted by typed Solana HTTP, WebSocket and Yellowstone gRPC adapters.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum SolanaCommitment { pub enum SolanaCommitment {
/// Query the most recent processed bank. /// Query the most recent processed bank.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs // file: crates/ksp-onchain-transport-lib/tests/public_api.rs
// version: 38 // version: 48
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract. //! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
@@ -760,3 +760,255 @@ fn public_v0_2_8_pre_009_helius_slots_updates_surface_reuses_shared_typed_contra
let _shared_slot_update = std::any::type_name::<ksp_onchain_transport_lib::SolanaSlotUpdate>(); let _shared_slot_update = std::any::type_name::<ksp_onchain_transport_lib::SolanaSlotUpdate>();
assert_eq!(ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates.as_str(), "slots_updates"); assert_eq!(ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates.as_str(), "slots_updates");
} }
#[tokio::test(flavor = "current_thread")]
async fn public_v0_2_9_pre_002_yellowstone_engine_settings_and_lazy_channel_are_available_from_crate_root() {
let url = ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse("http://127.0.0.1:10000")
.expect("public Yellowstone gRPC URL parser must accept HTTP fixture endpoint");
let session = ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::default();
assert!(session.validate().is_ok());
let endpoint = ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
"fixture",
true,
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("fixture-provider"),
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new("devnet"),
url,
session,
);
let settings = ksp_onchain_transport_lib::YellowstoneGrpcTransportSettings::new(std::vec![endpoint.clone()]);
assert!(settings.validate().is_ok());
let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::prepare(&endpoint);
assert!(channel.is_ok());
let channel = match channel {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(channel.endpoint_name(), "fixture");
assert_eq!(channel.provider().as_str(), "fixture-provider");
assert_eq!(channel.cluster().as_str(), "devnet");
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_CHANNEL_FAILED.domain(), "onchain_transport");
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_CHANNEL_FAILED.code(), "grpc_channel_failed");
}
#[test]
fn public_v0_2_9_pre_003_yellowstone_metadata_and_seven_unary_contracts_are_available_from_crate_root() {
let public = ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::public("x-ksp-public", "fixture");
assert!(public.is_ok());
let secret = ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::secret("x-ksp-token", "secret");
assert!(secret.is_ok());
let replay = ksp_onchain_transport_lib::YellowstoneReplayInfo::new(std::option::Option::Some(1));
assert_eq!(replay.first_available(), std::option::Option::Some(1));
let pong = ksp_onchain_transport_lib::YellowstonePong::new(2);
assert_eq!(pong.count(), 2);
let latest = ksp_onchain_transport_lib::YellowstoneLatestBlockhash::new(3, "hash".to_owned(), 4);
assert_eq!(latest.slot(), 3);
assert_eq!(latest.blockhash(), "hash");
assert_eq!(latest.last_valid_block_height(), 4);
assert_eq!(ksp_onchain_transport_lib::YellowstoneBlockHeight::new(5).block_height(), 5);
assert_eq!(ksp_onchain_transport_lib::YellowstoneSlot::new(6).slot(), 6);
let validity = ksp_onchain_transport_lib::YellowstoneBlockhashValidity::new(7, true);
assert_eq!(validity.slot(), 7);
assert!(validity.valid());
assert_eq!(ksp_onchain_transport_lib::YellowstoneVersionInfo::new("v".to_owned()).version(), "v");
let _replay_info = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::subscribe_replay_info;
let _ping = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::ping;
let _latest_blockhash = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::get_latest_blockhash;
let _block_height = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::get_block_height;
let _slot = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::get_slot;
async fn call_is_blockhash_valid(
client: &ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneBlockhashValidity> {
return client.is_blockhash_valid("fixture-blockhash", std::option::Option::None).await;
}
let _is_blockhash_valid = call_is_blockhash_valid;
let _version = ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient::get_version;
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_STATUS.domain(), "onchain_transport");
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_STATUS.code(), "grpc_status");
}
#[test]
fn public_v0_2_9_pre_004_yellowstone_subscribe_common_contract_is_available_from_crate_root() {
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
let account_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("accounts").expect("account filter name must validate");
let slot_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("slots").expect("slot filter name must validate");
let transaction_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("transactions").expect("transaction filter name must validate");
let transaction_status_name =
ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("transaction-status").expect("transaction-status filter name must validate");
let block_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("blocks").expect("block filter name must validate");
let block_meta_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("blocks-meta").expect("block-meta filter name must validate");
let entry_name = ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("entry").expect("entry filter name must validate");
assert!(request.insert_account_filter(account_name, ksp_onchain_transport_lib::YellowstoneSubscribeAccountFilter::new()).is_ok());
assert!(request.insert_slot_filter(slot_name, ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter::new()).is_ok());
assert!(request.insert_transaction_filter(transaction_name, ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter::new()).is_ok());
assert!(
request
.insert_transaction_status_filter(transaction_status_name, ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter::new())
.is_ok()
);
assert!(request.insert_block_filter(block_name, ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter::new()).is_ok());
assert!(request.insert_blocks_meta_filter(block_meta_name, ksp_onchain_transport_lib::YellowstoneSubscribeBlocksMetaFilter::new()).is_ok());
assert!(request.insert_entry_filter(entry_name, ksp_onchain_transport_lib::YellowstoneSubscribeEntryFilter::new()).is_ok());
request.set_commitment(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
let slice = ksp_onchain_transport_lib::YellowstoneAccountsDataSlice::new(4, 32).expect("account-data slice must validate");
assert!(request.push_accounts_data_slice(slice).is_ok());
request.set_ping(std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribePing::new(7)));
request.set_from_slot(std::option::Option::Some(99));
assert!(request.validate().is_ok());
assert_eq!(request.account_filter_count(), 1);
assert_eq!(request.slot_filter_count(), 1);
assert_eq!(request.transaction_filter_count(), 1);
assert_eq!(request.transaction_status_filter_count(), 1);
assert_eq!(request.block_filter_count(), 1);
assert_eq!(request.blocks_meta_filter_count(), 1);
assert_eq!(request.entry_filter_count(), 1);
assert_eq!(request.commitment(), std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
assert_eq!(request.accounts_data_slices(), &[slice]);
assert_eq!(request.ping(), std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribePing::new(7)));
assert_eq!(request.from_slot(), std::option::Option::Some(99));
}
#[test]
fn public_v0_2_9_pre_005_yellowstone_accounts_slots_contract_is_available_from_crate_root() {
let account = ksp_core_lib::Pubkey::new_from_array([1_u8; 32]);
let owner = ksp_core_lib::Pubkey::new_from_array([2_u8; 32]);
let mut accounts = ksp_onchain_transport_lib::YellowstoneSubscribeAccountFilter::new();
assert!(accounts.push_account(account).is_ok());
assert!(accounts.push_owner(owner).is_ok());
let memcmp = ksp_onchain_transport_lib::YellowstoneAccountMemcmp::bytes(0, vec![1_u8, 2, 3]).expect("public memcmp must validate");
assert!(accounts.push_filter(ksp_onchain_transport_lib::YellowstoneAccountFilterPredicate::Memcmp(memcmp)).is_ok());
accounts.set_nonempty_txn_signature(std::option::Option::Some(true));
let cuckoo =
ksp_onchain_transport_lib::YellowstoneCuckooFilter::new(vec![0_u8; 16], 4, 4, 8, 1, ksp_onchain_transport_lib::YellowstoneCuckooHashAlgorithm::SipHash)
.expect("public cuckoo filter must validate");
accounts.set_cuckoo_accounts_filter(std::option::Option::Some(cuckoo));
let mut slots = ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter::new();
slots.set_filter_by_commitment(std::option::Option::Some(true));
slots.set_interslot_updates(std::option::Option::Some(true));
let timestamp = ksp_onchain_transport_lib::YellowstoneUpdateTimestamp::new(1, 2).expect("public timestamp must validate");
let signature = ksp_onchain_transport_lib::YellowstoneTransactionSignature::new([3_u8; 64]);
assert_eq!(timestamp.nanos(), 2);
assert_eq!(signature.as_bytes(), &[3_u8; 64]);
assert_eq!(slots.filter_by_commitment(), std::option::Option::Some(true));
let _account_info = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneAccountInfo>();
let _account_update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneAccountUpdate>();
let _slot_update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSlotUpdate>();
let _slot_status = ksp_onchain_transport_lib::YellowstoneSlotStatus::FirstShredReceived;
let _lamports = ksp_onchain_transport_lib::YellowstoneAccountLamportsFilter::Gt(10);
let _encoding = ksp_onchain_transport_lib::YellowstoneAccountMemcmpEncoding::Bytes;
}
#[test]
fn public_v0_2_9_pre_007_yellowstone_transactions_contract_is_available_from_crate_root() {
let mut filter = ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter::new();
filter.set_vote(std::option::Option::Some(false));
filter.set_failed(std::option::Option::Some(true));
let selector = ksp_onchain_transport_lib::YellowstoneTransactionSignatureSelector::new("1".repeat(64)).expect("signature selector must validate");
filter.set_signature(std::option::Option::Some(selector));
assert!(filter.push_account_include(ksp_core_lib::Pubkey::new_from_array([1_u8; 32])).is_ok());
assert!(filter.push_account_exclude(ksp_core_lib::Pubkey::new_from_array([2_u8; 32])).is_ok());
assert!(filter.push_account_required(ksp_core_lib::Pubkey::new_from_array([3_u8; 32])).is_ok());
filter.set_token_accounts(std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneTokenAccountExpansion::All));
assert_eq!(filter.token_accounts(), std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneTokenAccountExpansion::All));
let _hash = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneHashBytes>();
let _instruction = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneCompiledInstruction>();
let _lookup = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneMessageAddressTableLookup>();
let _config = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionConfig>();
let _message = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionMessage>();
let _stored = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneStoredTransaction>();
let _meta = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionStatusMeta>();
let _info = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionInfo>();
let _update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionUpdate>();
let _status = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionStatusUpdate>();
let _error = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionError>();
let _inner = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneInnerInstructions>();
let _token = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTokenBalance>();
let _return_data = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneReturnData>();
let _reward = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneReward>();
}
#[test]
fn public_v0_2_9_pre_008_yellowstone_blocks_contract_is_available_from_crate_root() {
let account = ksp_core_lib::Pubkey::new_from_array([71_u8; 32]);
let mut block = ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter::new();
assert!(block.push_account_include(account).is_ok());
block.set_include_transactions(std::option::Option::Some(true));
block.set_include_accounts(std::option::Option::Some(false));
block.set_include_entries(std::option::Option::Some(true));
assert_eq!(block.account_include(), &[account]);
assert_eq!(block.include_transactions(), std::option::Option::Some(true));
assert_eq!(block.include_accounts(), std::option::Option::Some(false));
assert_eq!(block.include_entries(), std::option::Option::Some(true));
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
assert!(
request
.insert_block_filter(ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("blocks").expect("filter name must validate"), block,)
.is_ok()
);
assert!(
request
.insert_blocks_meta_filter(
ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("block-meta").expect("filter name must validate"),
ksp_onchain_transport_lib::YellowstoneSubscribeBlocksMetaFilter::new(),
)
.is_ok()
);
assert!(
request
.insert_entry_filter(
ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("entry").expect("filter name must validate"),
ksp_onchain_transport_lib::YellowstoneSubscribeEntryFilter::new(),
)
.is_ok()
);
assert_eq!(request.block_filter_count(), 1);
assert_eq!(request.blocks_meta_filter_count(), 1);
assert_eq!(request.entry_filter_count(), 1);
let _block_update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneBlockUpdate>();
let _block_meta = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneBlockMetaUpdate>();
let _block_rewards = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneBlockRewards>();
let _entry_info = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneEntryInfo>();
let _entry_update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneEntryUpdate>();
}
#[test]
fn public_v0_2_9_pre_009_yellowstone_bidi_session_contract_is_available_from_crate_root() {
fn assert_send<T: Send>() {
let _ = std::marker::PhantomData::<T>;
return;
}
assert_send::<ksp_onchain_transport_lib::SolanaYellowstoneGrpcSubscribeSession>();
assert_send::<ksp_onchain_transport_lib::YellowstoneSubscribeUpdate>();
let _open = ksp_onchain_transport_lib::YellowstoneGrpcChannel::open_standard_subscribe;
let _session = std::any::type_name::<ksp_onchain_transport_lib::SolanaYellowstoneGrpcSubscribeSession>();
let _ping = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribePingUpdate>();
let _pong = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribePongUpdate>();
let _states = [
ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Active,
ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Closing,
ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Closed,
ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Failed,
];
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW.domain(), "onchain_transport");
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW.code(), "grpc_backpressure_overflow");
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_SESSION_CLOSED.domain(), "onchain_transport");
assert_eq!(ksp_onchain_transport_lib::ERROR_CODE_GRPC_SESSION_CLOSED.code(), "grpc_session_closed");
}
#[test]
fn public_v0_2_9_pre_010_yellowstone_reconnect_snapshot_is_available_from_crate_root() {
fn assert_copy<T: Copy>() {
let _ = std::marker::PhantomData::<T>;
return;
}
assert_copy::<ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot>();
let _snapshot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot>();
let _state = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Reconnecting;
let _session_snapshot = ksp_onchain_transport_lib::SolanaYellowstoneGrpcSubscribeSession::snapshot;
let _reconnect_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::reconnect_count;
let _gap_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::continuity_gap_count;
let _duplicate_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::duplicate_update_count;
let _replay_count = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::replay_attempt_count;
let _requested = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_requested_from_slot;
let _observed = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::last_observed_slot;
let _terminal = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot::terminal_error_code;
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs // file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
// version: 32 // version: 41
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage. //! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
@@ -996,3 +996,351 @@ fn release_v0_2_8_pre_010_live_smoke_policy_preserves_secret_and_dependency_owne
let env_example = std::fs::read_to_string(workspace.join(".env.example")).expect(".env.example must be readable"); let env_example = std::fs::read_to_string(workspace.join(".env.example")).expect(".env.example must be readable");
assert!(env_example.contains("KSP_SECRET_HELIUS_API_KEY")); assert!(env_example.contains("KSP_SECRET_HELIUS_API_KEY"));
} }
#[test]
fn release_v0_2_9_pre_002_materializes_minimal_yellowstone_engine_without_provider_or_ws_coupling() {
let manifest_directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let workspace = manifest_directory.parent().and_then(std::path::Path::parent).expect("Transport test must resolve workspace root");
let root_manifest = std::fs::read_to_string(workspace.join("Cargo.toml")).expect("workspace manifest must be readable");
let transport_manifest = std::fs::read_to_string(manifest_directory.join("Cargo.toml")).expect("Transport manifest must be readable");
let settings_source = include_str!("../src/grpc_settings.rs");
let channel_source = include_str!("../src/grpc_channel.rs");
let crate_root = include_str!("../src/lib.rs");
assert!(root_manifest.contains("tonic = { version = \"^0.14\", default-features = false }"));
assert!(root_manifest.contains("yellowstone-grpc-proto = { version = \"^12.6\", default-features = false }"));
assert!(transport_manifest.contains("tonic = { workspace = true, features = [\"channel\""));
assert!(transport_manifest.contains("yellowstone-grpc-proto.workspace = true"));
assert!(!transport_manifest.contains("yellowstone-grpc-client"));
assert!(!transport_manifest.contains("ksp-config-lib"));
assert!(settings_source.contains("YellowstoneGrpcEndpointUrl"));
assert!(settings_source.contains("YellowstoneGrpcSessionSettings"));
assert!(settings_source.contains("YellowstoneGrpcTransportSettings"));
assert!(!settings_source.contains("WsProtocolKind"));
assert!(!settings_source.contains("KSP_SECRET_"));
assert!(channel_source.contains("tonic::transport::Endpoint::from_shared"));
assert!(channel_source.contains("connect_lazy"));
assert!(!channel_source.contains("WsSession"));
assert!(!crate_root.contains("pub use tonic"));
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
let _published_wire = std::any::type_name::<yellowstone_grpc_proto::geyser::SubscribeRequest>();
}
#[test]
fn release_v0_2_9_pre_003_adds_tls_metadata_and_exactly_seven_standard_unary_methods_without_subscribe() {
let manifest_directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let workspace = manifest_directory.parent().and_then(std::path::Path::parent).expect("Transport test must resolve workspace root");
let root_manifest = std::fs::read_to_string(workspace.join("Cargo.toml")).expect("workspace manifest must be readable");
let transport_manifest = std::fs::read_to_string(manifest_directory.join("Cargo.toml")).expect("Transport manifest must be readable");
let settings_source = include_str!("../src/grpc_settings.rs");
let channel_source = include_str!("../src/grpc_channel.rs");
let unary_source = include_str!("../src/grpc_unary.rs");
let crate_root = include_str!("../src/lib.rs");
assert!(root_manifest.contains("http = { version = \"^1.5\", default-features = false }"));
assert!(root_manifest.contains("tonic-prost = { version = \"^0.14\", default-features = false }"));
assert!(transport_manifest.contains("tonic = { workspace = true, features = [\"channel\", \"tls-aws-lc\", \"tls-webpki-roots\"] }"));
assert!(transport_manifest.contains("tonic = { workspace = true, features = [\"codegen\", \"server\"] }"));
assert!(transport_manifest.contains("tonic-prost.workspace = true"));
assert!(transport_manifest.contains("yellowstone-grpc-proto.workspace = true"));
assert!(transport_manifest.contains("yellowstone-grpc-proto = { workspace = true, features = [\"tonic\"] }"));
assert!(!transport_manifest.contains("yellowstone-grpc-client"));
assert!(settings_source.contains("YellowstoneGrpcMetadataEntry"));
assert!(settings_source.contains("set_sensitive"));
assert!(channel_source.contains("ClientTlsConfig"));
assert!(channel_source.contains("with_webpki_roots"));
assert!(channel_source.contains("pub async fn connect"));
for path in [
"/geyser.Geyser/SubscribeReplayInfo",
"/geyser.Geyser/Ping",
"/geyser.Geyser/GetLatestBlockhash",
"/geyser.Geyser/GetBlockHeight",
"/geyser.Geyser/GetSlot",
"/geyser.Geyser/IsBlockhashValid",
"/geyser.Geyser/GetVersion",
] {
assert!(unary_source.contains(path), "missing standard Yellowstone unary path: {path}");
}
assert!(!unary_source.contains("const PATH_SUBSCRIBE: "));
assert!(!unary_source.contains("SubscribeDeshred"));
assert!(!unary_source.contains("PublicNode"));
assert!(!crate_root.contains("pub use tonic"));
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
let _client = std::any::type_name::<ksp_onchain_transport_lib::SolanaYellowstoneGrpcUnaryClient>();
}
#[test]
fn release_v0_2_9_pre_004_materializes_only_standard_subscribe_common_contract() {
let source = include_str!("../src/grpc_subscribe.rs");
let crate_root = include_str!("../src/lib.rs");
for wire_field in [
"accounts:",
"slots:",
"transactions:",
"transactions_status:",
"blocks:",
"blocks_meta:",
"entry:",
"commitment:",
"accounts_data_slice:",
"ping:",
"from_slot:",
] {
assert!(source.contains(wire_field), "missing standard Yellowstone SubscribeRequest field: {wire_field}");
}
assert!(source.contains("MAX_GRPC_SUBSCRIBE_FILTER_GROUP_COUNT"));
assert!(source.contains("MAX_GRPC_SUBSCRIBE_FILTER_NAME_LENGTH_BYTES"));
assert!(source.contains("MAX_GRPC_SUBSCRIBE_DATA_SLICE_COUNT"));
assert!(source.contains("offset.checked_add(length)"));
assert!(source.contains("globally unique"));
assert!(source.contains("SubscribeRequestFilterBlocksMeta"));
assert!(source.contains("SubscribeRequestFilterEntry"));
assert!(source.contains("YellowstoneSubscribeAccountFilter"));
assert!(source.contains("YellowstoneSubscribeSlotFilter"));
assert!(source.contains("YellowstoneSubscribeTransactionFilter"));
assert!(source.contains("YellowstoneSubscribeBlockFilter"));
assert!(!source.contains("SubscribeDeshred"));
assert!(!source.contains("PublicNode"));
assert!(!source.contains("OrbitFlare"));
assert!(!source.contains("Helius"));
assert!(!source.contains("pub fn to_wire"));
assert!(!source.contains("pub(crate) fn to_wire"));
assert!(!crate_root.contains("pub use tonic"));
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
let _request = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeRequest>();
let _account = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeAccountFilter>();
let _slot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter>();
let _transaction = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter>();
let _block = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeBlockFilter>();
}
#[test]
fn release_v0_2_9_pre_005_accounts_and_slots_contract_remains_complete() {
let source = include_str!("../src/grpc_subscribe.rs");
let crate_root = include_str!("../src/lib.rs");
for required in [
"account: self.accounts",
"owner: self.owners",
"filters: self.filters",
"nonempty_txn_signature",
"cuckoo_accounts_filter",
"SubscribeRequestFilterAccountsFilterMemcmp",
"DataSize",
"TokenAccountState",
"YellowstoneAccountLamportsFilter",
"filter_by_commitment",
"interslot_updates",
"YellowstoneAccountUpdate",
"YellowstoneSlotUpdate",
"YellowstoneTransactionSignature",
"SlotFirstShredReceived",
"SlotCompleted",
"SlotCreatedBank",
"SlotDead",
"dead_error",
] {
assert!(source.contains(required), "missing pre.005 Accounts/Slots contract token: {required}");
}
assert!(source.contains("MAX_GRPC_SUBSCRIBE_ACCOUNT_SELECTOR_COUNT"));
assert!(source.contains("MAX_GRPC_SUBSCRIBE_ACCOUNT_PREDICATE_COUNT"));
assert!(source.contains("MAX_GRPC_SUBSCRIBE_CUCKOO_DATA_LENGTH_BYTES"));
assert!(source.contains("MAX_GRPC_SUBSCRIBE_ACCOUNT_DATA_LENGTH_BYTES"));
assert!(source.contains("MAX_GRPC_SUBSCRIBE_SLOT_DEAD_ERROR_LENGTH_BYTES"));
assert!(source.contains("fn decode_account_update"));
assert!(source.contains("fn decode_slot_update"));
assert!(!source.contains("SubscribeDeshred"));
assert!(!source.contains("PublicNode"));
assert!(!crate_root.contains("pub use tonic"));
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
let _account = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneAccountUpdate>();
let _slot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSlotUpdate>();
}
#[test]
fn release_v0_2_9_pre_006_namespaces_unambiguously_http_owned_private_modules() {
let crate_root = include_str!("../src/lib.rs");
let http_client = include_str!("../src/http_client.rs");
let http_executor = include_str!("../src/http_executor.rs");
let http_pool = include_str!("../src/http_pool.rs");
let http_resilience = include_str!("../src/http_resilience.rs");
let http_settings = include_str!("../src/http_settings.rs");
for declaration in ["mod http_client;", "mod http_executor;", "mod http_pool;", "mod http_resilience;", "mod http_settings;"] {
assert!(crate_root.contains(declaration), "missing HTTP-owned private module declaration: {declaration}");
}
for historical in ["mod client;", "mod executor;", "mod pool;", "mod resilience;", "mod settings;"] {
assert!(!crate_root.contains(historical), "historical ambiguous private module declaration remains: {historical}");
}
assert!(http_client.contains("HttpEndpointClient"));
assert!(http_executor.contains("execute_standard_rpc"));
assert!(http_pool.contains("HttpTransportPool"));
assert!(http_resilience.contains("HttpRetryDecision"));
assert!(http_settings.contains("HttpTransportSettings"));
// Shared/protocol-oriented modules deliberately keep their existing names.
for shared in ["mod constants;", "mod error;", "mod json_rpc;", "mod rpc_common;", "mod rpc_accounts;", "mod rpc_blocks;", "mod rpc_transactions;"] {
assert!(crate_root.contains(shared), "shared/protocol module was incorrectly HTTP-prefixed: {shared}");
}
}
#[test]
fn release_v0_2_9_pre_007_transactions_contract_remains_complete() {
let source = include_str!("../src/grpc_subscribe.rs");
let crate_root = include_str!("../src/lib.rs");
for required in [
"YellowstoneSubscribeTransactionFilter",
"YellowstoneTransactionSignatureSelector",
"YellowstoneTokenAccountExpansion",
"account_include",
"account_exclude",
"account_required",
"cuckoo_account_include",
"token_accounts",
"YellowstoneTransactionUpdate",
"YellowstoneTransactionStatusUpdate",
"YellowstoneTransactionStatusMeta",
"YellowstoneTransactionMessage",
"YellowstoneMessageAddressTableLookup",
"YellowstoneTransactionConfig",
"compute_units_consumed",
"cost_units",
"inner_instructions_none",
"log_messages_none",
"return_data_none",
"decode_transaction_update",
"decode_transaction_status_update",
] {
assert!(source.contains(required), "missing pre.007 Transactions contract token: {required}");
}
assert!(source.contains("base58_decoded_length"));
assert!(source.contains("YELLOWSTONE_TRANSACTION_SIGNATURE_WIRE_LENGTH_BYTES"));
assert!(!source.contains("pub async fn subscribe("));
assert!(!source.contains("SubscribeDeshred"));
assert!(!source.contains("PublicNode"));
assert!(!crate_root.contains("pub use tonic"));
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
let _filter = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeTransactionFilter>();
let _update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionUpdate>();
let _status = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneTransactionStatusUpdate>();
}
#[test]
fn release_v0_2_9_pre_008_standard_blocks_contract_remains_complete() {
let source = include_str!("../src/grpc_subscribe.rs");
let crate_root = include_str!("../src/lib.rs");
for required in [
"YellowstoneSubscribeBlockFilter",
"account_include",
"include_transactions",
"include_accounts",
"include_entries",
"cuckoo_account_include",
"YellowstoneBlockRewards",
"YellowstoneBlockUpdate",
"YellowstoneBlockMetaUpdate",
"YellowstoneEntryInfo",
"YellowstoneEntryUpdate",
"executed_transaction_count",
"updated_account_count",
"entries_count",
"starting_transaction_index",
"decode_block_update",
"decode_block_meta_update",
"decode_entry_update",
"decode_block_rewards",
] {
assert!(source.contains(required), "missing pre.008 Blocks contract token: {required}");
}
assert!(source.contains("MAX_GRPC_BLOCK_VECTOR_COUNT"));
assert!(source.contains("base58_decoded_length(value.as_str())"));
assert!(source.contains("std::vec::Vec<crate::YellowstoneTransactionInfo>"));
assert!(source.contains("std::vec::Vec<crate::YellowstoneAccountInfo>"));
assert!(source.contains("std::vec::Vec<crate::YellowstoneEntryInfo>"));
assert!(source.contains("fn decode_block_update"));
assert!(!source.contains("pub async fn subscribe("));
assert!(!source.contains("SubscribeDeshred"));
assert!(!source.contains("PublicNode"));
assert!(!crate_root.contains("pub use tonic"));
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
let _block = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneBlockUpdate>();
let _meta = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneBlockMetaUpdate>();
let _entry = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneEntryUpdate>();
}
#[test]
fn release_v0_2_9_pre_009_bounded_standard_bidi_session_contract_remains_complete() {
let stream_source = include_str!("../src/grpc_stream.rs");
let subscribe_source = include_str!("../src/grpc_subscribe.rs");
let channel_source = include_str!("../src/grpc_channel.rs");
let crate_root = include_str!("../src/lib.rs");
for required in [
"const PATH_SUBSCRIBE: &str = \"/geyser.Geyser/Subscribe\"",
".streaming(",
"tokio::sync::mpsc::channel",
"tokio::sync::watch::channel",
"try_send",
"AUTO_SUBSCRIBE_PING_ID",
"ping_request_wire",
"finish_client_half_close",
"timeout_at",
"max_decoding_message_size",
"max_encoding_message_size",
"YellowstoneGrpcSubscribeState",
"SolanaYellowstoneGrpcSubscribeSession",
] {
assert!(stream_source.contains(required), "missing pre.009 bidi runtime token: {required}");
}
for required in [
"YellowstoneSubscribeUpdate",
"YellowstoneSubscribePingUpdate",
"YellowstoneSubscribePongUpdate",
"yellowstone_subscribe_request_to_wire",
"yellowstone_subscribe_update_from_wire",
] {
assert!(subscribe_source.contains(required), "missing pre.009 runtime wire bridge token: {required}");
}
assert!(channel_source.contains("pub async fn open_standard_subscribe"));
assert!(!stream_source.contains("unbounded_channel"));
assert!(!stream_source.contains("SubscribeDeshred"));
assert!(!stream_source.contains("PublicNode"));
assert!(!stream_source.contains("OrbitFlare"));
assert!(!stream_source.contains("Helius"));
assert!(!crate_root.contains("pub use tonic"));
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
let _session = std::any::type_name::<ksp_onchain_transport_lib::SolanaYellowstoneGrpcSubscribeSession>();
let _state = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Active;
let _update = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneSubscribeUpdate>();
}
#[test]
fn release_v0_2_9_pre_010_adds_bounded_reconnect_replay_and_conservative_continuity_observability() {
let stream_source = include_str!("../src/grpc_stream.rs");
let crate_root = include_str!("../src/lib.rs");
for required in [
"YellowstoneGrpcSubscribeState::Reconnecting",
"YellowstoneGrpcSubscribeSnapshot",
"reconnect_subscribe_stream",
"replay_first_available",
"SubscribeReplayInfo",
"last_requested_from_slot",
"last_observed_slot",
"reconnect_count",
"replay_attempt_count",
"continuity_gap_count",
"duplicate_update_count",
"MAX_RECENT_UPDATE_IDENTITIES",
"first_available > requested",
"max_optional_slot",
"reconnect budget is exhausted",
"request mutation",
] {
assert!(stream_source.contains(required), "missing pre.010 reconnect/replay contract token: {required}");
}
assert!(stream_source.contains("still delivered to the caller"));
assert!(stream_source.contains("does not claim exactly-once delivery"));
assert!(!stream_source.contains("unbounded_channel"));
assert!(!stream_source.contains("SubscribeDeshred"));
assert!(!stream_source.contains("PublicNode"));
assert!(!stream_source.contains("OrbitFlare"));
assert!(!stream_source.contains("Helius"));
assert!(!crate_root.contains("pub use tonic"));
assert!(!crate_root.contains("pub use yellowstone_grpc_proto"));
let _snapshot = std::any::type_name::<ksp_onchain_transport_lib::YellowstoneGrpcSubscribeSnapshot>();
let _state = ksp_onchain_transport_lib::YellowstoneGrpcSubscribeState::Reconnecting;
}

View File

@@ -0,0 +1,169 @@
// file: crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs
// version: 5
//! Opt-in live PublicNode Mainnet/Testnet smokes for authenticated provider-neutral Yellowstone gRPC Subscribe.
use std::io::IsTerminal; // rust-rules: trait-import
struct PublicNodeTokens {
mainnet: std::string::String,
testnet: std::string::String,
}
static PUBLICNODE_X_TOKENS: std::sync::OnceLock<PublicNodeTokens> = std::sync::OnceLock::new();
fn read_token_line(label: &str) -> std::string::String {
let mut token = std::string::String::new();
match std::io::stdin().read_line(&mut token) {
std::result::Result::Ok(_) => {},
std::result::Result::Err(error) => panic!("PublicNode {label} x-token must be readable from smoke stdin: {error}"),
}
let token = token.trim().to_owned();
assert!(!token.is_empty(), "PublicNode {label} x-token provided on smoke stdin must not be empty");
return token;
}
fn publicnode_x_tokens() -> &'static PublicNodeTokens {
return PUBLICNODE_X_TOKENS.get_or_init(|| {
assert!(
!std::io::stdin().is_terminal(),
"pipe two PublicNode personal x-tokens to this ignored smoke on stdin: Mainnet first, Testnet second; never pass them as command-line arguments"
);
return PublicNodeTokens { mainnet: read_token_line("Mainnet"), testnet: read_token_line("Testnet") };
});
}
fn publicnode_endpoint(
name: &str,
cluster: &str,
url: &str,
x_token: &str,
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
let url = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse(url) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let session = ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::new(
std::time::Duration::from_secs(10),
std::time::Duration::from_secs(10),
std::time::Duration::from_secs(5),
ksp_onchain_transport_lib::YellowstoneGrpcReconnectSettings::new(0, std::time::Duration::from_millis(250), std::time::Duration::from_secs(2)),
8,
8,
16 * 1024 * 1024,
4 * 1024 * 1024,
);
let endpoint = ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
name,
true,
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("publicnode"),
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new(cluster),
url,
session,
);
let metadata = match ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::secret("x-token", x_token) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return endpoint.with_metadata(vec![metadata]);
}
fn slot_request() -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneSubscribeRequest> {
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
let name = match ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("slots") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
match request.insert_slot_filter(name, ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter::new()) {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
return std::result::Result::Ok(request);
}
async fn assert_publicnode_slot_stream(name: &str, cluster: &str, url: &str, x_token: &str) {
let endpoint = match publicnode_endpoint(name, cluster, url, x_token) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("programmatic PublicNode Yellowstone settings must accept secret x-token metadata: {error:?}"),
};
let endpoint_debug = format!("{endpoint:?}");
assert!(!endpoint_debug.contains(x_token), "PublicNode x-token must not appear in endpoint Debug");
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(&endpoint).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("PublicNode Yellowstone TLS connection must succeed: {error:?}"),
};
assert_eq!(channel.endpoint_name(), name);
assert_eq!(channel.provider().as_str(), "publicnode");
assert_eq!(channel.cluster().as_str(), cluster);
let request = match slot_request() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("PublicNode Yellowstone slot request must be valid: {error:?}"),
};
let mut session = match channel.open_standard_subscribe(request).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("PublicNode Yellowstone authenticated Subscribe must open: {error:?}"),
};
let slot_result = tokio::time::timeout(std::time::Duration::from_secs(20), async {
loop {
let next_update = match session.next_update().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("PublicNode Yellowstone Subscribe update must decode: {error:?}"),
};
match next_update {
std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Slot(update)) => return update.slot(),
std::option::Option::Some(_) => {},
std::option::Option::None => panic!("PublicNode Yellowstone Subscribe ended before a slot update"),
}
}
})
.await;
let slot = match slot_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("PublicNode Yellowstone Subscribe must publish a slot update before the smoke deadline: {error}"),
};
assert!(slot > 0);
let close_timeout_result = tokio::time::timeout(std::time::Duration::from_secs(7), session.close()).await;
let close_result = match close_timeout_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("KSP Yellowstone Subscribe close must remain bounded beyond the configured five-second provider half-close deadline: {error}")
},
};
match close_result {
std::result::Result::Ok(()) => {},
std::result::Result::Err(error) => {
assert_eq!(
error.code(),
ksp_onchain_transport_lib::ERROR_CODE_TIMEOUT,
"after a live slot was observed, PublicNode close may time out waiting for provider half-close but must not fail for another reason: {error:?}"
);
},
}
return;
}
#[tokio::test(flavor = "current_thread")]
#[ignore = "opt-in live PublicNode Mainnet Yellowstone gRPC smoke; reads the first personal x-token line from stdin and performs an external TLS/Subscribe request"]
async fn publicnode_mainnet_yellowstone_streams_slots_with_network_scoped_secret_x_token() {
let tokens = publicnode_x_tokens();
assert_publicnode_slot_stream(
"publicnode_mainnet_yellowstone",
"mainnet-beta",
"https://solana-yellowstone-grpc.publicnode.com:443",
tokens.mainnet.as_str(),
)
.await;
}
#[tokio::test(flavor = "current_thread")]
#[ignore = "opt-in live PublicNode Testnet Yellowstone gRPC smoke; reads the second personal x-token line from stdin and performs an external TLS/Subscribe request"]
async fn publicnode_testnet_yellowstone_streams_slots_with_network_scoped_secret_x_token() {
let tokens = publicnode_x_tokens();
assert_publicnode_slot_stream(
"publicnode_testnet_yellowstone",
"testnet",
"https://solana-testnet-yellowstone-grpc.publicnode.com:443",
tokens.testnet.as_str(),
)
.await;
}

View File

@@ -0,0 +1,60 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
// version: 3
fn endpoint(enabled: bool, value: &str) -> crate::YellowstoneGrpcEndpointSettings {
let parsed = crate::YellowstoneGrpcEndpointUrl::parse(value).expect("fixture Yellowstone gRPC URL must parse");
return crate::YellowstoneGrpcEndpointSettings::new(
"fixture",
enabled,
crate::YellowstoneGrpcProviderName::new("fixture-provider"),
crate::YellowstoneGrpcClusterName::new("devnet"),
parsed,
crate::YellowstoneGrpcSessionSettings::default(),
);
}
#[tokio::test(flavor = "current_thread")]
async fn grpc_channel_prepare_is_lazy_safe_and_keeps_tonic_private() {
let endpoint = endpoint(true, "http://127.0.0.1:10000/GRPC-SECRET-CANARY");
let channel = crate::YellowstoneGrpcChannel::prepare(&endpoint);
assert!(channel.is_ok());
let channel = match channel {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(channel.endpoint_name(), "fixture");
assert_eq!(channel.provider().as_str(), "fixture-provider");
assert_eq!(channel.cluster().as_str(), "devnet");
let rendered = format!("{channel:?}");
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
assert!(!rendered.contains("127.0.0.1"));
let _client = channel.standard_unary_client();
let _wire_type = std::any::type_name::<yellowstone_grpc_proto::geyser::SubscribeRequest>();
}
#[test]
fn grpc_channel_prepare_requires_active_tokio_runtime_without_panicking() {
let endpoint = endpoint(true, "http://127.0.0.1:10000/GRPC-SECRET-CANARY");
let result = crate::YellowstoneGrpcChannel::prepare(&endpoint);
assert!(result.is_err());
let rendered = format!("{result:?}");
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
assert!(!rendered.contains("127.0.0.1"));
}
#[test]
fn grpc_channel_prepare_rejects_disabled_endpoint_before_network_io() {
let endpoint = endpoint(false, "http://127.0.0.1:10000");
let result = crate::YellowstoneGrpcChannel::prepare(&endpoint);
assert!(result.is_err());
}
#[tokio::test(flavor = "current_thread")]
async fn grpc_channel_prepare_configures_https_without_exposing_url() {
let endpoint = endpoint(true, "https://example.invalid:443/GRPC-SECRET-CANARY");
let result = crate::YellowstoneGrpcChannel::prepare(&endpoint);
assert!(result.is_ok());
let rendered = format!("{result:?}");
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
assert!(!rendered.contains("example.invalid"));
}

View File

@@ -0,0 +1,198 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
// version: 3
fn endpoint(name: &str, enabled: bool, url: &str, session: crate::YellowstoneGrpcSessionSettings) -> crate::YellowstoneGrpcEndpointSettings {
let parsed = crate::YellowstoneGrpcEndpointUrl::parse(url).expect("fixture Yellowstone gRPC URL must parse");
return crate::YellowstoneGrpcEndpointSettings::new(
name,
enabled,
crate::YellowstoneGrpcProviderName::new("fixture"),
crate::YellowstoneGrpcClusterName::new("devnet"),
parsed,
session,
);
}
#[test]
fn grpc_endpoint_url_accepts_http_and_https_and_redacts_debug() {
for value in ["http://127.0.0.1:10000", "https://yellowstone.example.invalid:443/path?token=GRPC-SECRET-CANARY"] {
let parsed = crate::YellowstoneGrpcEndpointUrl::parse(value);
assert!(parsed.is_ok());
let parsed = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => continue,
};
assert_eq!(parsed.as_str(), value);
let rendered = format!("{parsed:?}");
assert_eq!(rendered, "YellowstoneGrpcEndpointUrl(<redacted>)");
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
}
}
#[test]
fn grpc_endpoint_url_rejects_non_http_schemes_without_echoing_secret() {
let result = crate::YellowstoneGrpcEndpointUrl::parse("wss://GRPC-SECRET-CANARY@example.invalid/socket");
assert!(result.is_err());
let error = match result {
std::result::Result::Ok(_) => return,
std::result::Result::Err(error) => error,
};
let rendered = error.to_string();
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
assert!(!format!("{error:?}").contains("GRPC-SECRET-CANARY"));
}
#[test]
fn grpc_endpoint_url_rejects_excessive_length_without_echoing_payload() {
let value = format!("https://example.invalid/{}", "S".repeat(super::MAX_GRPC_ENDPOINT_URL_LENGTH_BYTES + 1));
let result = crate::YellowstoneGrpcEndpointUrl::parse(value);
assert!(result.is_err());
let error = match result {
std::result::Result::Ok(_) => return,
std::result::Result::Err(error) => error,
};
assert!(!format!("{error:?}").contains(&"S".repeat(64)));
}
#[test]
fn grpc_session_defaults_are_bounded_and_validate() {
let settings = crate::YellowstoneGrpcSessionSettings::default();
assert!(settings.validate().is_ok());
assert!(!settings.connect_timeout().is_zero());
assert!(!settings.unary_timeout().is_zero());
assert!(!settings.close_timeout().is_zero());
assert!(settings.request_channel_capacity() > 0);
assert!(settings.update_channel_capacity() > 0);
assert!(settings.max_inbound_message_size_bytes() > 0);
assert!(settings.max_outbound_message_size_bytes() > 0);
assert!(settings.reconnect().max_retries() <= super::MAX_GRPC_RECONNECT_RETRIES);
}
#[test]
fn grpc_session_rejects_zero_excessive_and_reversed_runtime_bounds() {
let zero = crate::YellowstoneGrpcSessionSettings::new(
std::time::Duration::ZERO,
std::time::Duration::from_secs(1),
std::time::Duration::from_secs(1),
crate::YellowstoneGrpcReconnectSettings::default(),
1,
1,
1,
1,
);
assert!(zero.validate().is_err());
let excessive = crate::YellowstoneGrpcSessionSettings::new(
super::MAX_GRPC_RUNTIME_DURATION + std::time::Duration::from_secs(1),
std::time::Duration::from_secs(1),
std::time::Duration::from_secs(1),
crate::YellowstoneGrpcReconnectSettings::new(
super::MAX_GRPC_RECONNECT_RETRIES + 1,
std::time::Duration::from_millis(1),
std::time::Duration::from_millis(2),
),
super::MAX_GRPC_CHANNEL_CAPACITY + 1,
1,
super::MAX_GRPC_MESSAGE_SIZE_BYTES + 1,
1,
);
assert!(excessive.validate().is_err());
let reversed = crate::YellowstoneGrpcSessionSettings::new(
std::time::Duration::from_secs(1),
std::time::Duration::from_secs(1),
std::time::Duration::from_secs(1),
crate::YellowstoneGrpcReconnectSettings::new(5, std::time::Duration::from_secs(2), std::time::Duration::from_secs(1)),
1,
1,
1,
1,
);
assert!(reversed.validate().is_err());
}
#[test]
fn grpc_transport_settings_validate_unique_enabled_endpoints_and_redact_urls() {
let first = endpoint("first", true, "https://GRPC-SECRET-CANARY@example.invalid:443", crate::YellowstoneGrpcSessionSettings::default());
let second = endpoint("second", false, "http://127.0.0.1:10000", crate::YellowstoneGrpcSessionSettings::default());
let settings = crate::YellowstoneGrpcTransportSettings::new(std::vec![first.clone(), second]);
assert!(settings.validate().is_ok());
let rendered = format!("{settings:?}");
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
assert!(!rendered.contains("example.invalid"));
let duplicate = crate::YellowstoneGrpcTransportSettings::new(std::vec![first.clone(), first]);
assert!(duplicate.validate().is_err());
}
#[test]
fn grpc_transport_settings_reject_excessive_descriptor_and_endpoint_count() {
let oversized_name = "n".repeat(super::MAX_GRPC_DESCRIPTOR_LENGTH_BYTES + 1);
let oversized = endpoint(oversized_name.as_str(), true, "http://127.0.0.1:10000", crate::YellowstoneGrpcSessionSettings::default());
assert!(crate::YellowstoneGrpcTransportSettings::new(std::vec![oversized]).validate().is_err());
let parsed = crate::YellowstoneGrpcEndpointUrl::parse("http://127.0.0.1:10000").expect("fixture Yellowstone gRPC URL must parse");
let endpoints = (0..=super::MAX_GRPC_ENDPOINT_COUNT)
.map(|index| {
return crate::YellowstoneGrpcEndpointSettings::new(
format!("endpoint-{index}"),
true,
crate::YellowstoneGrpcProviderName::new("fixture"),
crate::YellowstoneGrpcClusterName::new("devnet"),
parsed.clone(),
crate::YellowstoneGrpcSessionSettings::default(),
);
})
.collect();
assert!(crate::YellowstoneGrpcTransportSettings::new(endpoints).validate().is_err());
}
#[test]
fn grpc_transport_settings_require_one_enabled_endpoint() {
let disabled = endpoint("disabled", false, "http://127.0.0.1:10000", crate::YellowstoneGrpcSessionSettings::default());
assert!(crate::YellowstoneGrpcTransportSettings::new(std::vec![disabled]).validate().is_err());
assert!(crate::YellowstoneGrpcTransportSettings::new(std::vec![]).validate().is_err());
}
#[test]
fn grpc_metadata_validates_ascii_bounds_sensitivity_and_redacted_debug() {
let public = crate::YellowstoneGrpcMetadataEntry::public("x-ksp-public", "VISIBLE-CANARY").expect("public metadata must validate");
let secret = crate::YellowstoneGrpcMetadataEntry::secret("x-ksp-token", "GRPC-SECRET-CANARY").expect("secret metadata must validate");
assert_eq!(public.key(), "x-ksp-public");
assert!(!public.is_secret());
assert!(secret.is_secret());
let rendered = format!("{public:?} {secret:?}");
assert!(!rendered.contains("VISIBLE-CANARY"));
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
let mut metadata = tonic::metadata::MetadataMap::new();
assert!(secret.append_to(&mut metadata).is_ok());
let appended = metadata.get("x-ksp-token").expect("secret metadata must be appended");
assert!(appended.is_sensitive());
}
#[test]
fn grpc_metadata_rejects_reserved_binary_uppercase_malformed_and_oversized_values() {
assert!(crate::YellowstoneGrpcMetadataEntry::public("grpc-timeout", "1").is_err());
assert!(crate::YellowstoneGrpcMetadataEntry::public("x-ksp-bin", "1").is_err());
assert!(crate::YellowstoneGrpcMetadataEntry::public("X-KSP-UPPER", "1").is_err());
assert!(crate::YellowstoneGrpcMetadataEntry::public("x-ksp-bad", "line\nfeed").is_err());
assert!(crate::YellowstoneGrpcMetadataEntry::secret("x-ksp-large", "S".repeat(super::MAX_GRPC_METADATA_VALUE_LENGTH_BYTES + 1)).is_err());
}
#[test]
fn grpc_endpoint_metadata_count_is_bounded_and_url_tracks_tls_scheme() {
let http = crate::YellowstoneGrpcEndpointUrl::parse("http://127.0.0.1:10000").expect("HTTP URL must parse");
let https = crate::YellowstoneGrpcEndpointUrl::parse("https://example.invalid:443").expect("HTTPS URL must parse");
assert!(!http.uses_tls());
assert!(https.uses_tls());
let metadata = (0..=super::MAX_GRPC_METADATA_ENTRY_COUNT)
.map(|index| {
return crate::YellowstoneGrpcMetadataEntry::public(format!("x-ksp-{index}"), "value").expect("generated metadata must be valid");
})
.collect();
let endpoint = crate::YellowstoneGrpcEndpointSettings::new(
"metadata-bound",
true,
crate::YellowstoneGrpcProviderName::new("fixture"),
crate::YellowstoneGrpcClusterName::new("devnet"),
http,
crate::YellowstoneGrpcSessionSettings::default(),
);
assert!(endpoint.with_metadata(metadata).is_err());
}

View File

@@ -0,0 +1,685 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs
// version: 2
#[derive(Clone, Copy)]
enum FixtureMode {
RoundTrip,
ClientHalfClose,
HostileClose,
Flood,
RemoteStatus,
Malformed,
Oversized,
Idle,
ReconnectReplay,
ReplayGap,
ReconnectExhausted,
}
#[derive(Clone)]
struct FixtureGeyser {
mode: FixtureMode,
half_close_seen: std::sync::Arc<std::sync::atomic::AtomicBool>,
subscribe_calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
}
#[allow(clippy::implicit_return)] // tonic::async_trait generates async wrapper tails outside the authored fixture bodies.
#[tonic::async_trait]
impl yellowstone_grpc_proto::geyser::geyser_server::Geyser for FixtureGeyser {
type SubscribeStream = super::MpscStream<std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdate, tonic::Status>>;
type SubscribeDeshredStream = futures_util::stream::Empty<std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdateDeshred, tonic::Status>>;
async fn subscribe(
&self,
request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeRequest>>,
) -> std::result::Result<tonic::Response<Self::SubscribeStream>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
let subscribe_call = self.subscribe_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst).saturating_add(1);
if matches!(self.mode, FixtureMode::ReconnectExhausted) && subscribe_call > 1 {
return std::result::Result::Err(tonic::Status::unavailable("GRPC-RECONNECT-SECRET-CANARY"));
}
let mut inbound = request.into_inner();
let (outbound_tx, outbound_rx) = tokio::sync::mpsc::channel(64);
let mode = self.mode;
let half_close_seen = self.half_close_seen.clone();
tokio::spawn(async move {
let initial = inbound.message().await;
let initial = match initial {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
_ => return,
};
match mode {
FixtureMode::RoundTrip => {
assert!(initial.slots.contains_key("initial"));
if outbound_tx.send(std::result::Result::Ok(ping_update())).await.is_err() {
return;
}
let ping = inbound.message().await;
let ping = match ping {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
_ => return,
};
assert_eq!(ping.ping.map(|value| value.id), std::option::Option::Some(1));
assert!(ping.accounts.is_empty());
assert!(ping.slots.is_empty());
assert!(ping.transactions.is_empty());
assert!(ping.transactions_status.is_empty());
assert!(ping.blocks.is_empty());
assert!(ping.blocks_meta.is_empty());
assert!(ping.entry.is_empty());
if outbound_tx.send(std::result::Result::Ok(pong_update(1))).await.is_err() {
return;
}
let mutation = inbound.message().await;
let mutation = match mutation {
std::result::Result::Ok(std::option::Option::Some(value)) => value,
_ => return,
};
assert!(mutation.accounts.contains_key("mutated"));
let _ = outbound_tx.send(std::result::Result::Ok(slot_update(901))).await;
},
FixtureMode::ClientHalfClose => {
let half_close = inbound.message().await;
if matches!(half_close, std::result::Result::Ok(std::option::Option::None)) {
half_close_seen.store(true, std::sync::atomic::Ordering::SeqCst);
}
},
FixtureMode::HostileClose => {
let half_close = inbound.message().await;
if matches!(half_close, std::result::Result::Ok(std::option::Option::None)) {
half_close_seen.store(true, std::sync::atomic::Ordering::SeqCst);
}
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
drop(outbound_tx);
},
FixtureMode::Flood => {
for slot in 0_u64..32_u64 {
if outbound_tx.send(std::result::Result::Ok(slot_update(1_000 + slot))).await.is_err() {
return;
}
}
},
FixtureMode::RemoteStatus => {
let _ = outbound_tx.send(std::result::Result::Err(tonic::Status::permission_denied("GRPC-STREAM-REMOTE-SECRET-CANARY"))).await;
},
FixtureMode::Malformed => {
let _ = outbound_tx
.send(std::result::Result::Ok(yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: std::vec::Vec::new(),
update_oneof: std::option::Option::None,
created_at: std::option::Option::None,
}))
.await;
},
FixtureMode::Oversized => {
let _ = outbound_tx.send(std::result::Result::Ok(slot_update(77))).await;
},
FixtureMode::Idle => {
let _ = inbound.message().await;
},
FixtureMode::ReconnectReplay => {
if subscribe_call == 1 {
assert_eq!(initial.from_slot, std::option::Option::None);
let _ = outbound_tx.send(std::result::Result::Ok(slot_update(500))).await;
} else {
assert_eq!(subscribe_call, 2);
assert_eq!(initial.from_slot, std::option::Option::Some(500));
if outbound_tx.send(std::result::Result::Ok(slot_update(500))).await.is_err() {
return;
}
if outbound_tx.send(std::result::Result::Ok(slot_update(501))).await.is_err() {
return;
}
let half_close = inbound.message().await;
if matches!(half_close, std::result::Result::Ok(std::option::Option::None)) {
half_close_seen.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
},
FixtureMode::ReplayGap => {
if subscribe_call == 1 {
assert_eq!(initial.from_slot, std::option::Option::None);
let _ = outbound_tx.send(std::result::Result::Ok(slot_update(500))).await;
} else {
assert_eq!(subscribe_call, 2);
assert_eq!(initial.from_slot, std::option::Option::Some(505));
if outbound_tx.send(std::result::Result::Ok(slot_update(505))).await.is_err() {
return;
}
let half_close = inbound.message().await;
if matches!(half_close, std::result::Result::Ok(std::option::Option::None)) {
half_close_seen.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
},
FixtureMode::ReconnectExhausted => {
assert_eq!(subscribe_call, 1);
let _ = outbound_tx.send(std::result::Result::Ok(slot_update(700))).await;
},
}
});
return std::result::Result::Ok(tonic::Response::new(super::MpscStream::new(outbound_rx)));
}
async fn subscribe_deshred(
&self,
_request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeDeshredRequest>>,
) -> std::result::Result<tonic::Response<Self::SubscribeDeshredStream>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("deshred is outside KSP 0.2.9"));
}
async fn subscribe_replay_info(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::SubscribeReplayInfoRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
let first_available = match self.mode {
FixtureMode::ReconnectReplay | FixtureMode::ReconnectExhausted => std::option::Option::Some(400),
FixtureMode::ReplayGap => std::option::Option::Some(505),
_ => return std::result::Result::Err(tonic::Status::unimplemented("replay info is outside this fixture mode")),
};
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse { first_available }));
}
async fn ping(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::PingRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::PongResponse>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside this fixture"));
}
async fn get_latest_blockhash(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetLatestBlockhashRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside this fixture"));
}
async fn get_block_height(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetBlockHeightRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetBlockHeightResponse>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside this fixture"));
}
async fn get_slot(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetSlotRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetSlotResponse>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside this fixture"));
}
async fn is_blockhash_valid(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::IsBlockhashValidRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::IsBlockhashValidResponse>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside this fixture"));
}
async fn get_version(
&self,
_request: tonic::Request<yellowstone_grpc_proto::geyser::GetVersionRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetVersionResponse>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("unary is outside this fixture"));
}
}
struct FixtureServer {
endpoint_url: std::string::String,
half_close_seen: std::sync::Arc<std::sync::atomic::AtomicBool>,
subscribe_calls: std::sync::Arc<std::sync::atomic::AtomicUsize>,
shutdown: std::option::Option<tokio::sync::oneshot::Sender<()>>,
task: tokio::task::JoinHandle<()>,
}
impl FixtureServer {
async fn start(mode: FixtureMode) -> Self {
let bind_address: std::net::SocketAddr = "127.0.0.1:0".parse().expect("fixture bind address must parse");
let incoming = tonic::transport::server::TcpIncoming::bind(bind_address).expect("fixture gRPC listener must bind");
let local_address = incoming.local_addr().expect("fixture gRPC listener must expose local address");
let (shutdown, shutdown_receiver) = tokio::sync::oneshot::channel();
let half_close_seen = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let subscribe_calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let service_half_close_seen = half_close_seen.clone();
let service_subscribe_calls = subscribe_calls.clone();
let task = tokio::spawn(async move {
let service = yellowstone_grpc_proto::geyser::geyser_server::GeyserServer::new(FixtureGeyser {
mode,
half_close_seen: service_half_close_seen,
subscribe_calls: service_subscribe_calls,
});
let result = tonic::transport::Server::builder()
.serve_with_incoming_shutdown(service, incoming, async move {
let _ = shutdown_receiver.await;
})
.await;
assert!(result.is_ok());
});
return Self {
endpoint_url: format!("http://{local_address}"),
half_close_seen,
subscribe_calls,
shutdown: std::option::Option::Some(shutdown),
task,
};
}
async fn stop(mut self) {
if let std::option::Option::Some(shutdown) = self.shutdown.take() {
let _ = shutdown.send(());
}
let result = self.task.await;
assert!(result.is_ok());
}
}
fn verify_fixture_metadata(metadata: &tonic::metadata::MetadataMap) -> std::result::Result<(), tonic::Status> {
let public = metadata.get("x-ksp-public").and_then(|value| return value.to_str().ok());
let secret = metadata.get("x-ksp-token").and_then(|value| return value.to_str().ok());
if public != std::option::Option::Some("fixture-public") || secret != std::option::Option::Some("GRPC-STREAM-SECRET-CANARY") {
return std::result::Result::Err(tonic::Status::unauthenticated("fixture metadata mismatch"));
}
return std::result::Result::Ok(());
}
fn fixture_settings(
url: &str,
request_capacity: usize,
update_capacity: usize,
max_inbound_message_size_bytes: usize,
max_outbound_message_size_bytes: usize,
) -> crate::YellowstoneGrpcEndpointSettings {
let defaults = crate::YellowstoneGrpcSessionSettings::default();
return fixture_settings_with_reconnect(
url,
request_capacity,
update_capacity,
max_inbound_message_size_bytes,
max_outbound_message_size_bytes,
crate::YellowstoneGrpcReconnectSettings::new(0, defaults.reconnect().initial_backoff(), defaults.reconnect().max_backoff()),
);
}
fn fixture_settings_with_reconnect(
url: &str,
request_capacity: usize,
update_capacity: usize,
max_inbound_message_size_bytes: usize,
max_outbound_message_size_bytes: usize,
reconnect: crate::YellowstoneGrpcReconnectSettings,
) -> crate::YellowstoneGrpcEndpointSettings {
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let session = crate::YellowstoneGrpcSessionSettings::new(
defaults.connect_timeout(),
defaults.unary_timeout(),
std::time::Duration::from_millis(250),
reconnect,
request_capacity,
update_capacity,
max_inbound_message_size_bytes,
max_outbound_message_size_bytes,
);
let metadata = std::vec![
crate::YellowstoneGrpcMetadataEntry::public("x-ksp-public", "fixture-public").expect("fixture public metadata must be valid"),
crate::YellowstoneGrpcMetadataEntry::secret("x-ksp-token", "GRPC-STREAM-SECRET-CANARY").expect("fixture secret metadata must be valid"),
];
return crate::YellowstoneGrpcEndpointSettings::new(
"fixture-stream",
true,
crate::YellowstoneGrpcProviderName::new("fixture-provider"),
crate::YellowstoneGrpcClusterName::new("devnet"),
crate::YellowstoneGrpcEndpointUrl::parse(url).expect("fixture URL must parse"),
session,
)
.with_metadata(metadata)
.expect("fixture metadata settings must validate");
}
fn initial_request() -> crate::YellowstoneSubscribeRequest {
let mut request = crate::YellowstoneSubscribeRequest::new();
request
.insert_slot_filter(
crate::YellowstoneSubscribeFilterName::new("initial").expect("fixture filter name must validate"),
crate::YellowstoneSubscribeSlotFilter::new(),
)
.expect("fixture slot filter must insert");
return request;
}
fn mutated_request() -> crate::YellowstoneSubscribeRequest {
let mut request = crate::YellowstoneSubscribeRequest::new();
request
.insert_account_filter(
crate::YellowstoneSubscribeFilterName::new("mutated").expect("fixture filter name must validate"),
crate::YellowstoneSubscribeAccountFilter::new(),
)
.expect("fixture account filter must insert");
return request;
}
fn ping_update() -> yellowstone_grpc_proto::geyser::SubscribeUpdate {
return yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: std::vec::Vec::new(),
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Ping(
yellowstone_grpc_proto::geyser::SubscribeUpdatePing {},
)),
created_at: std::option::Option::None,
};
}
fn pong_update(id: i32) -> yellowstone_grpc_proto::geyser::SubscribeUpdate {
return yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: std::vec::Vec::new(),
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Pong(
yellowstone_grpc_proto::geyser::SubscribeUpdatePong { id },
)),
created_at: std::option::Option::None,
};
}
fn slot_update(slot: u64) -> yellowstone_grpc_proto::geyser::SubscribeUpdate {
return yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: std::vec!["initial".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Slot(
yellowstone_grpc_proto::geyser::SubscribeUpdateSlot {
slot,
parent: std::option::Option::Some(slot.saturating_sub(1)),
status: yellowstone_grpc_proto::geyser::SlotStatus::SlotProcessed as i32,
dead_error: std::option::Option::None,
},
)),
created_at: std::option::Option::None,
};
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_bidi_round_trip_mutates_request_replies_to_ping_and_observes_server_half_close() {
let server = FixtureServer::start(FixtureMode::RoundTrip).await;
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
assert_eq!(session.state(), crate::YellowstoneGrpcSubscribeState::Active);
let ping = session.next_update().await.expect("Ping update must decode").expect("Ping update must be present");
assert!(matches!(ping, crate::YellowstoneSubscribeUpdate::Ping(_)));
let pong = session.next_update().await.expect("Pong update must decode").expect("Pong update must be present");
match pong {
crate::YellowstoneSubscribeUpdate::Pong(value) => assert_eq!(value.id(), 1),
_ => panic!("fixture must return Pong"),
}
session.try_update(&mutated_request()).expect("request mutation must enter bounded queue");
let slot = session.next_update().await.expect("slot update must decode").expect("slot update must be present");
match slot {
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 901),
_ => panic!("fixture must return Slot"),
}
assert!(session.next_update().await.expect("server half-close must be normal").is_none());
assert_eq!(session.state(), crate::YellowstoneGrpcSubscribeState::Closed);
session.close().await.expect("already half-closed fixture must close cleanly");
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_explicit_close_half_closes_request_stream_before_deadline() {
let server = FixtureServer::start(FixtureMode::ClientHalfClose).await;
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
session.close().await.expect("graceful half-close must complete");
assert!(server.half_close_seen.load(std::sync::atomic::Ordering::SeqCst));
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_hostile_server_shutdown_is_bounded_by_close_timeout() {
let server = FixtureServer::start(FixtureMode::HostileClose).await;
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
let error = session.close().await.expect_err("hostile server must hit the bounded graceful close deadline");
assert_eq!(error.code(), crate::ERROR_CODE_TIMEOUT);
assert!(server.half_close_seen.load(std::sync::atomic::Ordering::SeqCst));
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_session_drop_best_effort_half_closes_request_stream() {
let server = FixtureServer::start(FixtureMode::ClientHalfClose).await;
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
drop(session);
let observed = tokio::time::timeout(std::time::Duration::from_secs(1), async {
loop {
if server.half_close_seen.load(std::sync::atomic::Ordering::SeqCst) {
return true;
}
tokio::task::yield_now().await;
}
})
.await
.expect("fixture must observe dropped-session half-close before timeout");
assert!(observed);
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_slow_receiver_overflow_is_terminal_and_observable() {
let server = FixtureServer::start(FixtureMode::Flood).await;
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 2, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
assert!(session.next_update().await.expect("first queued update must remain readable").is_some());
assert!(session.next_update().await.expect("second queued update must remain readable").is_some());
let error = session.next_update().await.expect_err("overflow must become terminal after bounded queued updates drain");
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW);
let rendered = format!("{error:?} {session:?}");
assert!(!rendered.contains("GRPC-STREAM-SECRET-CANARY"));
assert!(!rendered.contains(server.endpoint_url.as_str()));
let _ = session.close().await;
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_stream_remote_status_is_safe_and_terminal() {
let server = FixtureServer::start(FixtureMode::RemoteStatus).await;
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
let error = session.next_update().await.expect_err("remote Status must fail stream");
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_STATUS);
let rendered = format!("{error:?} {session:?}");
assert!(!rendered.contains("GRPC-STREAM-REMOTE-SECRET-CANARY"));
assert!(!rendered.contains("GRPC-STREAM-SECRET-CANARY"));
assert!(!rendered.contains(server.endpoint_url.as_str()));
let _ = session.close().await;
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_malformed_update_is_rejected_without_raw_wire_escape() {
let server = FixtureServer::start(FixtureMode::Malformed).await;
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes());
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
let error = session.next_update().await.expect_err("missing update oneof must be rejected");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RESPONSE);
let _ = session.close().await;
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_inbound_message_limit_is_enforced_by_tonic_stream_decoder() {
let server = FixtureServer::start(FixtureMode::Oversized).await;
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, 1, defaults.max_outbound_message_size_bytes());
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
let error = session.next_update().await.expect_err("message above configured inbound bound must fail decoding");
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_STATUS);
let rendered = format!("{error:?} {session:?}");
assert!(!rendered.contains(server.endpoint_url.as_str()));
let _ = session.close().await;
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_outbound_mutation_size_is_rejected_before_queue_dispatch() {
let server = FixtureServer::start(FixtureMode::Idle).await;
let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, 64 * 1024 * 1024, 1);
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let session = channel
.open_standard_subscribe(crate::YellowstoneSubscribeRequest::new())
.await
.expect("empty initial request must fit one-byte fixture bound");
let error = session.try_update(&mutated_request()).expect_err("encoded mutation above configured bound must be rejected before dispatch");
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_BACKPRESSURE_OVERFLOW);
session.close().await.expect("idle fixture must observe client half-close");
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_reconnect_replays_from_last_observed_slot_and_counts_duplicate_identity() {
let server = FixtureServer::start(FixtureMode::ReconnectReplay).await;
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let settings = fixture_settings_with_reconnect(
server.endpoint_url.as_str(),
8,
8,
defaults.max_inbound_message_size_bytes(),
defaults.max_outbound_message_size_bytes(),
crate::YellowstoneGrpcReconnectSettings::new(3, std::time::Duration::from_millis(5), std::time::Duration::from_millis(20)),
);
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
let first = session.next_update().await.expect("first slot must decode").expect("first slot must be present");
match first {
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 500),
_ => panic!("fixture must return first Slot"),
}
let duplicate = session.next_update().await.expect("replayed duplicate must decode").expect("replayed duplicate must be present");
match duplicate {
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 500),
_ => panic!("fixture must return replayed Slot"),
}
let resumed = session.next_update().await.expect("resumed slot must decode").expect("resumed slot must be present");
match resumed {
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 501),
_ => panic!("fixture must return resumed Slot"),
}
let snapshot = session.snapshot();
assert_eq!(snapshot.state(), crate::YellowstoneGrpcSubscribeState::Active);
assert_eq!(snapshot.reconnect_count(), 1);
assert_eq!(snapshot.replay_attempt_count(), 1);
assert_eq!(snapshot.continuity_gap_count(), 0);
assert_eq!(snapshot.duplicate_update_count(), 1);
assert_eq!(snapshot.last_requested_from_slot(), std::option::Option::Some(500));
assert_eq!(snapshot.last_observed_slot(), std::option::Option::Some(501));
session.close().await.expect("reconnected stream must close cleanly");
assert_eq!(server.subscribe_calls.load(std::sync::atomic::Ordering::SeqCst), 2);
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_replay_info_proves_and_clamps_retention_gap_without_lossless_claim() {
let server = FixtureServer::start(FixtureMode::ReplayGap).await;
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let settings = fixture_settings_with_reconnect(
server.endpoint_url.as_str(),
8,
8,
defaults.max_inbound_message_size_bytes(),
defaults.max_outbound_message_size_bytes(),
crate::YellowstoneGrpcReconnectSettings::new(3, std::time::Duration::from_millis(5), std::time::Duration::from_millis(20)),
);
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
let _ = session.next_update().await.expect("first slot must decode").expect("first slot must be present");
let resumed = session.next_update().await.expect("clamped replay must decode").expect("clamped replay must be present");
match resumed {
crate::YellowstoneSubscribeUpdate::Slot(value) => assert_eq!(value.slot(), 505),
_ => panic!("fixture must return clamped Slot"),
}
let snapshot = session.snapshot();
assert_eq!(snapshot.reconnect_count(), 1);
assert_eq!(snapshot.replay_attempt_count(), 1);
assert_eq!(snapshot.continuity_gap_count(), 1);
assert_eq!(snapshot.duplicate_update_count(), 0);
assert_eq!(snapshot.last_requested_from_slot(), std::option::Option::Some(505));
assert_eq!(snapshot.last_observed_slot(), std::option::Option::Some(505));
session.close().await.expect("gap fixture must close cleanly");
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_reconnect_budget_exhaustion_is_terminal_and_safe() {
let server = FixtureServer::start(FixtureMode::ReconnectExhausted).await;
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let settings = fixture_settings_with_reconnect(
server.endpoint_url.as_str(),
8,
8,
defaults.max_inbound_message_size_bytes(),
defaults.max_outbound_message_size_bytes(),
crate::YellowstoneGrpcReconnectSettings::new(2, std::time::Duration::from_millis(5), std::time::Duration::from_millis(10)),
);
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
let _ = session.next_update().await.expect("first slot must decode").expect("first slot must be present");
let error = session.next_update().await.expect_err("failed reconnect attempts must exhaust the bounded budget");
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_CHANNEL_FAILED);
let snapshot = session.snapshot();
assert_eq!(snapshot.state(), crate::YellowstoneGrpcSubscribeState::Failed);
assert_eq!(snapshot.reconnect_count(), 0);
assert_eq!(snapshot.replay_attempt_count(), 2);
assert_eq!(snapshot.terminal_error_code(), std::option::Option::Some(crate::ERROR_CODE_GRPC_CHANNEL_FAILED));
let rendered = format!("{error:?} {session:?}");
assert!(!rendered.contains("GRPC-RECONNECT-SECRET-CANARY"));
assert!(!rendered.contains(server.endpoint_url.as_str()));
assert_eq!(server.subscribe_calls.load(std::sync::atomic::Ordering::SeqCst), 3);
let _ = session.close().await;
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_shutdown_interrupts_reconnect_backoff_and_mutation_is_rejected_during_reconnect() {
let server = FixtureServer::start(FixtureMode::ReconnectReplay).await;
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let settings = fixture_settings_with_reconnect(
server.endpoint_url.as_str(),
8,
8,
defaults.max_inbound_message_size_bytes(),
defaults.max_outbound_message_size_bytes(),
crate::YellowstoneGrpcReconnectSettings::new(3, std::time::Duration::from_millis(500), std::time::Duration::from_millis(500)),
);
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let mut session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open");
let _ = session.next_update().await.expect("first slot must decode").expect("first slot must be present");
tokio::time::timeout(std::time::Duration::from_secs(1), async {
loop {
if session.state() == crate::YellowstoneGrpcSubscribeState::Reconnecting {
return;
}
tokio::task::yield_now().await;
}
})
.await
.expect("fixture must enter reconnecting state");
let mutation_error = session.try_update(&mutated_request()).expect_err("request mutation must be rejected during reconnect");
assert_eq!(mutation_error.code(), crate::ERROR_CODE_GRPC_SESSION_CLOSED);
session.close().await.expect("shutdown must interrupt reconnect backoff without opening another stream");
assert_eq!(server.subscribe_calls.load(std::sync::atomic::Ordering::SeqCst), 1);
server.stop().await;
}

View File

@@ -0,0 +1,777 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs
// version: 5
fn filter_name(value: &str) -> crate::YellowstoneSubscribeFilterName {
return crate::YellowstoneSubscribeFilterName::new(value).expect("fixture filter name must validate");
}
fn minimal_transaction_info(signature_byte: u8, index: u64) -> yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo {
let meta = yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionStatusMeta { fee: 5_000, ..std::default::Default::default() };
return yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo {
signature: vec![signature_byte; 64],
is_vote: false,
transaction: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Transaction {
signatures: vec![vec![signature_byte; 64]],
message: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Message {
header: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 0,
}),
account_keys: vec![vec![1_u8; 32]],
recent_blockhash: vec![2_u8; 32],
instructions: vec![],
versioned: false,
address_table_lookups: vec![],
config: std::option::Option::None,
}),
}),
meta: std::option::Option::Some(meta),
index,
};
}
#[test]
fn yellowstone_subscribe_filter_name_is_bounded_globally_unique_and_debug_redacted() {
assert!(crate::YellowstoneSubscribeFilterName::new("").is_err());
assert!(crate::YellowstoneSubscribeFilterName::new(" leading").is_err());
assert!(crate::YellowstoneSubscribeFilterName::new("trailing ").is_err());
assert!(crate::YellowstoneSubscribeFilterName::new("line\nbreak").is_err());
assert!(crate::YellowstoneSubscribeFilterName::new("x".repeat(129)).is_err());
let name = filter_name("account-primary");
assert_eq!(name.as_str(), "account-primary");
assert!(!format!("{name:?}").contains("account-primary"));
let mut request = crate::YellowstoneSubscribeRequest::new();
assert!(request.insert_account_filter(name.clone(), crate::YellowstoneSubscribeAccountFilter::new()).is_ok());
let duplicate = request.insert_slot_filter(name, crate::YellowstoneSubscribeSlotFilter::new());
assert!(duplicate.is_err());
}
#[test]
fn yellowstone_subscribe_empty_and_named_empty_maps_encode_exactly() {
let empty = crate::YellowstoneSubscribeRequest::new().to_wire().expect("empty request must encode");
assert!(empty.accounts.is_empty());
assert!(empty.slots.is_empty());
assert!(empty.transactions.is_empty());
assert!(empty.transactions_status.is_empty());
assert!(empty.blocks.is_empty());
assert!(empty.blocks_meta.is_empty());
assert!(empty.entry.is_empty());
assert_eq!(empty.commitment, std::option::Option::None);
assert!(empty.accounts_data_slice.is_empty());
assert_eq!(empty.ping, std::option::Option::None);
assert_eq!(empty.from_slot, std::option::Option::None);
let mut request = crate::YellowstoneSubscribeRequest::new();
assert!(request.insert_account_filter(filter_name("accounts"), crate::YellowstoneSubscribeAccountFilter::new()).is_ok());
assert!(request.insert_slot_filter(filter_name("slots"), crate::YellowstoneSubscribeSlotFilter::new()).is_ok());
assert!(request.insert_transaction_filter(filter_name("transactions"), crate::YellowstoneSubscribeTransactionFilter::new()).is_ok());
assert!(request.insert_transaction_status_filter(filter_name("transaction-status"), crate::YellowstoneSubscribeTransactionFilter::new()).is_ok());
assert!(request.insert_block_filter(filter_name("blocks"), crate::YellowstoneSubscribeBlockFilter::new()).is_ok());
assert!(request.insert_blocks_meta_filter(filter_name("blocks-meta"), crate::YellowstoneSubscribeBlocksMetaFilter::new()).is_ok());
assert!(request.insert_entry_filter(filter_name("entry"), crate::YellowstoneSubscribeEntryFilter::new()).is_ok());
let wire = request.to_wire().expect("named empty request must encode");
assert_eq!(wire.accounts.len(), 1);
assert_eq!(wire.slots.len(), 1);
assert_eq!(wire.transactions.len(), 1);
assert_eq!(wire.transactions_status.len(), 1);
assert_eq!(wire.blocks.len(), 1);
assert_eq!(wire.blocks_meta.len(), 1);
assert_eq!(wire.entry.len(), 1);
assert_eq!(wire.accounts.get("accounts"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterAccounts::default()));
assert_eq!(wire.slots.get("slots"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterSlots::default()));
assert_eq!(
wire.transactions.get("transactions"),
std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default()),
);
assert_eq!(
wire.transactions_status.get("transaction-status"),
std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterTransactions::default())
);
assert_eq!(wire.blocks.get("blocks"), std::option::Option::Some(&yellowstone_grpc_proto::geyser::SubscribeRequestFilterBlocks::default()));
assert!(wire.blocks_meta.contains_key("blocks-meta"));
assert!(wire.entry.contains_key("entry"));
}
#[test]
fn yellowstone_subscribe_common_fields_preserve_optional_and_ordered_wire_semantics() {
let mut request = crate::YellowstoneSubscribeRequest::new();
request.set_commitment(std::option::Option::Some(crate::SolanaCommitment::Finalized));
let first = crate::YellowstoneAccountsDataSlice::new(8, 16).expect("first slice must validate");
let second = crate::YellowstoneAccountsDataSlice::new(64, 0).expect("zero-length slice remains representable");
assert!(request.push_accounts_data_slice(first).is_ok());
assert!(request.push_accounts_data_slice(second).is_ok());
request.set_ping(std::option::Option::Some(crate::YellowstoneSubscribePing::new(-7)));
request.set_from_slot(std::option::Option::Some(42));
assert_eq!(request.commitment(), std::option::Option::Some(crate::SolanaCommitment::Finalized));
assert_eq!(request.accounts_data_slices(), &[first, second]);
assert_eq!(request.ping(), std::option::Option::Some(crate::YellowstoneSubscribePing::new(-7)));
assert_eq!(request.from_slot(), std::option::Option::Some(42));
let wire = request.to_wire().expect("common fields must encode");
assert_eq!(wire.commitment, std::option::Option::Some(yellowstone_grpc_proto::geyser::CommitmentLevel::Finalized as i32));
assert_eq!(wire.accounts_data_slice.len(), 2);
assert_eq!(wire.accounts_data_slice[0].offset, 8);
assert_eq!(wire.accounts_data_slice[0].length, 16);
assert_eq!(wire.accounts_data_slice[1].offset, 64);
assert_eq!(wire.accounts_data_slice[1].length, 0);
assert_eq!(wire.ping.map(|ping| return ping.id), std::option::Option::Some(-7));
assert_eq!(wire.from_slot, std::option::Option::Some(42));
}
#[test]
fn yellowstone_subscribe_common_bounds_reject_before_wire_conversion() {
assert!(crate::YellowstoneAccountsDataSlice::new(0, 64 * 1024 * 1024).is_ok());
assert!(crate::YellowstoneAccountsDataSlice::new(0, 64 * 1024 * 1024 + 1).is_err());
assert!(crate::YellowstoneAccountsDataSlice::new(u64::MAX, 1).is_err());
let mut slices = crate::YellowstoneSubscribeRequest::new();
for index in 0..128_u64 {
let slice = crate::YellowstoneAccountsDataSlice::new(index, 1).expect("bounded fixture slice must validate");
assert!(slices.push_accounts_data_slice(slice).is_ok());
}
let excess = crate::YellowstoneAccountsDataSlice::new(129, 1).expect("excess fixture slice itself must validate");
assert!(slices.push_accounts_data_slice(excess).is_err());
let mut filters = crate::YellowstoneSubscribeRequest::new();
for index in 0..1_024_u32 {
let name = crate::YellowstoneSubscribeFilterName::new(format!("f{index}")).expect("bounded filter name must validate");
assert!(filters.insert_account_filter(name, crate::YellowstoneSubscribeAccountFilter::new()).is_ok());
}
let excess_name = crate::YellowstoneSubscribeFilterName::new("excess").expect("excess filter name must validate independently");
assert!(filters.insert_account_filter(excess_name, crate::YellowstoneSubscribeAccountFilter::new()).is_err());
}
#[test]
fn yellowstone_subscribe_debug_omits_filter_names_and_future_payloads() {
let mut request = crate::YellowstoneSubscribeRequest::new();
assert!(request.insert_account_filter(filter_name("sensitive-label"), crate::YellowstoneSubscribeAccountFilter::new()).is_ok());
request.set_ping(std::option::Option::Some(crate::YellowstoneSubscribePing::new(9)));
request.set_from_slot(std::option::Option::Some(77));
let debug = format!("{request:?}");
assert!(!debug.contains("sensitive-label"));
assert!(debug.contains("account_filter_count"));
assert!(debug.contains("from_slot"));
}
#[test]
fn yellowstone_account_and_slot_filters_encode_complete_current_wire() {
let account = ksp_core_lib::Pubkey::new_from_array([1_u8; 32]);
let owner = ksp_core_lib::Pubkey::new_from_array([2_u8; 32]);
let mut filter = crate::YellowstoneSubscribeAccountFilter::new();
assert!(filter.push_account(account).is_ok());
assert!(filter.push_owner(owner).is_ok());
let raw = crate::YellowstoneAccountMemcmp::bytes(4, vec![1_u8, 2, 3]).expect("raw memcmp must validate");
let base58 = crate::YellowstoneAccountMemcmp::base58(8, "1234").expect("base58 memcmp must validate");
let base64 = crate::YellowstoneAccountMemcmp::base64(12, "AQID==").expect("base64 memcmp must validate");
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Memcmp(raw)).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Memcmp(base58)).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Memcmp(base64)).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::DataSize(165)).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::TokenAccountState(true)).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Lamports(crate::YellowstoneAccountLamportsFilter::Eq(1))).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Lamports(crate::YellowstoneAccountLamportsFilter::Ne(2))).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Lamports(crate::YellowstoneAccountLamportsFilter::Lt(3))).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Lamports(crate::YellowstoneAccountLamportsFilter::Gt(4))).is_ok());
filter.set_nonempty_txn_signature(std::option::Option::Some(true));
let cuckoo =
crate::YellowstoneCuckooFilter::new(vec![9_u8; 16], 4, 4, 8, 77, crate::YellowstoneCuckooHashAlgorithm::SipHash).expect("cuckoo fixture must validate");
filter.set_cuckoo_accounts_filter(std::option::Option::Some(cuckoo));
let wire = filter.to_wire();
assert_eq!(wire.account, vec![account.to_string()]);
assert_eq!(wire.owner, vec![owner.to_string()]);
assert_eq!(wire.filters.len(), 9);
match wire.filters[0].filter.as_ref().expect("raw memcmp oneof must be present") {
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter::Filter::Memcmp(value) => assert_eq!(
value.data,
std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter_memcmp::Data::Bytes(vec![1_u8, 2, 3]))
),
_ => panic!("first predicate must stay memcmp"),
}
match wire.filters[5].filter.as_ref().expect("lamports oneof must be present") {
yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter::Filter::Lamports(value) => {
assert_eq!(value.cmp, std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_request_filter_accounts_filter_lamports::Cmp::Eq(1)))
},
_ => panic!("sixth predicate must stay lamports"),
}
assert_eq!(wire.nonempty_txn_signature, std::option::Option::Some(true));
let cuckoo = wire.cuckoo_accounts_filter.expect("cuckoo filter must be present");
assert_eq!(cuckoo.data, vec![9_u8; 16]);
assert_eq!(cuckoo.bucket_count, 4);
assert_eq!(cuckoo.entries_per_bucket, 4);
assert_eq!(cuckoo.fingerprint_bits, 8);
assert_eq!(cuckoo.hash_seed, 77);
assert_eq!(cuckoo.hash_algorithm, yellowstone_grpc_proto::geyser::CuckooHashAlgorithm::SipHash as i32);
let mut slots = crate::YellowstoneSubscribeSlotFilter::new();
slots.set_filter_by_commitment(std::option::Option::Some(false));
slots.set_interslot_updates(std::option::Option::Some(true));
assert_eq!(slots.filter_by_commitment(), std::option::Option::Some(false));
assert_eq!(slots.interslot_updates(), std::option::Option::Some(true));
let slots_wire = slots.to_wire();
assert_eq!(slots_wire.filter_by_commitment, std::option::Option::Some(false));
assert_eq!(slots_wire.interslot_updates, std::option::Option::Some(true));
}
#[test]
fn yellowstone_account_filter_bounds_and_debug_are_provider_neutral() {
assert!(crate::YellowstoneCuckooFilter::new(vec![], 0, 4, 8, 0, crate::YellowstoneCuckooHashAlgorithm::SipHash).is_err());
assert!(crate::YellowstoneCuckooFilter::new(vec![], 1, 0, 8, 0, crate::YellowstoneCuckooHashAlgorithm::SipHash).is_err());
assert!(crate::YellowstoneCuckooFilter::new(vec![], 1, 4, 7, 0, crate::YellowstoneCuckooHashAlgorithm::SipHash).is_err());
assert!(crate::YellowstoneCuckooFilter::new(vec![], 1, 4, 12, 0, crate::YellowstoneCuckooHashAlgorithm::SipHash).is_ok());
assert!(crate::YellowstoneAccountMemcmp::base58(0, "contains whitespace").is_err());
assert!(crate::YellowstoneAccountMemcmp::base64(0, "line\nbreak").is_err());
let memcmp = crate::YellowstoneAccountMemcmp::bytes(5, vec![7_u8, 8, 9]).expect("memcmp must validate");
let debug = format!("{memcmp:?}");
assert!(debug.contains("payload_length"));
assert!(!debug.contains("7, 8, 9"));
let mut filter = crate::YellowstoneSubscribeAccountFilter::new();
assert!(filter.push_account(ksp_core_lib::Pubkey::new_from_array([3_u8; 32])).is_ok());
assert!(filter.push_filter(crate::YellowstoneAccountFilterPredicate::Memcmp(memcmp)).is_ok());
let filter_debug = format!("{filter:?}");
assert!(filter_debug.contains("account_count"));
assert!(!filter_debug.contains(&ksp_core_lib::Pubkey::new_from_array([3_u8; 32]).to_string()));
}
#[test]
fn yellowstone_account_update_decodes_complete_wire_and_redacts_payload_debug() {
let signature = vec![6_u8; 64];
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["accounts-main".to_owned(), "accounts-owner".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Account(
yellowstone_grpc_proto::geyser::SubscribeUpdateAccount {
account: std::option::Option::Some(yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo {
pubkey: vec![1_u8; 32],
lamports: 42,
owner: vec![2_u8; 32],
executable: true,
rent_epoch: 9,
data: vec![0xAA_u8, 0xBB, 0xCC],
write_version: 7,
txn_signature: std::option::Option::Some(signature),
}),
slot: 123,
is_startup: true,
},
)),
created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 1_700_000_000, nanos: 123_456_789 }),
};
let update = super::decode_account_update(wire).expect("account update fixture must decode");
assert_eq!(update.filters()[0].as_str(), "accounts-main");
assert_eq!(update.filters()[1].as_str(), "accounts-owner");
assert_eq!(update.created_at().expect("timestamp must be present").seconds(), 1_700_000_000);
assert_eq!(update.created_at().expect("timestamp must be present").nanos(), 123_456_789);
assert_eq!(update.slot(), 123);
assert!(update.is_startup());
assert_eq!(update.account().pubkey(), &ksp_core_lib::Pubkey::new_from_array([1_u8; 32]));
assert_eq!(update.account().owner(), &ksp_core_lib::Pubkey::new_from_array([2_u8; 32]));
assert_eq!(update.account().lamports(), 42);
assert!(update.account().executable());
assert_eq!(update.account().rent_epoch(), 9);
assert_eq!(update.account().data(), &[0xAA_u8, 0xBB, 0xCC]);
assert_eq!(update.account().write_version(), 7);
assert_eq!(update.account().transaction_signature().expect("signature must be present").as_bytes(), &[6_u8; 64]);
let debug = format!("{update:?}");
assert!(!debug.contains("accounts-main"));
assert!(!debug.contains("170, 187, 204"));
assert!(!debug.contains(&ksp_core_lib::Pubkey::new_from_array([1_u8; 32]).to_string()));
}
#[test]
fn yellowstone_account_update_rejects_malformed_fixed_width_and_envelope_fields() {
let malformed_pubkey = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["accounts".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Account(
yellowstone_grpc_proto::geyser::SubscribeUpdateAccount {
account: std::option::Option::Some(yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo {
pubkey: vec![1_u8; 31],
lamports: 0,
owner: vec![2_u8; 32],
executable: false,
rent_epoch: 0,
data: vec![],
write_version: 0,
txn_signature: std::option::Option::None,
}),
slot: 0,
is_startup: false,
},
)),
created_at: std::option::Option::None,
};
assert!(super::decode_account_update(malformed_pubkey).is_err());
let malformed_signature = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["accounts".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Account(
yellowstone_grpc_proto::geyser::SubscribeUpdateAccount {
account: std::option::Option::Some(yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo {
pubkey: vec![1_u8; 32],
lamports: 0,
owner: vec![2_u8; 32],
executable: false,
rent_epoch: 0,
data: vec![],
write_version: 0,
txn_signature: std::option::Option::Some(vec![9_u8; 63]),
}),
slot: 0,
is_startup: false,
},
)),
created_at: std::option::Option::None,
};
assert!(super::decode_account_update(malformed_signature).is_err());
let missing_info = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["accounts".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Account(
yellowstone_grpc_proto::geyser::SubscribeUpdateAccount { account: std::option::Option::None, slot: 0, is_startup: false },
)),
created_at: std::option::Option::None,
};
assert!(super::decode_account_update(missing_info).is_err());
}
#[test]
fn yellowstone_slot_update_preserves_all_current_statuses_and_bounds_dead_error() {
let statuses = [
(yellowstone_grpc_proto::geyser::SlotStatus::SlotProcessed, crate::YellowstoneSlotStatus::Processed),
(yellowstone_grpc_proto::geyser::SlotStatus::SlotConfirmed, crate::YellowstoneSlotStatus::Confirmed),
(yellowstone_grpc_proto::geyser::SlotStatus::SlotFinalized, crate::YellowstoneSlotStatus::Finalized),
(yellowstone_grpc_proto::geyser::SlotStatus::SlotFirstShredReceived, crate::YellowstoneSlotStatus::FirstShredReceived),
(yellowstone_grpc_proto::geyser::SlotStatus::SlotCompleted, crate::YellowstoneSlotStatus::Completed),
(yellowstone_grpc_proto::geyser::SlotStatus::SlotCreatedBank, crate::YellowstoneSlotStatus::CreatedBank),
(yellowstone_grpc_proto::geyser::SlotStatus::SlotDead, crate::YellowstoneSlotStatus::Dead),
];
for (wire_status, expected) in statuses {
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["slots".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Slot(
yellowstone_grpc_proto::geyser::SubscribeUpdateSlot {
slot: 88,
parent: std::option::Option::Some(87),
status: wire_status as i32,
dead_error: if expected == crate::YellowstoneSlotStatus::Dead {
std::option::Option::Some("fork rejected".to_owned())
} else {
std::option::Option::None
},
},
)),
created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 5, nanos: 6 }),
};
let update = super::decode_slot_update(wire).expect("slot update fixture must decode");
assert_eq!(update.status(), expected);
assert_eq!(update.slot(), 88);
assert_eq!(update.parent(), std::option::Option::Some(87));
assert_eq!(update.filters()[0].as_str(), "slots");
if expected == crate::YellowstoneSlotStatus::Dead {
assert_eq!(update.dead_error(), std::option::Option::Some("fork rejected"));
assert!(!format!("{update:?}").contains("fork rejected"));
}
}
let unknown = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["slots".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Slot(
yellowstone_grpc_proto::geyser::SubscribeUpdateSlot {
slot: 1,
parent: std::option::Option::None,
status: 99,
dead_error: std::option::Option::None,
},
)),
created_at: std::option::Option::None,
};
assert!(super::decode_slot_update(unknown).is_err());
let oversized = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["slots".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Slot(
yellowstone_grpc_proto::geyser::SubscribeUpdateSlot {
slot: 1,
parent: std::option::Option::None,
status: yellowstone_grpc_proto::geyser::SlotStatus::SlotDead as i32,
dead_error: std::option::Option::Some("x".repeat(16 * 1024 + 1)),
},
)),
created_at: std::option::Option::None,
};
assert!(super::decode_slot_update(oversized).is_err());
}
#[test]
fn yellowstone_transaction_filters_encode_complete_current_wire_and_redact_selectors() {
let mut filter = crate::YellowstoneSubscribeTransactionFilter::new();
filter.set_vote(std::option::Option::Some(false));
filter.set_failed(std::option::Option::Some(true));
let signature = crate::YellowstoneTransactionSignatureSelector::new("1".repeat(64)).expect("signature selector must validate");
filter.set_signature(std::option::Option::Some(signature));
assert!(filter.push_account_include(ksp_core_lib::Pubkey::new_from_array([1_u8; 32])).is_ok());
assert!(filter.push_account_exclude(ksp_core_lib::Pubkey::new_from_array([2_u8; 32])).is_ok());
assert!(filter.push_account_required(ksp_core_lib::Pubkey::new_from_array([3_u8; 32])).is_ok());
let cuckoo =
crate::YellowstoneCuckooFilter::new(vec![0_u8; 16], 4, 4, 8, 7, crate::YellowstoneCuckooHashAlgorithm::SipHash).expect("cuckoo filter must validate");
filter.set_cuckoo_account_include(std::option::Option::Some(cuckoo));
filter.set_token_accounts(std::option::Option::Some(crate::YellowstoneTokenAccountExpansion::BalanceChanged));
let wire = filter.to_wire();
assert_eq!(wire.vote, std::option::Option::Some(false));
assert_eq!(wire.failed, std::option::Option::Some(true));
assert!(wire.signature.is_some());
assert_eq!(wire.account_include.len(), 1);
assert_eq!(wire.account_exclude.len(), 1);
assert_eq!(wire.account_required.len(), 1);
assert!(wire.cuckoo_account_include.is_some());
assert_eq!(wire.token_accounts, std::option::Option::Some(yellowstone_grpc_proto::geyser::TokenAccountExpansionControlFlag::BalanceChanged as i32));
let debug = format!("{filter:?}");
assert!(debug.contains("account_include_count"));
assert!(!debug.contains(&"1".repeat(32)));
assert!(!debug.contains(&ksp_core_lib::Pubkey::new_from_array([1_u8; 32]).to_string()));
assert!(crate::YellowstoneTransactionSignatureSelector::new("contains-0-O-I-l").is_err());
}
#[test]
fn yellowstone_transaction_update_decodes_current_storage_wire_including_v1_config_and_meta() {
let confirmed = yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionInfo {
signature: vec![9_u8; 64],
is_vote: false,
transaction: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Transaction {
signatures: vec![vec![9_u8; 64], vec![8_u8; 64]],
message: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Message {
header: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::MessageHeader {
num_required_signatures: 2,
num_readonly_signed_accounts: 1,
num_readonly_unsigned_accounts: 1,
}),
account_keys: vec![vec![1_u8; 32], vec![2_u8; 32]],
recent_blockhash: vec![3_u8; 32],
instructions: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::CompiledInstruction {
program_id_index: 1,
accounts: vec![0_u8, 1],
data: vec![4_u8, 5, 6],
}],
versioned: true,
address_table_lookups: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::MessageAddressTableLookup {
account_key: vec![4_u8; 32],
writable_indexes: vec![1_u8, 2],
readonly_indexes: vec![3_u8],
}],
config: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionConfig {
priority_fee: std::option::Option::Some(7),
compute_unit_limit: std::option::Option::Some(8),
loaded_accounts_data_size_limit: std::option::Option::Some(9),
heap_size: std::option::Option::Some(10),
}),
}),
}),
meta: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionStatusMeta {
err: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionError { err: vec![11_u8, 12] }),
fee: 5_000,
pre_balances: vec![100, 200],
post_balances: vec![90, 210],
inner_instructions: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::InnerInstructions {
index: 0,
instructions: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::InnerInstruction {
program_id_index: 1,
accounts: vec![0_u8],
data: vec![13_u8, 14],
stack_height: std::option::Option::Some(2),
}],
}],
inner_instructions_none: false,
log_messages: vec!["Program log: fixture".to_owned()],
log_messages_none: false,
pre_token_balances: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::TokenBalance {
account_index: 0,
mint: "mint-fixture".to_owned(),
ui_token_amount: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::UiTokenAmount {
ui_amount: 1.5,
decimals: 6,
amount: "1500000".to_owned(),
ui_amount_string: "1.5".to_owned(),
}),
owner: "owner-fixture".to_owned(),
program_id: "program-fixture".to_owned(),
}],
post_token_balances: vec![],
rewards: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::Reward {
pubkey: ksp_core_lib::Pubkey::new_from_array([5_u8; 32]).to_string(),
lamports: 17,
post_balance: 18,
reward_type: yellowstone_grpc_proto::solana::storage::confirmed_block::RewardType::Staking as i32,
commission: "5".to_owned(),
commission_bps: "500".to_owned(),
}],
loaded_writable_addresses: vec![vec![6_u8; 32]],
loaded_readonly_addresses: vec![vec![7_u8; 32]],
return_data: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::ReturnData {
program_id: vec![8_u8; 32],
data: vec![15_u8, 16],
}),
return_data_none: false,
compute_units_consumed: std::option::Option::Some(123),
cost_units: std::option::Option::Some(456),
}),
index: 3,
};
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["transactions-main".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Transaction(
yellowstone_grpc_proto::geyser::SubscribeUpdateTransaction { transaction: std::option::Option::Some(confirmed), slot: 42 },
)),
created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 100, nanos: 200 }),
};
let update = super::decode_transaction_update(wire).expect("transaction update fixture must decode");
assert_eq!(update.slot(), 42);
assert_eq!(update.filters()[0].as_str(), "transactions-main");
assert_eq!(update.transaction().signature().as_bytes(), &[9_u8; 64]);
assert_eq!(format!("{:?}", update.transaction().signature()), "YellowstoneTransactionSignature(<redacted>)");
assert_eq!(update.transaction().index(), 3);
assert_eq!(update.transaction().transaction().signatures().len(), 2);
assert!(update.transaction().transaction().message().versioned());
let config = update.transaction().transaction().message().config().expect("v1 config must be preserved");
assert_eq!(config.priority_fee(), std::option::Option::Some(7));
assert_eq!(config.heap_size(), std::option::Option::Some(10));
assert_eq!(update.transaction().meta().fee(), 5_000);
assert_eq!(update.transaction().meta().error().expect("error must be present").as_bytes(), &[11_u8, 12]);
assert_eq!(update.transaction().meta().inner_instructions()[0].instructions()[0].stack_height(), std::option::Option::Some(2));
assert_eq!(update.transaction().meta().pre_token_balances()[0].ui_token_amount().expect("token amount must be present").amount(), "1500000");
let token_debug = format!("{:?}", update.transaction().meta().pre_token_balances()[0]);
assert!(!token_debug.contains("mint-fixture"));
assert!(!token_debug.contains("owner-fixture"));
assert!(!token_debug.contains("program-fixture"));
assert_eq!(update.transaction().meta().rewards()[0].reward_type(), crate::YellowstoneRewardType::Staking);
assert_eq!(update.transaction().meta().loaded_writable_addresses()[0], ksp_core_lib::Pubkey::new_from_array([6_u8; 32]));
assert_eq!(update.transaction().meta().return_data().expect("return data must be present").data(), &[15_u8, 16]);
assert_eq!(update.transaction().meta().compute_units_consumed(), std::option::Option::Some(123));
assert_eq!(update.transaction().meta().cost_units(), std::option::Option::Some(456));
let debug = format!("{update:?}");
assert!(!debug.contains("Program log: fixture"));
assert!(!debug.contains("11, 12"));
}
#[test]
fn yellowstone_transaction_status_update_preserves_error_and_rejects_malformed_signature() {
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["status".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::TransactionStatus(
yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionStatus {
slot: 55,
signature: vec![2_u8; 64],
is_vote: true,
index: 4,
err: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::TransactionError { err: vec![99_u8] }),
},
)),
created_at: std::option::Option::None,
};
let update = super::decode_transaction_status_update(wire).expect("transaction-status update must decode");
assert_eq!(update.slot(), 55);
assert!(update.is_vote());
assert_eq!(update.index(), 4);
assert_eq!(update.signature().as_bytes(), &[2_u8; 64]);
assert_eq!(update.error().expect("status error must be present").as_bytes(), &[99_u8]);
assert!(!format!("{update:?}").contains("99"));
let malformed = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["status".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::TransactionStatus(
yellowstone_grpc_proto::geyser::SubscribeUpdateTransactionStatus {
slot: 1,
signature: vec![0_u8; 63],
is_vote: false,
index: 0,
err: std::option::Option::None,
},
)),
created_at: std::option::Option::None,
};
assert!(super::decode_transaction_status_update(malformed).is_err());
}
#[test]
fn yellowstone_block_filter_encodes_complete_current_wire_and_redacts_selectors() {
let mut filter = crate::YellowstoneSubscribeBlockFilter::new();
let account = ksp_core_lib::Pubkey::new_from_array([31_u8; 32]);
assert!(filter.push_account_include(account).is_ok());
filter.set_include_transactions(std::option::Option::Some(true));
filter.set_include_accounts(std::option::Option::Some(false));
filter.set_include_entries(std::option::Option::Some(true));
let cuckoo = crate::YellowstoneCuckooFilter::new(vec![0_u8; 16], 4, 4, 8, 9, crate::YellowstoneCuckooHashAlgorithm::SipHash)
.expect("block Cuckoo filter must validate");
filter.set_cuckoo_account_include(std::option::Option::Some(cuckoo));
let wire = filter.to_wire();
assert_eq!(wire.account_include, vec![account.to_string()]);
assert_eq!(wire.include_transactions, std::option::Option::Some(true));
assert_eq!(wire.include_accounts, std::option::Option::Some(false));
assert_eq!(wire.include_entries, std::option::Option::Some(true));
assert!(wire.cuckoo_account_include.is_some());
let debug = format!("{filter:?}");
assert!(debug.contains("account_include_count"));
assert!(!debug.contains(&account.to_string()));
}
#[test]
fn yellowstone_block_update_reuses_transaction_account_entry_dtos_and_preserves_server_counts() {
let blockhash = ksp_core_lib::Pubkey::new_from_array([21_u8; 32]).to_string();
let parent_blockhash = ksp_core_lib::Pubkey::new_from_array([22_u8; 32]).to_string();
let reward_pubkey = ksp_core_lib::Pubkey::new_from_array([23_u8; 32]);
let account_pubkey = ksp_core_lib::Pubkey::new_from_array([24_u8; 32]);
let wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["blocks-main".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Block(
yellowstone_grpc_proto::geyser::SubscribeUpdateBlock {
slot: 500,
blockhash: blockhash.clone(),
rewards: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::Rewards {
rewards: vec![yellowstone_grpc_proto::solana::storage::confirmed_block::Reward {
pubkey: reward_pubkey.to_string(),
lamports: 10,
post_balance: 11,
reward_type: yellowstone_grpc_proto::solana::storage::confirmed_block::RewardType::Fee as i32,
commission: "".to_owned(),
commission_bps: "".to_owned(),
}],
num_partitions: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::NumPartitions { num_partitions: 3 }),
}),
block_time: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::UnixTimestamp { timestamp: 1_700_000_000 }),
block_height: std::option::Option::Some(yellowstone_grpc_proto::solana::storage::confirmed_block::BlockHeight { block_height: 499 }),
transactions: vec![minimal_transaction_info(7, 2)],
parent_slot: 499,
parent_blockhash: parent_blockhash.clone(),
executed_transaction_count: 12,
updated_account_count: 34,
accounts: vec![yellowstone_grpc_proto::geyser::SubscribeUpdateAccountInfo {
pubkey: vec![24_u8; 32],
lamports: 77,
owner: vec![25_u8; 32],
executable: false,
rent_epoch: 4,
data: vec![1_u8, 2, 3],
write_version: 8,
txn_signature: std::option::Option::Some(vec![7_u8; 64]),
}],
entries_count: 56,
entries: vec![yellowstone_grpc_proto::geyser::SubscribeUpdateEntry {
slot: 500,
index: 4,
num_hashes: 5,
hash: vec![26_u8; 32],
executed_transaction_count: 6,
starting_transaction_index: 7,
}],
},
)),
created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 12, nanos: 34 }),
};
let update = super::decode_block_update(wire).expect("block update fixture must decode");
assert_eq!(update.filters()[0].as_str(), "blocks-main");
assert_eq!(update.slot(), 500);
assert_eq!(update.blockhash(), blockhash);
assert_eq!(update.parent_slot(), 499);
assert_eq!(update.parent_blockhash(), parent_blockhash);
assert_eq!(update.block_time(), std::option::Option::Some(1_700_000_000));
assert_eq!(update.block_height(), std::option::Option::Some(499));
assert_eq!(update.executed_transaction_count(), 12);
assert_eq!(update.transactions().len(), 1);
assert_eq!(update.transactions()[0].index(), 2);
assert_eq!(update.updated_account_count(), 34);
assert_eq!(update.accounts().len(), 1);
assert_eq!(update.accounts()[0].pubkey(), &account_pubkey);
assert_eq!(update.entries_count(), 56);
assert_eq!(update.entries().len(), 1);
assert_eq!(update.entries()[0].starting_transaction_index(), 7);
let rewards = update.rewards().expect("block rewards must be present");
assert_eq!(rewards.rewards().len(), 1);
assert_eq!(rewards.num_partitions(), std::option::Option::Some(3));
assert_eq!(rewards.rewards()[0].pubkey(), &reward_pubkey);
let debug = format!("{update:?}");
assert!(!debug.contains(&blockhash));
assert!(!debug.contains(&parent_blockhash));
assert!(!debug.contains(&reward_pubkey.to_string()));
assert!(!debug.contains(&account_pubkey.to_string()));
let malformed = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["blocks-main".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Block(
yellowstone_grpc_proto::geyser::SubscribeUpdateBlock {
slot: 1,
blockhash: "not-a-solana-hash".to_owned(),
rewards: std::option::Option::None,
block_time: std::option::Option::None,
block_height: std::option::Option::None,
transactions: vec![],
parent_slot: 0,
parent_blockhash: ksp_core_lib::Pubkey::new_from_array([1_u8; 32]).to_string(),
executed_transaction_count: 0,
updated_account_count: 0,
accounts: vec![],
entries_count: 0,
entries: vec![],
},
)),
created_at: std::option::Option::None,
};
assert!(super::decode_block_update(malformed).is_err());
}
#[test]
fn yellowstone_block_meta_and_entry_updates_preserve_optional_and_legacy_entry_fields() {
let blockhash = ksp_core_lib::Pubkey::new_from_array([41_u8; 32]).to_string();
let parent_blockhash = ksp_core_lib::Pubkey::new_from_array([42_u8; 32]).to_string();
let meta_wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["meta".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::BlockMeta(
yellowstone_grpc_proto::geyser::SubscribeUpdateBlockMeta {
slot: 900,
blockhash: blockhash.clone(),
rewards: std::option::Option::None,
block_time: std::option::Option::None,
block_height: std::option::Option::None,
parent_slot: 899,
parent_blockhash: parent_blockhash.clone(),
executed_transaction_count: 17,
entries_count: 18,
},
)),
created_at: std::option::Option::None,
};
let meta = super::decode_block_meta_update(meta_wire).expect("block-meta update must decode");
assert_eq!(meta.slot(), 900);
assert_eq!(meta.blockhash(), blockhash);
assert_eq!(meta.parent_blockhash(), parent_blockhash);
assert_eq!(meta.rewards(), std::option::Option::None);
assert_eq!(meta.block_time(), std::option::Option::None);
assert_eq!(meta.block_height(), std::option::Option::None);
assert_eq!(meta.executed_transaction_count(), 17);
assert_eq!(meta.entries_count(), 18);
assert!(!format!("{meta:?}").contains(&blockhash));
let entry_wire = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["entries".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Entry(
yellowstone_grpc_proto::geyser::SubscribeUpdateEntry {
slot: 900,
index: 3,
num_hashes: 4,
hash: vec![43_u8; 32],
executed_transaction_count: 5,
starting_transaction_index: 0,
},
)),
created_at: std::option::Option::Some(yellowstone_grpc_proto::prost_types::Timestamp { seconds: 6, nanos: 7 }),
};
let entry = super::decode_entry_update(entry_wire).expect("entry update must decode");
assert_eq!(entry.filters()[0].as_str(), "entries");
assert_eq!(entry.entry().slot(), 900);
assert_eq!(entry.entry().index(), 3);
assert_eq!(entry.entry().num_hashes(), 4);
assert_eq!(entry.entry().hash().as_bytes(), &[43_u8; 32]);
assert_eq!(entry.entry().executed_transaction_count(), 5);
assert_eq!(entry.entry().starting_transaction_index(), 0);
assert!(!format!("{entry:?}").contains("43, 43"));
let malformed_entry = yellowstone_grpc_proto::geyser::SubscribeUpdate {
filters: vec!["entries".to_owned()],
update_oneof: std::option::Option::Some(yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof::Entry(
yellowstone_grpc_proto::geyser::SubscribeUpdateEntry {
slot: 1,
index: 0,
num_hashes: 0,
hash: vec![0_u8; 31],
executed_transaction_count: 0,
starting_transaction_index: 0,
},
)),
created_at: std::option::Option::None,
};
assert!(super::decode_entry_update(malformed_entry).is_err());
}

View File

@@ -0,0 +1,288 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs
// version: 2
#[derive(Clone, Default)]
struct FixtureGeyser;
#[allow(clippy::implicit_return)] // tonic::async_trait generates async wrapper tails outside the authored fixture bodies.
#[tonic::async_trait]
impl yellowstone_grpc_proto::geyser::geyser_server::Geyser for FixtureGeyser {
type SubscribeStream = std::pin::Pin<
std::boxed::Box<dyn futures_util::Stream<Item = std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdate, tonic::Status>> + Send + 'static>,
>;
type SubscribeDeshredStream = std::pin::Pin<
std::boxed::Box<
dyn futures_util::Stream<Item = std::result::Result<yellowstone_grpc_proto::geyser::SubscribeUpdateDeshred, tonic::Status>> + Send + 'static,
>,
>;
async fn subscribe(
&self,
_request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeRequest>>,
) -> std::result::Result<tonic::Response<Self::SubscribeStream>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("streaming is outside the pre.003 fixture"));
}
async fn subscribe_deshred(
&self,
_request: tonic::Request<tonic::Streaming<yellowstone_grpc_proto::geyser::SubscribeDeshredRequest>>,
) -> std::result::Result<tonic::Response<Self::SubscribeDeshredStream>, tonic::Status> {
return std::result::Result::Err(tonic::Status::unimplemented("deshred is outside KSP 0.2.9"));
}
async fn subscribe_replay_info(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::SubscribeReplayInfoRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::SubscribeReplayInfoResponse {
first_available: std::option::Option::Some(100),
}));
}
async fn ping(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::PingRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::PongResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
let count = request.into_inner().count;
if count == -999 {
return std::result::Result::Err(tonic::Status::permission_denied("GRPC-REMOTE-SECRET-CANARY"));
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::PongResponse { count }));
}
async fn get_latest_blockhash(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::GetLatestBlockhashRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetLatestBlockhashResponse {
slot: 101,
blockhash: "fixture-blockhash".to_owned(),
last_valid_block_height: 999,
}));
}
async fn get_block_height(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::GetBlockHeightRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetBlockHeightResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetBlockHeightResponse { block_height: 202 }));
}
async fn get_slot(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::GetSlotRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetSlotResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
if request.get_ref().commitment == std::option::Option::Some(yellowstone_grpc_proto::geyser::CommitmentLevel::Processed as i32) {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
} else if let std::result::Result::Err(error) =
require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed)
{
return std::result::Result::Err(error);
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetSlotResponse { slot: 303 }));
}
async fn is_blockhash_valid(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::IsBlockhashValidRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::IsBlockhashValidResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = require_commitment(request.get_ref().commitment, yellowstone_grpc_proto::geyser::CommitmentLevel::Confirmed) {
return std::result::Result::Err(error);
}
if request.get_ref().blockhash != "fixture-blockhash" {
return std::result::Result::Err(tonic::Status::invalid_argument("unexpected fixture blockhash"));
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::IsBlockhashValidResponse { slot: 404, valid: true }));
}
async fn get_version(
&self,
request: tonic::Request<yellowstone_grpc_proto::geyser::GetVersionRequest>,
) -> std::result::Result<tonic::Response<yellowstone_grpc_proto::geyser::GetVersionResponse>, tonic::Status> {
if let std::result::Result::Err(error) = verify_fixture_metadata(request.metadata()) {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(tonic::Response::new(yellowstone_grpc_proto::geyser::GetVersionResponse {
version: "fixture-yellowstone-12.6".to_owned(),
}));
}
}
struct FixtureServer {
endpoint_url: std::string::String,
shutdown: std::option::Option<tokio::sync::oneshot::Sender<()>>,
task: tokio::task::JoinHandle<()>,
}
impl FixtureServer {
async fn start() -> Self {
let bind_address: std::net::SocketAddr = "127.0.0.1:0".parse().expect("fixture bind address must parse");
let incoming = tonic::transport::server::TcpIncoming::bind(bind_address).expect("fixture gRPC listener must bind");
let local_address = incoming.local_addr().expect("fixture gRPC listener must expose local address");
let (shutdown, shutdown_receiver) = tokio::sync::oneshot::channel();
let task = tokio::spawn(async move {
let service = yellowstone_grpc_proto::geyser::geyser_server::GeyserServer::new(FixtureGeyser);
let result = tonic::transport::Server::builder()
.serve_with_incoming_shutdown(service, incoming, async move {
let _ = shutdown_receiver.await;
})
.await;
assert!(result.is_ok());
});
return Self { endpoint_url: format!("http://{local_address}"), shutdown: std::option::Option::Some(shutdown), task };
}
async fn stop(mut self) {
if let std::option::Option::Some(shutdown) = self.shutdown.take() {
let _ = shutdown.send(());
}
let result = self.task.await;
assert!(result.is_ok());
}
}
fn verify_fixture_metadata(metadata: &tonic::metadata::MetadataMap) -> std::result::Result<(), tonic::Status> {
let public = metadata.get("x-ksp-public").and_then(|value| return value.to_str().ok());
let secret = metadata.get("x-ksp-token").and_then(|value| return value.to_str().ok());
if public != std::option::Option::Some("fixture-public") || secret != std::option::Option::Some("GRPC-SECRET-CANARY") {
return std::result::Result::Err(tonic::Status::unauthenticated("fixture metadata mismatch"));
}
return std::result::Result::Ok(());
}
fn require_commitment(actual: std::option::Option<i32>, expected: yellowstone_grpc_proto::geyser::CommitmentLevel) -> std::result::Result<(), tonic::Status> {
if actual != std::option::Option::Some(expected as i32) {
return std::result::Result::Err(tonic::Status::invalid_argument("fixture commitment mismatch"));
}
return std::result::Result::Ok(());
}
fn fixture_settings(url: &str, unary_timeout: std::time::Duration) -> crate::YellowstoneGrpcEndpointSettings {
let defaults = crate::YellowstoneGrpcSessionSettings::default();
let session = crate::YellowstoneGrpcSessionSettings::new(
defaults.connect_timeout(),
unary_timeout,
defaults.close_timeout(),
defaults.reconnect().clone(),
defaults.request_channel_capacity(),
defaults.update_channel_capacity(),
defaults.max_inbound_message_size_bytes(),
defaults.max_outbound_message_size_bytes(),
);
let metadata = std::vec![
crate::YellowstoneGrpcMetadataEntry::public("x-ksp-public", "fixture-public").expect("fixture public metadata must be valid"),
crate::YellowstoneGrpcMetadataEntry::secret("x-ksp-token", "GRPC-SECRET-CANARY").expect("fixture secret metadata must be valid"),
];
return crate::YellowstoneGrpcEndpointSettings::new(
"fixture",
true,
crate::YellowstoneGrpcProviderName::new("fixture-provider"),
crate::YellowstoneGrpcClusterName::new("devnet"),
crate::YellowstoneGrpcEndpointUrl::parse(url).expect("fixture URL must parse"),
session,
)
.with_metadata(metadata)
.expect("fixture metadata settings must validate");
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_unary_fixture_covers_all_seven_standard_methods_and_metadata() {
let server = FixtureServer::start().await;
let settings = fixture_settings(server.endpoint_url.as_str(), std::time::Duration::from_secs(1));
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let client = channel.standard_unary_client();
let replay = client.subscribe_replay_info().await.expect("ReplayInfo fixture must succeed");
assert_eq!(replay.first_available(), std::option::Option::Some(100));
let pong = client.ping(7).await.expect("Ping fixture must succeed");
assert_eq!(pong.count(), 7);
let latest = client
.get_latest_blockhash(std::option::Option::Some(crate::SolanaCommitment::Confirmed))
.await
.expect("latest blockhash fixture must succeed");
assert_eq!(latest.slot(), 101);
assert_eq!(latest.blockhash(), "fixture-blockhash");
assert_eq!(latest.last_valid_block_height(), 999);
let height = client.get_block_height(std::option::Option::Some(crate::SolanaCommitment::Confirmed)).await.expect("block height fixture must succeed");
assert_eq!(height.block_height(), 202);
let slot = client.get_slot(std::option::Option::Some(crate::SolanaCommitment::Confirmed)).await.expect("slot fixture must succeed");
assert_eq!(slot.slot(), 303);
let validity = client
.is_blockhash_valid("fixture-blockhash", std::option::Option::Some(crate::SolanaCommitment::Confirmed))
.await
.expect("blockhash validity fixture must succeed");
assert_eq!(validity.slot(), 404);
assert!(validity.valid());
let version = client.get_version().await.expect("version fixture must succeed");
assert_eq!(version.version(), "fixture-yellowstone-12.6");
let rendered = format!("{client:?} {channel:?}");
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
assert!(!rendered.contains(server.endpoint_url.as_str()));
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_remote_status_does_not_copy_message_details_or_metadata() {
let server = FixtureServer::start().await;
let settings = fixture_settings(server.endpoint_url.as_str(), std::time::Duration::from_secs(1));
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let error = channel.standard_unary_client().ping(-999).await.expect_err("fixture must return remote status");
assert_eq!(error.code(), crate::ERROR_CODE_GRPC_STATUS);
let rendered = format!("{error:?}");
assert!(!rendered.contains("GRPC-REMOTE-SECRET-CANARY"));
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
assert!(!rendered.contains(server.endpoint_url.as_str()));
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_unary_timeout_is_bounded_and_safe() {
let server = FixtureServer::start().await;
let settings = fixture_settings(server.endpoint_url.as_str(), std::time::Duration::from_millis(5));
let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect");
let error = channel
.standard_unary_client()
.get_slot(std::option::Option::Some(crate::SolanaCommitment::Processed))
.await
.expect_err("delayed fixture must exceed unary timeout");
assert_eq!(error.code(), crate::ERROR_CODE_TIMEOUT);
let rendered = format!("{error:?}");
assert!(!rendered.contains("GRPC-SECRET-CANARY"));
assert!(!rendered.contains(server.endpoint_url.as_str()));
server.stop().await;
}
#[tokio::test(flavor = "current_thread")]
async fn yellowstone_invalid_blockhash_is_rejected_before_io() {
let settings = fixture_settings("http://127.0.0.1:1", std::time::Duration::from_secs(1));
let channel = crate::YellowstoneGrpcChannel::prepare(&settings).expect("lazy fixture channel must prepare");
let error = channel
.standard_unary_client()
.is_blockhash_valid(" ", std::option::Option::None)
.await
.expect_err("invalid blockhash must be rejected locally");
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/client.rs // file: crates/ksp-onchain-transport-lib/unit_tests/http_client.rs
// version: 3 // version: 4
fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings { fn endpoint(enabled: bool, url_text: &str) -> crate::HttpEndpointSettings {
let url = crate::HttpEndpointUrl::parse(url_text).expect("test endpoint URL must parse"); let url = crate::HttpEndpointUrl::parse(url_text).expect("test endpoint URL must parse");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/executor.rs // file: crates/ksp-onchain-transport-lib/unit_tests/http_executor.rs
// version: 2 // version: 3
fn pool_for_url(url: &str, request_timeout: std::time::Duration, max_retries: u32) -> crate::HttpTransportPool { fn pool_for_url(url: &str, request_timeout: std::time::Duration, max_retries: u32) -> crate::HttpTransportPool {
let role = crate::HttpEndpointRoleSettings::new( let role = crate::HttpEndpointRoleSettings::new(

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/pool.rs // file: crates/ksp-onchain-transport-lib/unit_tests/http_pool.rs
// version: 4 // version: 5
fn role(name: &str, priority: u32, request_kinds: std::vec::Vec<crate::HttpRequestKind>) -> crate::HttpEndpointRoleSettings { fn role(name: &str, priority: u32, request_kinds: std::vec::Vec<crate::HttpRequestKind>) -> crate::HttpEndpointRoleSettings {
return crate::HttpEndpointRoleSettings::new( return crate::HttpEndpointRoleSettings::new(

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/resilience.rs // file: crates/ksp-onchain-transport-lib/unit_tests/http_resilience.rs
// version: 2 // version: 3
fn non_zero(value: u32) -> std::num::NonZeroU32 { fn non_zero(value: u32) -> std::num::NonZeroU32 {
return std::num::NonZeroU32::new(value).expect("test limit must be non-zero"); return std::num::NonZeroU32::new(value).expect("test limit must be non-zero");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/unit_tests/settings.rs // file: crates/ksp-onchain-transport-lib/unit_tests/http_settings.rs
// version: 2 // version: 3
fn non_zero(value: u32) -> std::num::NonZeroU32 { fn non_zero(value: u32) -> std::num::NonZeroU32 {
return std::num::NonZeroU32::new(value).expect("test non-zero value must remain non-zero"); return std::num::NonZeroU32::new(value).expect("test non-zero value must remain non-zero");

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-wallet-lib/Cargo.toml # file: crates/ksp-wallet-lib/Cargo.toml
# version: 6 # version: 7
[package] [package]
name = "ksp-wallet-lib" name = "ksp-wallet-lib"
@@ -9,12 +9,12 @@ repository.workspace = true
[dependencies] [dependencies]
argon2 = { workspace = true, features = ["alloc", "zeroize"] } argon2 = { workspace = true, features = ["alloc", "zeroize"] }
base64.workspace = true
chacha20poly1305 = { workspace = true, features = ["alloc", "zeroize"] } chacha20poly1305 = { workspace = true, features = ["alloc", "zeroize"] }
ed25519-dalek = { workspace = true, features = ["signature", "zeroize"] } ed25519-dalek = { workspace = true, features = ["signature", "zeroize"] }
getrandom.workspace = true getrandom.workspace = true
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" }
base64.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
solana-keypair.workspace = true solana-keypair.workspace = true

View File

@@ -0,0 +1,139 @@
<!-- file: deltas/0.2.9/pre.001-fix.001.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.001-fix.001` — sizing/session + architecture provider Yellowstone
## 1. Base requise
```text
0.2.9-pre.001
workspace.package.version = 0.2.9-pre.1
```
Ce correctif est **documentaire uniquement**. Conformément à `VER-ID-008`, il ne modifie pas `workspace.package.version`.
## 2. Motif du correctif
La première livraison `pre.001` contenait bien une mention générale du budget `1520 minutes`, mais elle ne rendait pas assez visible le contrat normatif complet de `PROMPT_STRUCTURE.md` :
```text
une prerelease estimée > 1520 min doit être scindée
une release concrète doit rester ouvrable et clôturable dans une seule session de chat
```
Elle traitait aussi PublicNode et OrbitFlare principalement comme des environnements de smoke derrière un seul backend Yellowstone provider-neutral. Cette représentation ne reprenait pas suffisamment la séparation déjà validée pour WebSocket entre moteur partagé, protocole standard et adaptation provider.
## 3. Décisions corrigées
### 3.1 Dimensionnement
Le forecast `0.2.9` affiche désormais explicitement pour **chaque prerelease** :
```text
budget nominal = 1520 minutes maximum de travail effectif
```
et le gate global :
```text
0.2.9 complète <= une session de chat
```
Si une tranche dépasse ce budget ou si la clôture de la release dans la session devient incertaine, le scope est scindé avant implémentation lourde supplémentaire.
Le forecast est recalibré à **11 prereleases prévues** avant `rel.001`, soit environ `165220 minutes` de travail effectif nominal hors temps d'attente des commandes.
### 3.2 Architecture Yellowstone
`0.2.9` distingue désormais trois niveaux dans `ksp-onchain-transport-lib` :
```text
N1 moteur client Yellowstone gRPC partagé
N2 façade Solana Yellowstone standard
N3 première intégration provider PublicNode / Allnodes-backed
```
Le moteur reste Yellowstone-specific ; KSP ne crée pas une abstraction gRPC universelle ni un serveur/plugin Geyser.
La règle provider est :
```text
aucun second actor/channel/stream par provider
aucune façade spécialisée vide
façade/type provider public uniquement si une différence réelle de contrat le justifie
```
PublicNode devient donc la **première intégration concrète** de `0.2.9`, avec Mainnet et Testnet comme cibles live opt-in. Il réutilise le moteur et le wire standard.
### 3.3 Releases providers suivantes
La séquence fonctionnelle est recalibrée :
```text
0.2.9 moteur Yellowstone + Solana standard + PublicNode
0.2.10 OrbitFlare Yellowstone gRPC
0.2.11 Helius LaserStream gRPC
0.2.12 eRPC Yellowstone gRPC, conditionnel après réaudit accès/capabilities
0.2.13 off-chain price transport
0.2.14 Price Desk + intégration prix Wallet Desk
0.2.15 interface/wire foundation
0.2.16 program-api foundation
```
Chaque future release provider réutilise `0.2.9`. Si le gate d'un provider montre qu'il ne diffère que par une URL interchangeable, il ne doit pas provoquer une façade artificielle ; son scope peut être réduit/fusionné avant implémentation.
### 3.4 Rust/toolchain
Les numéros de versions Rust upstream/MSRV ne sont plus suivis comme information de planification ordinaire.
KSP utilise la **Rust stable courante de l'opérateur**. Le seul gate utile est que les dépendances sélectionnées compilent avec cette stable. Un minimum Rust d'une dépendance n'est documenté que s'il devient un blocage réel.
## 4. Fichiers modifiés
```text
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
ROADMAP.md
```
## 5. Fichier ajouté
```text
deltas/0.2.9/pre.001-fix.001.md
```
## 6. Fichiers non modifiés intentionnellement
```text
Cargo.toml
prompts/014-V0_2_9_START_PROMPT.md
crates/**
config/**
.env.example
```
Le prompt `014` reste l'autorité historique ayant ouvert `pre.001`; le présent fix trace le recalibrage résultant du gate et devient l'état de planification courant.
## 7. Validation
À exécuter pour le correctif documentaire :
```bash
python3 scripts/audit_rust_workspace_rules.py
```
Aucun `cargo check/clippy/test` supplémentaire n'est requis par le contenu du fix puisqu'aucun artefact code/build/runtime/config n'est modifié ; la baseline technique reste celle enregistrée par `pre.001`.
## 8. État de sortie
```text
forecast visible et conforme à PROMPT_STRUCTURE.md
1520 min max par prerelease explicités
release <= une session explicitée
moteur Yellowstone séparé de la façade standard
PublicNode promu de smoke à première intégration provider
OrbitFlare/Helius/eRPC reportés dans des releases dédiées
séquence prix/interface/program décalée en conséquence
aucun changement runtime/dependency
```

View File

@@ -0,0 +1,154 @@
<!-- file: deltas/0.2.9/pre.001-fix.002.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.001-fix.002` — provider divergence + waitlist Yellowstone
## 1. Base requise
```text
0.2.9-pre.001-fix.001
workspace.package.version = 0.2.9-pre.1
```
Ce correctif est **documentaire uniquement**. Conformément au workflow de version, il ne modifie pas `workspace.package.version`.
## 2. Motif du correctif
Le cadrage `pre.001-fix.001` avait correctement séparé :
```text
N1 moteur Yellowstone partagé
N2 façade Solana Yellowstone standard
N3 intégration provider
```
mais la formulation « sans duplication du moteur ni du wire standard » pouvait être comprise comme une hypothèse d'équivalence wire/capabilities entre Yellowstone upstream et tous les providers.
Cette hypothèse n'est pas acceptable : un provider peut implémenter seulement un sous-ensemble du standard, imposer des limites/auth/lifecycle différents ou proposer des extensions propres.
Le correctif ferme aussi la liste des providers à implémenter à court terme :
```text
0.2.9 PublicNode
0.2.10 OrbitFlare
0.2.11 Helius LaserStream gRPC
```
Les autres providers ne reçoivent plus de numéro de release réservé.
## 3. Décisions corrigées
### 3.1 Règle N1 / N2 / N3
```text
N1 = moteur client Yellowstone unique
N2 = contrat Solana Yellowstone standard KSP
N3 = adaptation provider
```
Invariant N1 :
```text
aucun second actor/channel/stream moteur par provider
```
Règle N2/N3 :
```text
réutiliser N2 lorsqu'une capacité provider est réellement compatible
restreindre explicitement N2 lorsqu'une capacité standard est absente/non supportée
ajouter une extension typed N3 lorsqu'un provider étend le protocole/wire
porter en N3 les différences auth/metadata/compression/keepalive/replay/from_slot/limites/lifecycle
ne jamais présumer une équivalence provider/standard sans preuve
```
Une divergence réelle n'est donc ni dupliquée arbitrairement ni masquée derrière le standard.
### 3.2 Providers actuellement planifiés
```text
0.2.9 moteur Yellowstone + standard Solana + PublicNode
0.2.10 OrbitFlare Yellowstone gRPC
0.2.11 Helius LaserStream gRPC
```
Chaque release provider commence par un delta audit avec Yellowstone upstream courant et ne matérialise que ses différences réelles.
### 3.3 Providers en attente
Les providers suivants sont retirés de la séquence numérotée :
```text
TODO eRPC
TODO Triton
TODO Alchemy
TODO QuickNode
TODO Chainstack
IDEAS Tatum
IDEAS Shyft
IDEAS Solinfra
IDEAS NodeFlare
IDEAS autres providers
```
Aucun Config profile, type public, dépendance, smoke ou forecast n'est préparé pour eux tant qu'une décision explicite d'implémentation n'est pas prise.
### 3.4 Séquence fonctionnelle libérée
La suppression de la réservation `0.2.12 eRPC` ramène la suite active à :
```text
0.2.12 off-chain price transport
0.2.13 Price Desk + intégration prix Wallet Desk
0.2.14 interface/wire foundation
0.2.15 program-api foundation
```
## 4. Fichiers modifiés
```text
ROADMAP.md
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
```
## 5. Fichier ajouté
```text
deltas/0.2.9/pre.001-fix.002.md
```
## 6. Fichiers non modifiés intentionnellement
```text
Cargo.toml
prompts/014-V0_2_9_START_PROMPT.md
crates/**
config/**
.env.example
```
Le prompt `014` reste l'autorité historique d'ouverture ; le plan et les deltas successifs enregistrent les décisions prises pendant le gate.
## 7. Validation
Correctif documentaire :
```bash
python3 scripts/audit_rust_workspace_rules.py
```
Aucun `cargo check/clippy/test` supplémentaire n'est requis par ce correctif tant qu'aucun artefact code/build/runtime/config n'est modifié.
## 8. État de sortie
```text
N1 moteur unique explicite
N2 standard non présumé universel
N3 peut réutiliser/restreindre/étendre
PublicNode/OrbitFlare/Helius seuls providers planifiés
reste des providers en TODO/IDEAS non numérotés
off-chain price revient en 0.2.12
aucun changement runtime/dependency
```

377
deltas/0.2.9/pre.001.md Normal file
View File

@@ -0,0 +1,377 @@
<!-- file: deltas/0.2.9/pre.001.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.001` — audit/sizing Yellowstone gRPC standard
## 1. Base requise et vérifiée
Archive autoritaire fournie :
```text
khadhroony-solana-project-v0.2.8-full-from-gitea.zip
```
État vérifié :
```text
workspace.package.version = 0.2.8
deltas/0.2.8/rel.001.md présent
prompts/014-V0_2_9_START_PROMPT.md présent
prompt fourni = byte-identique au prompt embarqué
metadata .git absente de l'archive
```
Cette livraison ouvre :
```text
workspace.package.version = 0.2.9-pre.1
commit attendu = v0.2.9-pre.001
aucun tag prerelease
```
## 2. Objet
`pre.001` exécute le gate obligatoire :
```text
lecture base/règles/architecture
baseline stable
réaudit Yellowstone actuel
matrice service/proto
licences/dependencies/MSRV
architecture provider-neutral
threat model/resources/backpressure/replay
sizing et forecast
récensement des providers gRPC gratuits durables
```
Aucune implémentation gRPC lourde n'est ajoutée.
## 3. Baseline opérateur acquise
Preuve fournie sur `v0.2.8` :
```text
cargo fmt --all OK
python3 scripts/audit_rust_workspace_rules.py OK / clean
cargo check --workspace OK
cargo clippy --workspace --all-targets OK
cargo test --workspace OK
cargo tree -p ksp-onchain-transport-lib fourni
cargo tree --duplicates fourni
```
Transport pendant le workspace test :
```text
335 unit
41 public_api
34 release_completeness
4 doctests
```
Les live smokes restent opt-in/ignored par défaut.
## 4. Réaudit Yellowstone du 2026-08-23
Divergence importante par rapport au snapshot du start prompt :
```text
prompt snapshot release = v14.2.2+solana.4.1.0
release GitHub courante = v15.1.2+solana.4.2.0, publiée 2026-08-18, Rust 1.96.1
client crate courant = yellowstone-grpc-client 13.3.0
proto crate courant = yellowstone-grpc-proto 12.6.0
```
Service Geyser courant inventorié :
```text
Subscribe IN
SubscribeDeshred OUT — Triton extension / pre-execution
SubscribeReplayInfo IN
Ping IN
GetLatestBlockhash IN
GetBlockHeight IN
GetSlot IN
IsBlockhashValid IN
GetVersion IN
```
`SubscribeRequest` standard retenu :
```text
accounts
slots
transactions
transactions_status
blocks
blocks_meta
entry
commitment
accounts_data_slice
ping
from_slot
```
Capacités nouvelles/actuelles retenues car présentes dans le proto publié standard :
```text
CuckooFilter account/block/transaction include
TokenAccountExpansionControlFlag ALL/BALANCE_CHANGED
lamports eq/ne/lt/gt
slot interslot updates
```
`SubscribeUpdate` :
```text
account
slot
transaction
transaction_status
block
ping
pong
block_meta
entry
```
## 5. Licence et dépendances
Upstream :
```text
repository default = AGPL-3.0-only
client subtree = Apache-2.0
proto subtree = Apache-2.0
```
Stratégies comparées :
```text
A client upstream + proto -> rejetée comme default
B proto publié + client KSP autour de Tonic -> RETENUE
C proto vendored KSP -> fallback seulement
```
Raisons de B :
```text
pas de copie proto
wire officiel disponible
licence Apache compatible MIT
reconnect/backpressure/redaction KSP-owned
upstream raw types cachés
stack tonic 0.14 cohérente avec HTTP/2 moderne existant
```
Aucune dependency n'est ajoutée dans `pre.001`. `pre.002` doit matérialiser les features minimales et produire les graphes Cargo.
## 6. Replay/continuity
L'upstream a corrigé en juillet 2026 un cas où `from_slot` pour blocks pouvait être accepté puis reprendre live avec un **state gap** ; l'autoreconnect upstream a aussi été renforcé pour traiter l'equivocation entre nodes.
KSP ferme donc :
```text
reconnect/resubscribe = oui, borné et KSP-owned
from_slot = oui
ReplayInfo = oui
exactly-once = non garanti
lossless = non garanti
gaps/duplicates = observables
node divergence = couverture explicite seulement si prouvée
```
## 7. Config / architecture
Décisions :
```text
backend gRPC distinct de HTTP/WS
pas de WsProtocolKind gRPC
pas de raw Tonic client public
Transport owns Yellowstone
Config -> Transport seulement
metadata publique et metadata sensible séparées
Transport ne lit aucun env
```
Le schéma V2 actuel étant fermé, l'ajout gRPC cible :
```text
format_version = 3
globals.grpc_defaults
profiles[].grpc_endpoints[]
metadata[]
secret_metadata[]
```
V1 et V2 restent backward-readable.
## 8. Providers gRPC gratuits durables
### PublicNode / Allnodes-backed — priorité 1
Confirmé officiellement :
```text
Solana Mainnet Yellowstone GRPC
Solana Testnet GRPC
Mainnet endpoint = solana-yellowstone-grpc.publicnode.com:443
service annoncé free/public
```
Décision : **dans `0.2.9`**, mais pas sous forme d'adapter public spécifique. PublicNode sert de premier smoke live standard provider-neutral.
Le hostname Testnet exact n'est pas figé avant vérification officielle/live.
### OrbitFlare — priorité 2
Plan Free officiel :
```text
$0/mo
10 RPS
1 TPS
gRPC Devnet only
Unlimited credit limits
```
Décision : **dans `0.2.9` comme second smoke live**, via generic secret metadata Config -> Transport. Aucun type `OrbitFlareGrpc*` public.
### Tatum — tertiaire
```text
Mainnet Yellowstone gRPC
Free plan durable
3 RPS
100K lifetime credits
5 subscriptions
```
Décision : opérateur optionnel, pas gate durable principal à cause du quota lifetime.
### Vérifiés non gratuits / non applicables
```text
Helius Free sans gRPC
Shyft Free = No gRPC Access
Alchemy PAYG/Enterprise requis
QuickNode Scale/Business ou add-on payant
Chainstack add-on Yellowstone payant, Growth+
ERPC Geyser payant ; trial seulement 1 jour
NodeFlare plan Yellowstone mensuel payant
Bitquery CoreCast custom, pas Yellowstone standard ; stream free non garanti
```
Candidat à revalider :
```text
Solinfra = free tier + Yellowstone annoncés publiquement, mais accès gRPC inclus dans Free non confirmé
```
## 9. Threat model
Le plan ferme explicitement :
```text
metadata/URI credential leak
Tonic Status arbitraire
oversized messages
stream flood/backpressure
filter explosion / collisions de noms
unknown enum/oneof
server/client half-close
reconnect loop
node divergence
late updates après mutation
from_slot gaps/duplicates
TLS failures
unary timeout
```
## 10. Forecast recalibré
```text
pre.001 DONE audit/sizing/provider/deps/license
pre.002 deps + settings/errors/facade + cargo tree
pre.003 TLS/metadata + local fixture + 7 unary
pre.004 Subscribe common + Cuckoo/token/from_slot/bounds
pre.005 Accounts + Slots
pre.006 Transactions + transaction_status
pre.007 Blocks + block_meta + entry
pre.008 bidi lifecycle/backpressure/half-close/shutdown
pre.009 reconnect/replay/gaps/duplicates/equivocation
pre.010 Config V3
pre.011 PublicNode/OrbitFlare live + compliance + final cargo graphs
pre.012 docs/workspace final + prompt 0.2.10
rel.001 stable
```
Chaque tranche vise ~1520 minutes ; `pre.012` n'est pas une deadline.
Critères de split : conflit MSRV/stack réseau, explosion DTO transaction/block, nouvelle évolution proto matérielle, replay trop large, ou comportement provider-specific contaminant le contrat standard.
## 11. Fichiers ajoutés
```text
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
deltas/0.2.9/pre.001.md
```
## 12. Fichiers modifiés
```text
Cargo.toml
docs/000-README.md
docs/plans/000-README.md
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
docs/validation/000-README.md
```
## 13. Fichiers volontairement inchangés
```text
ROADMAP.md
CHANGELOG.md
.env.example
config/**
crates/**
docs/architecture/**
crates/ksp-onchain-transport-lib/README.md
crates/ksp-onchain-transport-lib/USAGE.md
```
Aucun gRPC runtime/config/dependency n'est introduit avant le gate positif de cette tranche.
## 14. Validations du sandbox de préparation
Le sandbox a pu exécuter :
```text
inspection archive/version/rel/prompt OK
comparaison prompt fourni / prompt archive OK
lecture règles/architecture/plans/code/config OK
réaudit upstream Yellowstone/provider/dependencies OK
python3 scripts/audit_rust_workspace_rules.py OK / clean après overlay
```
Le sandbox ne dispose pas de Cargo ; aucun nouveau `cargo check/clippy/test/tree` n'est déclaré réussi localement après le changement de version/documentation.
## 15. Validation opérateur attendue avant commit
Comme `pre.001` ne modifie aucun Rust/dependency/config runtime, exécuter au minimum :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test --workspace
cargo tree -p ksp-onchain-transport-lib
cargo tree -p ksp-onchain-transport-lib --duplicates
```
Attendu : aucune nouvelle dependency gRPC avant `pre.002`.

View File

@@ -0,0 +1,105 @@
<!-- file: deltas/0.2.9/pre.002-fix.001.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.002-fix.001` — Clippy + runtime Tokio du channel lazy
## 1. Déclencheur
Le premier gate opérateur de `pre.002` a fourni les preuves suivantes :
```text
cargo fmt --all PASS
audit_rust_workspace_rules.py PASS / clean
cargo check --workspace PASS avec warning channel dead_code
cargo clippy --workspace --all-targets FAIL implicit_return dans grpc_settings test
cargo test -p ksp-onchain-transport-lib FAIL 344/345 : connect_lazy hors runtime Tokio
cargo test -p ksp-core-lib --test workspace_dependencies PASS 3/3
cargo tree Transport / duplicates fournis et inspectés
```
Le test gRPC a révélé que `tonic::transport::Endpoint::connect_lazy()` nécessite un reactor Tokio actif et peut paniquer s'il est appelé depuis un contexte synchrone sans runtime. Ce comportement doit être borné dans la surface KSP au lieu d'être laissé comme précondition implicite Tonic.
## 2. Corrections
### Clippy
La closure qui génère l'inventaire d'endpoints excessif dans `unit_tests/grpc_settings.rs` utilise désormais un `return` explicite, conformément à `clippy::implicit_return = deny`.
### Runtime Tokio
`YellowstoneGrpcChannel::prepare()` vérifie maintenant :
```text
tokio::runtime::Handle::try_current()
```
avant l'appel à `connect_lazy()`. En l'absence de runtime actif, KSP retourne :
```text
onchain_transport/grpc_channel_failed
```
avec uniquement `endpoint_name` comme contexte sûr. L'URL et le message interne Tonic ne sont pas réémis.
Le test positif devient un `#[tokio::test(flavor = "current_thread")]`. Un nouveau test synchrone prouve que l'absence de runtime est retournée comme erreur sans exposer l'URL canari.
### Champ Tonic
Le channel **reste un champ de production**. Il n'est pas placé sous `#[cfg(test)]`, car `pre.002` matérialise réellement la ressource physique N1 qui sera consommée en `pre.003`.
Le champ privé est temporairement nommé :
```text
_channel: tonic::transport::Channel
```
pour exprimer qu'il est intentionnellement conservé mais pas encore consommé par une opération de production dans cette tranche. Cela supprime le warning `dead_code` sans modifier la shape production/test.
## 3. Graphe Cargo observé
Le gate opérateur confirme :
```text
tonic 0.14.6
yellowstone-grpc-proto 12.6.0
prost/prost-types 0.14.4
solana-pubkey 4.3.0 unifié
http 1.5.0
hyper 1.11.0
hyper-util 0.1.20
tower 0.5.3
bytes 1.12.1
```
Les occurrences Prost 0.14.4 dans `--duplicates` viennent des unités runtime/build (`prost-build`, `tonic-prost-build`) de la même version ; aucune seconde version concurrente n'est introduite.
## 4. Version
Le fix touche le code/runtime ; la version workspace devient :
```text
0.2.9-pre.2.fix.1
```
Commit attendu après validation :
```text
v0.2.9-pre.002-fix.001
```
## 5. Validation requise
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
cargo tree -p ksp-onchain-transport-lib
cargo tree -p ksp-onchain-transport-lib --duplicates
cargo tree --duplicates
```
Le passage à `pre.003` est interdit tant que ces gates ne sont pas verts ou qu'un nouvel écart n'est pas traité par un fix supplémentaire.

View File

@@ -0,0 +1,68 @@
<!-- file: deltas/0.2.9/pre.002-fix.002.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.002-fix.002` — canari public API sous runtime Tokio
## 1. Déclencheur
Le second gate opérateur, après `pre.002-fix.001`, confirme :
```text
cargo fmt --all PASS
audit_rust_workspace_rules.py PASS / clean
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-onchain-transport-lib unit PASS 346/346
public_api FAIL 41/42
dependency canary Core PASS 3/3
```
Le seul échec restant est le canari :
```text
public_v0_2_9_pre_002_yellowstone_engine_settings_and_lazy_channel_are_available_from_crate_root
```
Il appelle `YellowstoneGrpcChannel::prepare()` depuis un `#[test]` synchrone. Depuis `fix.001`, `prepare()` exige volontairement un runtime Tokio actif avant `tonic::transport::Endpoint::connect_lazy()` ; le canari public API n'avait pas encore été aligné sur cette précondition KSP explicite.
## 2. Correction
Le test d'intégration devient :
```text
#[tokio::test(flavor = "current_thread")]
async fn public_v0_2_9_pre_002_...
```
Aucun code N1 de production n'est modifié. En particulier :
```text
YellowstoneGrpcChannel::prepare() inchangé
_channel de production conservé
aucun cfg(test) ajouté au channel
aucune dépendance/feature Cargo ajoutée
aucun TLS/metadata/unary anticipé
```
## 3. Version
Le correctif touche le code de test et fait partie du signal technique de la prerelease :
```text
workspace.package.version = 0.2.9-pre.2.fix.2
commit attendu = v0.2.9-pre.002-fix.002
```
## 4. Validation requise
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Les graphes Cargo n'ont pas changé avec `fix.002`; ils ne nécessitent pas une nouvelle inspection structurelle, mais peuvent être relancés si l'opérateur veut conserver une preuve complète du gate final `pre.002`.

259
deltas/0.2.9/pre.002.md Normal file
View File

@@ -0,0 +1,259 @@
<!-- file: deltas/0.2.9/pre.002.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.002` — moteur Yellowstone : dépendances + settings/errors + channel minimal
## 1. Objet
Cette tranche matérialise uniquement la première fondation **N1 moteur Yellowstone gRPC** validée par `pre.001` :
```text
proto Yellowstone publié
+ dépendances Tonic minimales
+ settings runtime bornés/redacted
+ error code initial gRPC
+ channel Tonic lazy privé
```
Elle ne commence pas encore N2 fonctionnel : aucun unary Yellowstone, aucune metadata/auth, aucune fixture serveur gRPC, aucun `Subscribe`, aucun lifecycle bidi et aucune intégration PublicNode ne sont introduits ici.
Version workspace :
```text
0.2.9-pre.2
```
Commit attendu après validation opérateur :
```text
v0.2.9-pre.002
```
## 2. Dépendances matérialisées
Stratégie B conservée, mais avec le feature set le plus étroit utile à cette tranche :
```text
workspace root:
tonic = ^0.14, default-features = false
yellowstone-grpc-proto = ^12.6, default-features = false
ksp-onchain-transport-lib:
tonic workspace = true, features = ["channel"]
yellowstone-grpc-proto workspace = true, aucune feature locale
```
Explicitement absents :
```text
yellowstone-grpc-client
tonic-prost direct KSP
prost direct KSP
prost-types direct KSP
tokio-stream nouveau
feature tonic du proto
feature tonic-compression du proto
TLS Tonic
```
La crate publiée `yellowstone-grpc-proto` fournit les messages Protobuf générés utiles sans que KSP copie les `.proto`. Son build utilise son outillage `protoc` vendored ; aucun `protoc` système ni `build.rs` KSP n'est ajouté.
Le feature optionnel `tonic` de la crate proto n'est pas activé en `pre.002` : le moteur ne consomme encore aucun client RPC généré. `pre.003` devra justifier explicitement toute feature/dependency supplémentaire nécessaire aux unary, TLS et metadata.
## 3. Settings Yellowstone N1
Ajout de `grpc_settings.rs` avec les contrats publics crate-root :
```text
YellowstoneGrpcEndpointUrl
YellowstoneGrpcProviderName
YellowstoneGrpcClusterName
YellowstoneGrpcReconnectSettings
YellowstoneGrpcSessionSettings
YellowstoneGrpcEndpointSettings
YellowstoneGrpcTransportSettings
```
Principes :
```text
Transport-owned
aucun Config/env
aucun WsProtocolKind/WsEndpointSettings
URL http/https distincte de HTTP/WS et Debug redacted
provider/cluster = descripteurs ouverts, pas des enums commerciaux fermés
URL/descripteurs/nombre dendpoints bornés
connect/unary/close timeouts bornés
reconnect attempts/backoff bornés
request/update channel capacities bornées
max inbound/outbound message sizes bornées
au moins un endpoint configuré et enabled
endpoint names uniques
```
Les defaults servent uniquement de runtime defaults N1 ; ils ne deviennent pas des quotas provider.
## 4. Channel minimal
Ajout de `YellowstoneGrpcChannel` dans `grpc_channel.rs`.
`prepare()` :
```text
valide les settings avant I/O
refuse un endpoint disabled
construit tonic::transport::Endpoint depuis l'URL sensible
applique connect timeout / unary timeout / request buffer
utilise connect_lazy()
ne réalise aucune connexion réseau
ne configure encore ni TLS ni metadata
ne recopie pas les erreurs URI Tonic arbitraires dans KspError
```
Le `tonic::transport::Channel` réel est un champ privé et n'a aucun getter public. `Debug` n'expose ni URL, ni channel brut, ni credential potentiel.
Cette surface ne constitue pas encore `YellowstoneGrpcSession` : elle prépare seulement la ressource physique minimale qui sera utilisée par `pre.003+`.
## 5. Erreurs et observabilité
Nouveau code :
```text
onchain_transport/grpc_channel_failed
```
Les erreurs de préparation conservent uniquement des contexts allowlistés sûrs (`endpoint_name`) et n'injectent pas le message d'erreur Tonic/URI dans le contrat KSP.
Les logs passent exclusivement par `ksp-logging-lib` et n'émettent que :
```text
endpoint_name
provider
cluster
bornes numériques non sensibles
```
Aucune URL n'est loggée.
## 6. Canaries ajoutés
Unit tests Transport :
```text
gRPC URL http/https + Debug redacted
gRPC URL non-http ou surdimensionnée rejetée sans secret echo
descripteurs et endpoint inventory plafonnés
defaults session bornés
zero/excessive/reversed runtime settings rejetés
endpoint inventory unique/enabled
channel lazy préparé sans réseau
channel Debug sans URL/secret
endpoint disabled rejeté avant I/O
raw proto disponible uniquement côté interne/test
```
Public API :
```text
settings N1 constructibles depuis crate root
YellowstoneGrpcChannel constructible sans Config
nouvel error code public KSP
aucun raw client Tonic requis par le consumer
```
Release completeness / workspace dependency canaries :
```text
dépendances root/member exactes
yellowstone-grpc-client absent
Transport -X-> Config
aucun couplage Ws*
aucun pub use tonic/yellowstone_grpc_proto
channel = from_shared + connect_lazy
TLS/metadata explicitement absents de pre.002
```
## 7. Documentation
Les versions opérateur alignées de :
```text
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
```
sont conservées comme base de mise en forme et avancées pour enregistrer :
```text
feature set réellement matérialisé
séparation stricte pre.002 / pre.003
surface N1 réellement ouverte
état des canaries
Cargo gates encore opérateur
```
Le forecast reste inchangé en nombre de tranches : `pre.003` porte toujours TLS/metadata + fixture locale + 7 unary.
## 8. Validation exécutée dans l'environnement de préparation
Exécuté réellement :
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
```
L'environnement de préparation ne possède pas Cargo/Rust. Les commandes Cargo ci-dessous ne sont donc **pas déclarées réussies**.
## 9. Gate opérateur requis avant commit
Après application du delta :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
cargo tree -p ksp-onchain-transport-lib
cargo tree -p ksp-onchain-transport-lib --duplicates
cargo tree --duplicates
```
Inspecter particulièrement :
```text
yellowstone-grpc-proto 12.6.x réellement résolu
tonic 0.14.x réellement résolu
prost / prost-types / tonic-prost transitifs
bytes / http / hyper / hyper-util / tower
rustls / tokio-rustls
solana-pubkey unifié ou doublons justifiés
aucune arrivée de yellowstone-grpc-client
features Tonic réellement activées
```
Si la compilation démontre que `yellowstone-grpc-proto` sans feature `tonic` ou Tonic `channel` ne suffit pas à la tranche actuelle, ne pas élargir silencieusement : produire un `pre.002-fix.001` avec la dépendance minimale démontrée.
## 10. Fichiers ajoutés/modifiés
```text
Cargo.toml
crates/ksp-core-lib/tests/workspace_dependencies.rs
crates/ksp-onchain-transport-lib/Cargo.toml
crates/ksp-onchain-transport-lib/src/error.rs
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
crates/ksp-onchain-transport-lib/src/grpc_settings.rs
crates/ksp-onchain-transport-lib/src/lib.rs
crates/ksp-onchain-transport-lib/tests/public_api.rs
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
deltas/0.2.9/pre.002.md
```

View File

@@ -0,0 +1,82 @@
<!-- file: deltas/0.2.9/pre.003-fix.001.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.003-fix.001` — conformité Clippy de la fixture unary
## 1. Déclencheur
Le premier gate opérateur `pre.003` confirme :
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS / clean
cargo check --workspace PASS
cargo clippy --workspace --all-targets FAIL
11 x clippy::implicit_return dans unit_tests/grpc_unary.rs
cargo test -p ksp-onchain-transport-lib PASS
unit 354/354
public_api 43/43
release_completeness 36/36
doctests 4/4
cargo test -p ksp-core-lib --test workspace_dependencies
PASS 3/3
cargo test --workspace PASS
```
Le problème est donc limité à la conformité Clippy de la fixture locale ; aucune défaillance fonctionnelle TLS/metadata/unary n'est observée.
## 2. Diagnostic
Neuf diagnostics pointent les méthodes de l'implémentation `Geyser` annotée `#[tonic::async_trait]`. Les corps source ont déjà leurs retours explicites ; le lint vise les wrappers/tails async issus de l'expansion de macro et propose un `return async fn ...` non applicable au code Rust source.
Deux diagnostics supplémentaires concernent les closures `and_then` utilisées pour lire les metadata de fixture ; ces retours sont directement sous contrôle KSP.
## 3. Correction
```text
impl FixtureGeyser :
allow(clippy::implicit_return) strictement local et commenté
aucune relaxation au niveau crate/workspace
closures metadata :
return explicite conforme à la politique workspace
```
Aucun changement de production :
```text
YellowstoneGrpcChannel inchangé
SolanaYellowstoneGrpcUnaryClient inchangé
TLS/metadata/settings inchangés
7 unary inchangés
aucune dépendance/feature Cargo ajoutée ou retirée
Subscribe/PublicNode/Config V3 toujours hors tranche
```
## 4. Version
```text
workspace.package.version = 0.2.9-pre.3.fix.1
commit attendu = v0.2.9-pre.003-fix.001
```
## 5. Validation requise
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Les dépendances et features ne changent pas avec ce fix. Les graphes Cargo demandés par `pre.003` restent néanmoins à fournir/inspecter si ce n'est pas déjà fait :
```bash
cargo tree -p ksp-onchain-transport-lib
cargo tree -p ksp-onchain-transport-lib -e features
cargo tree -p ksp-onchain-transport-lib --duplicates
cargo tree --duplicates
```

294
deltas/0.2.9/pre.003.md Normal file
View File

@@ -0,0 +1,294 @@
<!-- file: deltas/0.2.9/pre.003.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.003` — TLS + metadata générique + fixture locale + 7 unary Yellowstone
## 1. Objet
Cette tranche poursuit le moteur Yellowstone après la fermeture opérateur complète de `pre.002-fix.002`. Elle matérialise exactement le forecast prévu :
```text
N1 : connexion HTTP/2 réelle + TLS WebPKI + metadata ASCII publique/secrète redacted
N2 : sept unary RPCs Yellowstone standard typed derrière une façade KSP
tests : fixture Geyser locale couvrant le wire réel, metadata, Status hostile et timeout
```
Restent hors tranche :
```text
Subscribe / streaming bidi
SubscribeDeshred
PublicNode / N3 provider
Config V3
reconnect/resubscribe/replay lifecycle complet
```
Version workspace :
```text
0.2.9-pre.3
```
Commit attendu après validation opérateur :
```text
v0.2.9-pre.003
```
## 2. Preuve héritée — fermeture de `pre.002`
Le gate opérateur final fourni pour `0.2.9-pre.2.fix.2` est intégralement vert :
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS / clean
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test -p ksp-onchain-transport-lib PASS
unit 346/346
public_api 42/42
release_completeness 35/35
doctests 4/4
cargo test -p ksp-core-lib --test workspace_dependencies
PASS 3/3
cargo test --workspace PASS
```
`pre.003` part donc d'une base technique fermée, sans dette reportée de `pre.002`.
## 3. Dépendances et features
La stratégie B reste inchangée : le runtime KSP utilise les messages publiés `yellowstone-grpc-proto` et un client KSP autour de Tonic ; `yellowstone-grpc-client` reste absent.
Ajouts workspace :
```text
http = ^1.5, default-features = false
tonic-prost = ^0.14, default-features = false
```
Features Transport runtime :
```text
tonic = channel + tls-aws-lc + tls-webpki-roots
tonic-prost = runtime ProstCodec
yellowstone-grpc-proto = aucune feature locale
```
Features uniquement dev/test :
```text
tonic = codegen + server
yellowstone-grpc-proto = tonic
```
La feature `tonic` de la crate proto est donc utilisée uniquement pour générer `GeyserServer` dans la fixture locale. Le runtime ne consomme ni le client généré upstream ni sa sémantique de lifecycle.
Toujours absents comme dépendances KSP directes :
```text
yellowstone-grpc-client
prost
prost-types
```
Le choix `tls-aws-lc` reste aligné avec la stack Rustls déjà présente via Reqwest ; le gate Cargo opérateur doit confirmer l'absence de duplication injustifiée.
## 4. N1 — TLS, connexion réelle et metadata
`YellowstoneGrpcChannel` possède maintenant deux chemins explicites :
```text
prepare() -> connect_lazy(), aucun socket
connect() -> établissement réel du channel HTTP/2
```
Pour un endpoint `https://`, le builder applique :
```text
ClientTlsConfig
WebPKI roots
TLS handshake timeout borné
connect timeout borné
request buffer borné
```
Les erreurs d'établissement restent projetées vers `onchain_transport/grpc_channel_failed` sans recopier URI ou message arbitraire Tonic.
Ajout de `YellowstoneGrpcMetadataEntry` :
```text
ASCII uniquement
clé <= 128 bytes
valeur <= 8 KiB
<= 64 entrées par endpoint
clés grpc-* refusées
metadata binaire *-bin hors contrat pre.003
public et secret séparés explicitement
valeurs toujours absentes de Debug
secret -> MetadataValue::set_sensitive(true)
```
Transport ne connaît aucun nom `KSP_SECRET_*`, ne lit pas l'environnement et ne lie aucune clé provider particulière au standard N1/N2.
## 5. N2 — sept unary standard
Ajout de `SolanaYellowstoneGrpcUnaryClient`, construit uniquement depuis `YellowstoneGrpcChannel`. Le Tonic `Channel`, `Grpc`, les Requests et les messages protobuf restent privés.
Surface exacte :
```text
SubscribeReplayInfo
Ping
GetLatestBlockhash
GetBlockHeight
GetSlot
IsBlockhashValid
GetVersion
```
Les chemins wire sont les chemins `geyser.Geyser` officiels. `SolanaCommitment` existant est réutilisé et mappé vers `Processed / Confirmed / Finalized`.
DTOs publics KSP :
```text
YellowstoneReplayInfo
YellowstonePong
YellowstoneLatestBlockhash
YellowstoneBlockHeight
YellowstoneSlot
YellowstoneBlockhashValidity
YellowstoneVersionInfo
```
Bornes/sécurité :
```text
max inbound/outbound appliqués au dispatcher Tonic
une seule deadline KSP couvre readiness + metadata + unary dispatch
blockhash request localement borné/validé avant I/O
blockhash/version response textuellement bornés
Ping exige l'echo exact du count
```
Nouveau code d'erreur :
```text
onchain_transport/grpc_status
```
Un `tonic::Status` distant n'est jamais copié dans `KspError` : seuls l'opération et le code gRPC sont conservés. Message/details/metadata provider restent hors diagnostic public.
## 6. Fixture Geyser locale
Les tests dev activent le serveur généré officiel et démarrent un `GeyserServer` sur `127.0.0.1:0`. Les deux RPCs streaming obligatoires du trait retournent volontairement `Unimplemented`, car ils restent hors `pre.003`.
La fixture vérifie réellement :
```text
les 7 paths unary
commitment wire
metadata publique reçue
metadata secrète reçue
replay optional first_available
Ping exact
latest blockhash / height / slot / validity / version
Status hostile contenant un canari secret non propagé
timeout unary local déterministe
blockhash invalide rejeté avant I/O
Debug client/channel sans URL ni secret
```
La fixture est locale et déterministe ; aucun provider externe n'est requis pour fermer cette tranche.
## 7. Canaries structurels
Les canaries vérifient en plus :
```text
feature set runtime/dev exact
yellowstone-grpc-client absent
proto runtime sans feature tonic
server/codegen uniquement dev/test
aucun pub use tonic/yellowstone_grpc_proto
sept chemins unary exacts
aucun Subscribe standard ajouté
aucun SubscribeDeshred ajouté
aucun PublicNode ajouté
Transport -X-> Config/env/WS pour la surface gRPC
```
## 8. Documentation
Mise à jour de :
```text
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
```
La matrice enregistre la fermeture réelle de `pre.002` et place les sept unary/TLS/metadata/fixture en candidate `pre.003`. Le forecast reste inchangé : `pre.004` ouvre seulement la foundation `Subscribe`.
## 9. Validation exécutée dans l'environnement de préparation
Exécuté réellement après les modifications source/documentaires :
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
```
L'environnement de préparation ne fournit pas Cargo/Rust ; aucune commande Cargo n'est déclarée réussie localement.
## 10. Gate opérateur requis
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
cargo tree -p ksp-onchain-transport-lib
cargo tree -p ksp-onchain-transport-lib -e features
cargo tree -p ksp-onchain-transport-lib --duplicates
cargo tree --duplicates
```
Les graphes doivent être réinspectés car `http`, `tonic-prost` et les features TLS/server/codegen changent le graphe de `pre.002`. Contrôler notamment :
```text
tonic / tonic-prost
prost / prost-types
http / hyper / hyper-util / tower
rustls / tokio-rustls / aws-lc
yellowstone-grpc-proto runtime vs dev feature unification
solana-pubkey
yellowstone-grpc-client absent
router/gzip/zstd non activés par la surface KSP ; server/codegen uniquement via dev/test
```
## 11. Fichiers ajoutés/modifiés
```text
Cargo.toml
crates/ksp-core-lib/tests/workspace_dependencies.rs
crates/ksp-onchain-transport-lib/Cargo.toml
crates/ksp-onchain-transport-lib/src/error.rs
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
crates/ksp-onchain-transport-lib/src/grpc_settings.rs
crates/ksp-onchain-transport-lib/src/grpc_unary.rs
crates/ksp-onchain-transport-lib/src/lib.rs
crates/ksp-onchain-transport-lib/src/rpc_common.rs
crates/ksp-onchain-transport-lib/tests/public_api.rs
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
deltas/0.2.9/pre.003.md
```

View File

@@ -0,0 +1,64 @@
<!-- file: deltas/0.2.9/pre.004-fix.001.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.004-fix.001` — suppression des warnings `dead_code` des conversions wire test-only
## 1. Déclencheur
Le premier gate opérateur `pre.004` est fonctionnellement vert :
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS / clean
cargo check --workspace PASS + 10 warnings dead_code
cargo clippy --workspace --all-targets PASS + 10 warnings dead_code
Transport unit 359/359 PASS
Transport public_api 44/44 PASS
Transport release_completeness 37/37 PASS
Transport doctests 4/4 PASS
Core workspace_dependencies 3/3 PASS
cargo test --workspace PASS + mêmes warnings de compilation
```
Les dix warnings concernent exclusivement les conversions protobuf privées de `grpc_subscribe.rs` (`to_wire` et `commitment_to_wire`).
## 2. Diagnostic
`pre.004` matérialise le contrat `SubscribeRequest` et vérifie son wire dans les unit tests, mais le stream bidi n'est volontairement ouvert qu'en `pre.008`. Ces helpers ont donc un consommateur test aujourd'hui et aucun consommateur runtime encore.
Contrairement au channel N1 de `pre.002`, ces fonctions ne représentent pas un état runtime devant exister avant leur consommation. Les compiler en production maintenant ne fait qu'introduire du code mort temporaire.
## 3. Correction
```text
#[cfg(test)] sur les 9 méthodes to_wire de grpc_subscribe.rs
#[cfg(test)] sur commitment_to_wire
aucune modification des types publics
aucune modification des validations/bornes
aucune modification du protobuf produit par les tests
aucune dépendance/feature Cargo modifiée
```
Lors de `pre.008`, quand la session bidi consommera réellement ces conversions, le `cfg(test)` sera retiré des helpers nécessaires au runtime.
## 4. Version
```text
workspace.package.version = 0.2.9-pre.4.fix.1
commit attendu = v0.2.9-pre.004-fix.001
aucun tag prerelease
```
## 5. Validation requise
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Aucun `cargo tree` supplémentaire n'est requis : le fix ne modifie ni dépendance ni feature.

250
deltas/0.2.9/pre.004.md Normal file
View File

@@ -0,0 +1,250 @@
<!-- file: deltas/0.2.9/pre.004.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.004` — fondation `SubscribeRequest` Yellowstone standard
## 1. Base et signal de version
Base obligatoire :
```text
0.2.9-pre.3.fix.1
```
Le gate opérateur fourni ferme `pre.003-fix.001` :
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS / clean
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS
cargo test --workspace PASS
Transport unit 354/354
Transport public_api 43/43
Transport release_completeness 36/36
Transport doctests 4/4
cargo tree -p ... fourni/relu
cargo tree -p ... -e features fourni/relu
cargo tree -p ... --duplicates fourni/relu
cargo tree --duplicates fourni/relu
```
Le graphe pertinent reste unifié sur `tonic 0.14.6`, `tonic-prost 0.14.6`, `prost/prost-types 0.14.4`, `yellowstone-grpc-proto 12.6.0` et `solana-pubkey 4.3.0`.
Cette tranche non-fix synchronise :
```text
workspace.package.version = 0.2.9-pre.4
commit attendu = v0.2.9-pre.004
aucun tag prerelease
```
## 2. Scope exact
`pre.004` matérialise uniquement la fondation N2 du `SubscribeRequest` standard publié :
```text
accounts
slots
transactions
transactions_status
blocks
blocks_meta
entry
commitment
accounts_data_slice
ping
from_slot
```
Le proto `yellowstone-grpc-proto 12.6.0` a été recontrôlé avant implémentation et conserve exactement ces champs top-level.
Sont explicitement hors tranche :
```text
champs détaillés Accounts / Slots -> pre.005
champs détaillés Transactions / transaction_status -> pre.006
champs détaillés Blocks / block_meta / entry updates -> pre.007
stream bidi / mutation / Ping-Pong runtime -> pre.008
reconnect / replay / gaps / duplicates -> pre.009
Config V3 / PublicNode -> pre.010/011
SubscribeDeshred -> OUT 0.2.9 standard
```
## 3. Contrat KSP ajouté
Nouveaux types publics :
```text
YellowstoneSubscribeFilterName
YellowstoneAccountsDataSlice
YellowstoneSubscribePing
YellowstoneSubscribeAccountFilter
YellowstoneSubscribeSlotFilter
YellowstoneSubscribeTransactionFilter
YellowstoneSubscribeBlockFilter
YellowstoneSubscribeBlocksMetaFilter
YellowstoneSubscribeEntryFilter
YellowstoneSubscribeRequest
```
Les quatre filtres de famille riches sont volontairement des shells typés en `pre.004`. Ils permettent de matérialiser les sept maps et les entrées nommées vides sans anticiper les champs propres aux tranches suivantes.
Le wire `yellowstone_grpc_proto::geyser::SubscribeRequest` reste privé : aucun type Tonic/Prost/Yellowstone n'est réexporté au crate root.
## 4. Sémantique maps / noms
KSP conserve sept maps logiques indépendantes.
```text
map vide = aucun filtre actif dans la famille
entrée nommée + message vide = activation explicite d'un groupe vide supporté par le proto
```
Le Protobuf ne distingue pas une map top-level omise d'une map vide ; KSP ne prétend donc pas préserver une différence wire inexistante.
Les noms sont :
```text
non vides
trim exact
sans caractère de contrôle
<= 128 octets UTF-8
uniques globalement entre les sept maps
```
L'unicité globale évite l'ambiguïté lorsque Yellowstone renvoie seulement les noms correspondants dans `SubscribeUpdate.filters[]`.
Le nombre total de groupes nommés est plafonné à `1024` sur l'ensemble des sept maps.
## 5. Champs communs et bounds
`commitment` réutilise `SolanaCommitment` et mappe exactement :
```text
Processed -> PROCESSED
Confirmed -> CONFIRMED
Finalized -> FINALIZED
```
`accounts_data_slice` :
```text
ordre d'insertion conservé
nombre <= 128
length <= 64 MiB
offset + length doit rester représentable en u64
length = 0 reste représentable
```
`ping` conserve l'identifiant `i32` exact.
`from_slot` conserve l'optional `u64` sans lui attribuer encore une promesse de replay/lossless ; sa sémantique lifecycle reste en `pre.009`.
## 6. Diagnostics et sécurité
`Debug` de `YellowstoneSubscribeFilterName` est redacted.
`Debug` de `YellowstoneSubscribeRequest` expose uniquement :
```text
compteurs par famille
commitment
nombre de data slices
ping
from_slot
```
Il n'expose aucun nom de filtre ni futur payload account/transaction/block.
Les erreurs déterministes utilisent `ERROR_CODE_INVALID_RPC_PARAMETERS` et n'incluent pas les valeurs de noms/payloads rejetés.
## 7. Dépendances
Aucune dépendance ni feature Cargo n'est modifiée dans `pre.004`.
Les graphes fournis pour `pre.003-fix.001` restent l'autorité courante ; aucun nouveau `cargo tree` n'est requis spécifiquement par ce delta sauf anomalie de compilation.
## 8. Tests/canaries ajoutés
Unit tests :
```text
nom vide/whitespace/control/oversized rejeté
Debug nom redacted
unicité globale cross-family
request vide -> sept maps vides + common absent
sept entrées nommées vides -> wire exact
commitment/ping/from_slot exacts
ordre data slices conservé
zero-length slice conservée
slice length/overflow/count bornés
filter-group count borné
request Debug sans filter name
```
Public API : construction des sept maps et champs communs depuis le crate root.
Release completeness :
```text
onze champs top-level présents dans l'adapter
bounds communs matérialisés
aucun SubscribeDeshred
aucun PublicNode/OrbitFlare/Helius runtime
aucun raw Tonic/Yellowstone reexport
to_wire protobuf non public
```
## 9. Fichiers ajoutés
```text
crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs
crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs
deltas/0.2.9/pre.004.md
```
## 10. Fichiers modifiés
```text
Cargo.toml
crates/ksp-onchain-transport-lib/src/lib.rs
crates/ksp-onchain-transport-lib/tests/public_api.rs
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
```
## 11. Validation de préparation
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
```
L'environnement de préparation ne fournit pas Cargo/Rust ; aucune compilation n'est déclarée réussie localement.
## 12. Gate opérateur attendu
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Attendu si aucun fix n'est nécessaire :
```text
Transport unit 359
Transport public_api 44
Transport release_completeness 37
Transport doctests 4
```
Les live smokes restent ignored/opt-in.

View File

@@ -0,0 +1,61 @@
<!-- file: deltas/0.2.9/pre.005-fix.001.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.005-fix.001`
## Objet
Corriger le premier gate opérateur de `0.2.9-pre.005` sans modifier le contrat fonctionnel Accounts + Slots.
## Preuve opérateur avant fix
Le gate `pre.005` confirme :
```text
cargo fmt --all PASS
workspace Rust audit PASS / clean
cargo check --workspace PASS + 4 dead_code warnings
cargo clippy --workspace --all-targets FAIL: implicit_return + wrong_self_convention
Transport unit 364/364 PASS
Transport public_api 45/45 PASS
Transport release_completeness 38/38 PASS
Transport doctests 4/4 PASS
Core dependency canary 3/3 PASS
cargo test --workspace PASS + 4 dead_code warnings
```
## Corrections
| Surface | Correction | Justification |
|-------------------------------------------|--------------------------------------------|--------------------------------------------------------------------------------|
| quatre constantes de bounds update | ajout `#[cfg(test)]` | uniquement consommées par les décodeurs Account/Slot test-only avant `pre.008` |
| `YellowstoneSubscribeSlotFilter::to_wire` | receiver `self` | type `Copy`, conformité `clippy::wrong_self_convention` |
| `dead_error.as_ref().is_some_and(...)` | `return` explicite dans la closure | conformité `-D clippy::implicit-return` |
| `016` + `012` | réalignement de tous les tableaux Markdown | préserve le format visuel manuel demandé par l'opérateur |
| workspace version | `0.2.9-pre.5.fix.1` | signal technique de fix |
## Non-changements
```text
aucun changement du contrat public Accounts/Slots
aucun changement du wire protobuf
aucune dépendance/feature Cargo
aucun stream bidi
aucune famille Transactions/Blocks
aucun PublicNode / Config V3
aucun allow(dead_code)
```
## Gate attendu
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Critère de fermeture : `check`, Clippy et workspace passent sans les quatre warnings `dead_code` de `pre.005`.

121
deltas/0.2.9/pre.005.md Normal file
View File

@@ -0,0 +1,121 @@
<!-- file: deltas/0.2.9/pre.005.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.005` — Yellowstone standard Accounts + Slots
## Base et signal de version
Base autoritaire :
```text
0.2.9-pre.4.fix.1
```
Signal technique de cette tranche :
```text
workspace.package.version = 0.2.9-pre.5
commit attendu = v0.2.9-pre.005
aucun tag prerelease
```
Le gate opérateur de `pre.004-fix.001` est vert sans les warnings `dead_code` précédents ; `pre.004` est donc fermée.
## Périmètre réalisé
Cette tranche complète **uniquement Accounts + Slots** du contrat Yellowstone `Subscribe` standard actuel.
Accounts request :
```text
account[]
owner[]
filters[] :
memcmp bytes/base58/base64
datasize
token_account_state
lamports eq/ne/lt/gt
nonempty_txn_signature?
cuckoo_accounts_filter?
```
Slots request :
```text
filter_by_commitment?
interslot_updates?
```
Updates KSP :
```text
Account : filters/created_at + account info + slot + is_startup
Slot : filters/created_at + slot + parent? + status + dead_error?
```
Les sept statuts Slot actuels sont couverts : processed, confirmed, finalized, first-shred-received, completed, created-bank et dead.
## Cuckoo standard
Le Cuckoo account filter est traité comme capacité du standard Yellowstone courant, pas comme extension provider. KSP expose son propre DTO avec données, dimensions, fingerprint bits, seed et algorithme ; l'algorithme courant matérialisé est SipHash. Aucun type upstream n'est réexporté.
## Validation et sécurité
Bornes KSP provider-neutral ajoutées sur :
```text
account/owner selectors
account predicates
memcmp binary/text
Cuckoo payload/dimensions/fingerprint
account-data update
update filter names/count
timestamp nanos
transaction signature fixe 64 octets
dead_error
```
Les pubkeys/owners de wire doivent faire exactement 32 octets et les signatures 64 octets. Un enum Slot inconnu, un oneof ou champ obligatoire absent et les payloads hors bornes deviennent des erreurs KSP déterministes ; aucun payload provider arbitraire n'est recopié dans les diagnostics. Les Debug KSP n'exposent pas les sélecteurs, données memcmp/Cuckoo/account-data ni le texte `dead_error`.
## Frontière runtime
Le stream bidi reste hors tranche. Les conversions protobuf request et les décodeurs Account/Slot restent donc compilés seulement pour les tests jusqu'à `pre.008`, où ils acquerront leur premier consommateur runtime. Cela évite les warnings `dead_code` sans dupliquer l'implémentation.
## Explicitement hors `pre.005`
```text
Transactions + transaction_status
Blocks + block_meta + entry
Ping/Pong lifecycle et mutation bidi
backpressure/shutdown stream
reconnect/resubscribe/replay
Config V3
PublicNode provider integration
SubscribeDeshred
```
## Dépendances
Aucune dépendance ni feature Cargo n'est ajoutée ou modifiée. Les graphes inspectés en `pre.003` restent applicables.
## Validation attendue
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Aucun `cargo tree` supplémentaire n'est requis pour cette tranche si le manifest reste inchangé.
## Suite
```text
pre.006 Transactions + transaction_status
pre.007 Blocks + block_meta + entry
pre.008 stream bidi + mutation + Ping/Pong + half-close + backpressure + shutdown
```

167
deltas/0.2.9/pre.006.md Normal file
View File

@@ -0,0 +1,167 @@
<!-- file: deltas/0.2.9/pre.006.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.006` — namespace privé HTTP explicite
## 1. Base requise
```text
0.2.9-pre.5.fix.1
```
Le gate opérateur de `pre.005-fix.001` est intégralement vert : fmt, audit Rust, check, Clippy, Transport 364 unit + 45 public API + 38 release-completeness + 4 doctests, dependency canary Core 3/3 et workspace complet.
## 2. Objectif
Réduire l'ambiguïté croissante dans `ksp-onchain-transport-lib` maintenant que HTTP, WebSocket et Yellowstone gRPC coexistent dans la même crate.
Cinq modules privés sont exclusivement propriétaires de la pile HTTP et reçoivent un préfixe explicite :
```text
client.rs -> http_client.rs
executor.rs -> http_executor.rs
pool.rs -> http_pool.rs
resilience.rs -> http_resilience.rs
settings.rs -> http_settings.rs
```
Les unit tests miroirs suivent exactement les mêmes renommages.
## 3. Frontière du renommage
Le renommage n'est pas appliqué mécaniquement à tous les anciens modules.
`rpc_accounts`, `rpc_blocks`, `rpc_transactions` et `rpc_common` portent déjà des DTOs/types Solana réutilisés par WebSocket et/ou gRPC. Le préfixe `http_` y serait donc architecturalement faux. Les autres `rpc_*` restent dans la même famille cohérente.
`json_rpc` décrit une couche de protocole/enveloppe et conserve son nom. `constants` et `error` sont transverses à plusieurs transports et restent également inchangés.
Aucun type ou nom public n'est renommé : les surfaces HTTP publiques utilisent déjà des noms `Http*` et restent réexportées depuis le crate root.
## 4. Forecast recalibré
L'ancien `pre.006` fonctionnel est décalé afin de ne pas mélanger ce refactor de fichiers avec l'ajout Transactions/transaction_status.
```text
pre.006 namespace privé HTTP explicite
pre.007 Transactions + transaction_status
pre.008 Blocks + block_meta + entry
pre.009 bidi/backpressure/half-close/shutdown
pre.010 reconnect/replay/gaps/duplicates
pre.011 Config V3 + protocol/provider + profils PublicNode
pre.012 PublicNode live + compliance + docs/prompt 0.2.10
rel.001
```
Le contenu fonctionnel des tranches décalées ne change pas.
## 5. Fichiers ajoutés par renommage
```text
crates/ksp-onchain-transport-lib/src/http_client.rs
crates/ksp-onchain-transport-lib/src/http_executor.rs
crates/ksp-onchain-transport-lib/src/http_pool.rs
crates/ksp-onchain-transport-lib/src/http_resilience.rs
crates/ksp-onchain-transport-lib/src/http_settings.rs
crates/ksp-onchain-transport-lib/unit_tests/http_client.rs
crates/ksp-onchain-transport-lib/unit_tests/http_executor.rs
crates/ksp-onchain-transport-lib/unit_tests/http_pool.rs
crates/ksp-onchain-transport-lib/unit_tests/http_resilience.rs
crates/ksp-onchain-transport-lib/unit_tests/http_settings.rs
```
## 6. Fichiers supprimés
Un ZIP overlay ne peut pas supprimer ces chemins. L'opérateur doit donc les retirer explicitement après extraction :
```text
crates/ksp-onchain-transport-lib/src/client.rs
crates/ksp-onchain-transport-lib/src/executor.rs
crates/ksp-onchain-transport-lib/src/pool.rs
crates/ksp-onchain-transport-lib/src/resilience.rs
crates/ksp-onchain-transport-lib/src/settings.rs
crates/ksp-onchain-transport-lib/unit_tests/client.rs
crates/ksp-onchain-transport-lib/unit_tests/executor.rs
crates/ksp-onchain-transport-lib/unit_tests/pool.rs
crates/ksp-onchain-transport-lib/unit_tests/resilience.rs
crates/ksp-onchain-transport-lib/unit_tests/settings.rs
```
Commande opérateur :
```bash
rm \
crates/ksp-onchain-transport-lib/src/{client,executor,pool,resilience,settings}.rs \
crates/ksp-onchain-transport-lib/unit_tests/{client,executor,pool,resilience,settings}.rs
```
## 7. Fichiers modifiés
```text
Cargo.toml
crates/ksp-onchain-transport-lib/src/lib.rs
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
deltas/0.2.9/pre.006.md
```
`workspace.package.version` devient :
```text
0.2.9-pre.6
```
## 8. Canaries
Le nouveau canari release-completeness vérifie :
- les cinq déclarations privées `mod http_*` ;
- l'absence des cinq anciennes déclarations ambiguës ;
- la présence des types/fonctions HTTP structurants dans les nouveaux fichiers ;
- le maintien volontaire des modules partagés/protocolaires `rpc_*`, `json_rpc`, `constants`, `error` ;
- l'absence de changement de surface publique requise par ce refactor.
## 9. Documentation Markdown
Les tableaux touchés dans `016` et `012` sont reformattés selon le comportement JetBrains RustRover : largeur de chaque colonne basée sur son contenu le plus large, puis exactement un espace de padding de part et d'autre du contenu avant les pipes.
## 10. Validations exécutées lors de la préparation
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
```
Contrôles statiques supplémentaires :
```text
5 nouveaux modules HTTP source présents
5 nouveaux unit tests miroirs présents
5 anciens modules source absents du worktree final
5 anciens unit tests absents du worktree final
aucun renommage des modules rpc_* partagés
aucun changement de dépendance Cargo
aucune modification de public_api.rs
```
## 11. Validations non exécutées dans l'environnement de préparation
Cargo/Rust ne sont pas disponibles dans l'environnement de préparation. L'opérateur doit exécuter après extraction **et suppression des anciens chemins** :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Aucun `cargo tree` n'est requis : aucune dépendance ni feature ne change.
## 12. Verdict
`pre.006` est une candidate structurelle sans changement fonctionnel ni API publique. Sa fermeture exige l'absence effective des dix anciens chemins et un gate Cargo intégralement vert.

165
deltas/0.2.9/pre.007.md Normal file
View File

@@ -0,0 +1,165 @@
<!-- file: deltas/0.2.9/pre.007.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.007` — Yellowstone Transactions + `transaction_status`
## 1. Base et gate précédent
Base exacte :
```text
0.2.9-pre.006
```
Le gate opérateur `pre.006` est fermé : fmt/audit/check/Clippy/workspace PASS, Transport 364 unit + 45 public API + 39 release-completeness + 4 doctests, dependency canary Core 3/3 PASS.
Version technique de cette tranche :
```text
0.2.9-pre.7
```
Commit attendu après gate vert :
```text
v0.2.9-pre.007
```
Aucun tag Git stable n'est créé pour cette prerelease.
## 2. Objet de la tranche
`pre.007` complète exclusivement les deux familles Yellowstone standard `transactions` et `transactions_status` du `SubscribeRequest`/`SubscribeUpdate` courant. Elle n'ouvre toujours pas le stream bidi runtime.
Le proto publié `yellowstone-grpc-proto 12.6.0` a été réaudité avant implémentation. Le même `SubscribeRequestFilterTransactions` est utilisé par les deux maps et expose :
```text
vote?
failed?
signature?
account_include[]
account_exclude[]
account_required[]
cuckoo_account_include?
token_accounts? = ALL | BALANCE_CHANGED
```
## 3. Contrat request KSP
La tranche matérialise `YellowstoneSubscribeTransactionFilter` et les types associés :
```text
YellowstoneTransactionSignatureSelector
YellowstoneTokenAccountExpansion
YellowstoneCuckooFilter réutilisé
```
La signature textuelle est :
- non vide ;
- bornée à 128 octets ;
- strictement Base58 ;
- décodée par KSP pour vérifier une largeur exacte de 64 octets ;
- absente des diagnostics et du `Debug`.
Les listes include/exclude/required conservent l'ordre d'insertion et réutilisent le bound déterministe des sélecteurs Accounts. Le Cuckoo transaction réutilise le contrat standard KSP déjà introduit en `pre.005`.
## 4. Updates et `solana-storage.proto`
Les nouveaux DTOs KSP couvrent les variantes :
```text
SubscribeUpdateTransaction
SubscribeUpdateTransactionStatus
```
et projettent sans raw reexport upstream les structures utiles :
```text
Transaction / Message / MessageHeader
CompiledInstruction / MessageAddressTableLookup
TransactionConfig V1
TransactionStatusMeta / TransactionError
InnerInstructions / InnerInstruction
TokenBalance / UiTokenAmount
ReturnData / Reward
```
Le `Message.config` optionnel actuel est conservé avec :
```text
priority_fee?
compute_unit_limit?
loaded_accounts_data_size_limit?
heap_size?
```
`TransactionStatusMeta` conserve séparément les marqueurs legacy `inner_instructions_none`, `log_messages_none` et `return_data_none`, ainsi que les payloads correspondants. Les champs optionnels `compute_units_consumed` et `cost_units`, les loaded addresses, rewards et token balances sont également préservés.
`TransactionError.err` reste un payload opaque borné ; KSP n'introduit aucun décodage JSON ou Program arbitraire.
## 5. Validation et sûreté
Les décodeurs test-only vérifient notamment :
| Élément | Politique `pre.007` |
|------------------------------------------------------|----------------------------------------------------------------------------|
| signatures wire | exactement 64 octets |
| hash / account keys / loaded addresses / program ids | exactement 32 octets |
| vecteurs transaction/meta | bornes KSP avant projection |
| instruction / return-data payloads | bornés |
| logs et textes provider | count/length bornés |
| error bytes | opaques et bornés |
| `Debug` | pas de signature, pubkey, log, instruction data ou error bytes arbitraires |
Les conversions protobuf request et les décodeurs update restent `#[cfg(test)]` jusqu'à `pre.009`, premier consommateur runtime prévu lors de l'ouverture du stream bidi.
## 6. Frontières préservées
Toujours hors `pre.007` :
```text
Blocks + block_meta + entry pre.008
bidi/backpressure/Ping-Pong/shutdown pre.009
reconnect/replay/gaps/duplicates pre.010
Config V3 + PublicNode profiles pre.011
PublicNode live/compliance pre.012
SubscribeDeshred OUT 0.2.9 standard
```
Aucun provider N3 n'est ajouté. Aucune dépendance ni feature Cargo n'est modifiée.
## 7. Preuves ajoutées
Tests Transport ajoutés :
```text
transaction filter exact wire + redaction + Base58 width
transaction update storage fixture + TransactionConfig V1 + meta
transaction_status update + error + malformed signature
public API root contract
release-completeness exact scope
```
Compte cible après compilation :
```text
Transport unit ~367
Transport public API 46
Transport release completeness 40
Transport doctests 4
```
## 8. Gate opérateur requis
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Aucun `cargo tree` supplémentaire n'est requis : aucune dépendance ou feature ne change dans cette tranche.

View File

@@ -0,0 +1,59 @@
<!-- file: deltas/0.2.9/pre.008-fix.001.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.008-fix.001` — hygiène Clippy fixture Blocks
## 1. Objet
Corriger l'unique warning Clippy observé au premier gate opérateur de `pre.008`, sans modifier le contrat fonctionnel Blocks + `block_meta` + `entry`.
## 2. Preuve opérateur avant fix
```text
cargo fmt --all PASS
workspace Rust audit PASS / clean
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS + 1 warning field_reassign_with_default
Transport unit 370/370 PASS
Transport public_api 47/47 PASS
Transport release_completeness 41/41 PASS
Transport doctests 4/4 PASS
Core dependency canary 3/3 PASS
cargo test --workspace PASS
```
## 3. Correction
| Surface | Correction | Justification |
|---------------------------------------------|--------------------------------------------------------------|------------------------------------------------|
| `minimal_transaction_info()` fixture Blocks | `TransactionStatusMeta { fee: 5_000, ..Default::default() }` | supprime `clippy::field_reassign_with_default` |
| workspace version | `0.2.9-pre.8.fix.1` | signal technique du delta fix |
| `016` + `012` | preuve du gate et statut `fix.001` | traçabilité de fermeture de `pre.008` |
## 4. Non-changements
```text
aucun changement runtime N1/N2
aucun changement du filtre Blocks
aucun changement des DTOs Block/BlockMeta/Entry
aucun changement du wire protobuf
aucune dépendance/feature Cargo
aucun allow Clippy
aucun stream bidi
aucun reconnect/replay
aucun PublicNode / Config V3
```
## 5. Gate attendu
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Critère de fermeture : même gate vert, cette fois sans warning `field_reassign_with_default`.

73
deltas/0.2.9/pre.008.md Normal file
View File

@@ -0,0 +1,73 @@
<!-- file: deltas/0.2.9/pre.008.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.008` — Yellowstone Blocks + block_meta + entry
## 1. Base et signal technique
Base : `0.2.9-pre.007`, fermée sur gate opérateur intégralement vert.
```text
workspace.package.version = 0.2.9-pre.8
```
## 2. Scope
La tranche complète uniquement la dernière famille de filtres/updates standard avant louverture bidi : Blocks, `block_meta` et `entry`.
| Surface | Matérialisation |
|----------------------------|-----------------|
| filtre Blocks | complet |
| `blocks_meta` | marker inchangé |
| `entry` | marker inchangé |
| `SubscribeUpdateBlock` | DTO + decode |
| `SubscribeUpdateBlockMeta` | DTO + decode |
| `SubscribeUpdateEntry` | DTO + decode |
| stream bidi / lifecycle | OUT |
| reconnect / replay | OUT |
| PublicNode / Config V3 | OUT |
| `SubscribeDeshred` | OUT `0.2.9` |
## 3. Filtre Blocks
`YellowstoneSubscribeBlockFilter` matérialise exactement le wire courant : `account_include[]`, `include_transactions?`, `include_accounts?`, `include_entries?` et `cuckoo_account_include?`. Les account selectors utilisent `ksp_core_lib::Pubkey`, restent ordonnés et bornés ; `Debug` ne rend aucune adresse.
## 4. Updates Blocks
`YellowstoneBlockUpdate` conserve la metadata de bloc, rewards, temps/hauteur optionnels, parent, compteurs et payloads optionnels. Les transactions réutilisent `YellowstoneTransactionInfo`, les comptes réutilisent `YellowstoneAccountInfo`, et les entries utilisent `YellowstoneEntryInfo`.
`YellowstoneBlockMetaUpdate` conserve la variante metadata-only. `YellowstoneEntryUpdate` enveloppe la même `YellowstoneEntryInfo` réutilisée dans les blocs. `starting_transaction_index` reste explicite, y compris sa valeur legacy `0`.
Les compteurs serveur sont indépendants des tailles de vecteurs : aucune égalité artificielle nest imposée quand les flags `include_*` demandent lomission dun payload.
## 5. Validation et sécurité
Les blockhash textuels sont bornés, trim-exacts et Base58-décodés vers exactement 32 octets ; les hash dentrée sont exactement 32 octets. Les collections Block/Rewards sont bornées. `Debug` ne copie aucun blockhash, parent blockhash, reward pubkey, account selector, données account, transaction, instruction ou entry hash.
Les conversions protobuf et décodeurs restent sous `#[cfg(test)]` jusquà louverture du stream runtime en `pre.009`.
## 6. Tests/canaries ajoutés
```text
block filter exact wire + redaction
block update complet avec Transaction/Account/Entry réutilisés
server counts distincts des payload-vector lengths
block-meta optional states
entry starting_transaction_index + malformed hash
public API canary pre.008
release completeness canary pre.008
```
## 7. Gate opérateur
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Aucune dépendance ni feature Cargo nest modifiée ; aucun `cargo tree` supplémentaire nest requis.

View File

@@ -0,0 +1,75 @@
<!-- file: deltas/0.2.9/pre.009-fix.001.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.009-fix.001` — hygiène Clippy du flux bidi
## 1. Objet
Supprimer les deux warnings Clippy observés au gate opérateur de `pre.009`, sans modifier le protocole Yellowstone, le lifecycle bidi, les bornes ni les dépendances.
## 2. Gate opérateur avant fix
| Gate | Résultat |
|------------------------------------------|-------------------|
| `cargo fmt --all` | PASS |
| audit Rust workspace | PASS / clean |
| `cargo check --workspace` | PASS |
| `cargo clippy --workspace --all-targets` | PASS + 2 warnings |
| Transport unit | 379/379 PASS |
| Transport `public_api` | 48/48 PASS |
| Transport `release_completeness` | 42/42 PASS |
| Transport doctests | 4/4 PASS |
| Core dependency canary | 3/3 PASS |
| `cargo test --workspace` | PASS |
Warnings :
```text
clippy::large_enum_variant
YellowstoneSubscribeUpdate::Transaction >= 664 bytes
YellowstoneSubscribeUpdate::Block >= 280 bytes
clippy::extra_unused_type_parameters
assert_send<T: Send>() dans tests/public_api.rs
```
## 3. Correctifs
### 3.1 `YellowstoneSubscribeUpdate::Transaction`
La variante devient :
```rust
Transaction(Box<YellowstoneTransactionUpdate>)
```
Le décodeur alloue cette indirection uniquement pour les updates transaction. Le DTO `YellowstoneTransactionUpdate`, le wire protobuf et les autres variantes ne changent pas. La queue bidi na plus à dimensionner chaque élément sur la variante transaction de ~664 octets.
### 3.2 Canari `Send`
Le helper de test conserve `T: Send` et matérialise `PhantomData<T>` dans son corps. Le paramètre générique devient réellement utilisé sans construction réseau ni changement de contrat.
## 4. Frontières inchangées
```text
reconnect / resubscribe / replay OUT -> pre.010
SubscribeDeshred OUT 0.2.9
PublicNode / Config V3 OUT pre.009
dépendances / features inchangées
```
Aucun `#[allow(clippy::...)]` nest ajouté.
## 5. Gate attendu
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Attendu : Clippy sans warning et compteurs fonctionnels inchangés à 379 unit / 48 public API / 42 release-completeness / 4 doctests.

68
deltas/0.2.9/pre.009.md Normal file
View File

@@ -0,0 +1,68 @@
<!-- file: deltas/0.2.9/pre.009.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.009` — bidi standard + backpressure + half-close + shutdown
## Base
```text
0.2.9-pre.008-fix.001
gate opérateur final : fmt/audit/check/Clippy/workspace PASS sans warning
Transport : 370 unit + 47 public API + 41 release-completeness + 4 doctests
```
## Changements
- ouvre `/geyser.Geyser/Subscribe` avec le channel Tonic N1 existant ;
- ajoute `SolanaYellowstoneGrpcSubscribeSession` et son état public sûr ;
- branche directement une `mpsc` request bornée dans le stream client Tonic ;
- ajoute `try_update()` avec validation, borne protobuf et erreurs Full/Closed structurées ;
- promeut les conversions request et décodeurs `pre.004008` de test-only vers runtime privé ;
- expose `YellowstoneSubscribeUpdate` couvrant les neuf variantes standard ;
- répond automatiquement au Ping serveur avec un request ping-only `id=1` et conserve Pong observable ;
- traite le server half-close comme terminaison normale de `pre.009` ;
- ajoute client half-close, shutdown borné, Drop best-effort et overflow terminal ;
- conserve les `Status` distants sous forme de diagnostics KSP sûrs sans recopier message/details/metadata ;
- ajoute les codes `grpc_backpressure_overflow` et `grpc_session_closed`.
## Fixture locale
| Cas | Preuve |
|------------------------|-------------|
| round-trip bidi | PASS source |
| mutation request | PASS source |
| Ping -> ping id=1 | PASS source |
| Pong observable | PASS source |
| server half-close | PASS source |
| client half-close | PASS source |
| shutdown hostile borné | PASS source |
| Drop session | PASS source |
| slow receiver overflow | PASS source |
| remote Status sûr | PASS source |
| update malformed | PASS source |
| inbound oversized | PASS source |
| outbound oversized | PASS source |
## Frontières
```text
OUT pre.009 : reconnect/resubscribe/from_slot replay policy/gaps/duplicates
OUT 0.2.9 : SubscribeDeshred
OUT pre.009 : PublicNode / Config V3 / provider facade
```
Aucune dépendance ou feature Cargo n'est ajoutée. Le runtime n'utilise toujours pas `yellowstone-grpc-client` et n'expose aucun type Tonic/protobuf brut.
## Validation candidate
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Le `cargo tree` n'est pas requis : aucune dépendance ni feature n'a changé.

View File

@@ -0,0 +1,101 @@
<!-- file: deltas/0.2.9/pre.010-fix.001.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.010-fix.001` — Clippy reconnect + cohérence crate-root
## 1. Base
```text
0.2.9-pre.010
workspace.package.version = 0.2.9-pre.10
```
Le gate opérateur fourni confirme :
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS / clean
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS + 2 warnings
Transport unit 383/383 PASS
Transport public_api 49/49 PASS
Transport release_completeness 43/43 PASS
Transport doctests 4/4 PASS
cargo test --workspace PASS
```
## 2. Motif
Deux `clippy::collapsible_if` subsistent dans `grpc_stream.rs` :
```text
ContinuityTracker::observe() éviction du cache borné
run_subscribe_actor() Ping -> send_automatic_ping
```
La revue de code détecte également une incohérence documentaire dans `src/lib.rs` : le commentaire crate-root reste au niveau `pre.009` et affirme encore que reconnect/replay sont hors tranche, alors que `pre.010` les a matérialisés.
## 3. Correctifs
### 3.1 Clippy
Les deux `if` imbriqués sont convertis en let-chains équivalentes. Aucun `#[allow(clippy::...)]` n'est ajouté.
Aucune sémantique n'est modifiée :
```text
cache duplicate toujours borné à 512 identités
Ping automatique même send_automatic_ping, mêmes erreurs terminales
reconnect inchangé
ReplayInfo/from_slot inchangés
gaps/duplicates compteurs inchangés
backpressure/shutdown inchangés
```
### 3.2 Documentation crate-root
`ksp-onchain-transport-lib/src/lib.rs` décrit maintenant explicitement `0.2.9-pre.010` : reconnect borné KSP-owned, reprise depuis le dernier request/slot observé, gap prudent via ReplayInfo et duplicates observables sans promesse exactly-once/lossless.
### 3.3 Signal de version
Le correctif touche du Rust ; conformément à `VER-ID-007`/`VER-ID-010` :
```text
workspace.package.version = 0.2.9-pre.10.fix.1
commit attendu = v0.2.9-pre.010-fix.001
```
## 4. Documentation de release
Mise à jour :
```text
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
```
Le premier gate opérateur et le statut du fix y sont enregistrés. `pre.011` reste explicitement non commencée avant fermeture sans warning du présent fix.
## 5. Frontières inchangées
```text
OUT fix.001 : Config V3 / PublicNode / provider facade
OUT 0.2.9 : SubscribeDeshred
aucune dépendance / feature Cargo ajoutée
aucun wire protobuf modifié
aucun contrat public reconnect/replay modifié
```
## 6. Gate opérateur attendu
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Attendu : Clippy sans warning, Transport 383 unit / 49 public API / 43 release-completeness / 4 doctests et workspace vert. Aucun `cargo tree` supplémentaire n'est requis, le graphe Cargo étant inchangé.

94
deltas/0.2.9/pre.010.md Normal file
View File

@@ -0,0 +1,94 @@
<!-- file: deltas/0.2.9/pre.010.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.010` — reconnect borné + replay prudent + continuité observable
## Base
```text
0.2.9-pre.009-fix.001
gate opérateur final : fmt/audit/check/Clippy/workspace PASS sans warning
Transport : 379 unit + 48 public API + 42 release-completeness + 4 doctests
```
## Changements runtime
- ajoute l'état public `YellowstoneGrpcSubscribeState::Reconnecting` ;
- ajoute `YellowstoneGrpcSubscribeSnapshot` avec compteurs sûrs de reconnect/replay/continuité ;
- active le `YellowstoneGrpcReconnectSettings` déjà introduit en `pre.002` pour le stream `Subscribe` ;
- rouvre le stream avec un backoff exponentiel borné et un nombre d'essais borné ;
- conserve le dernier `YellowstoneSubscribeRequest` complet accepté ;
- après perte du stream, resoumet ce request déterministement avec un `from_slot` au moins égal au plus haut slot déjà observé ;
- consulte `SubscribeReplayInfo` avant les tentatives de replay lorsque `from_slot` est disponible ;
- si `first_available > from_slot`, incrémente `continuity_gap_count` et clamp le replay à `first_available` ;
- maintient un cache borné de 512 identités d'updates pour observer les duplicates autour d'un replay ;
- les duplicates restent livrés : aucun contrat exactly-once n'est inventé ;
- rejette les mutations request pendant `Reconnecting` pour éviter une application ambiguë entre deux streams physiques ;
- un shutdown pendant le backoff interrompt immédiatement la boucle et interdit une nouvelle ouverture ;
- un épuisement du budget devient terminal avec un code KSP sûr `grpc_channel_failed`.
## Sémantique de continuité
```text
from_slot de reconnect = max(from_slot explicite du dernier request, last_observed_slot)
ReplayInfo = information de rétention ; pas preuve de replay complet
continuity gap = couverture de replay indisponible, comptée seulement si first_available > slot demandé
slot manquant entre deux updates filtrés = jamais assimilé automatiquement à un gap
duplicate = identité KSP bornée déjà observée ; update toujours livrée
exactly-once = non garanti
lossless = non garanti
ordre global sans gap = non garanti
```
Identités retenues pour l'observation bornée :
```text
Account slot + pubkey + write_version
Slot slot + status
Transaction slot + signature
TransactionStatus slot + signature
Block slot + blockhash
BlockMeta slot + blockhash
Entry slot + index + hash
Ping/Pong hors déduplication
```
La couverture d'une éventuelle divergence de node reste volontairement limitée : deux Block/BlockMeta de même slot mais de blockhash différent ne sont pas classés duplicate. KSP ne généralise pas cette preuve aux familles qui ne transportent pas de blockhash.
## Fixture locale
| Cas | Preuve |
|------------------------------------------------|-------------|
| reconnect après server half-close | PASS source |
| `from_slot = last_observed_slot` | PASS source |
| ReplayInfo disponible | PASS source |
| replay duplicate observable | PASS source |
| couverture replay indisponible prouvée + clamp | PASS source |
| budget reconnect épuisé | PASS source |
| mutation pendant reconnect rejetée | PASS source |
| shutdown pendant backoff | PASS source |
| endpoint/Status secret non réémis | PASS source |
| anciens gates bidi avec reconnect `0` | PASS source |
## Frontières
```text
OUT pre.010 : Config V3 / PublicNode / provider facade
OUT 0.2.9 : SubscribeDeshred
aucune nouvelle dépendance / feature Cargo
aucun yellowstone-grpc-client runtime
```
## Validation candidate
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Le `cargo tree` n'est pas requis : aucune dépendance ni feature n'a changé.

284
deltas/0.2.9/pre.011.md Normal file
View File

@@ -0,0 +1,284 @@
<!-- file: deltas/0.2.9/pre.011.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.011` — Config Transport V3 + PublicNode Mainnet + consolidation documentaire
## 1. Base et version
Base immédiate :
```text
0.2.9-pre.010-fix.001
Cargo workspace = 0.2.9-pre.10.fix.1
```
Version candidate :
```text
0.2.9-pre.011
Cargo workspace = 0.2.9-pre.11
```
Le gate opérateur fourni pour `pre.010-fix.001` est entièrement vert : fmt/audit/check/Clippy sans warning, Transport 383 unit + 49 public API + 43 release-completeness + 4 doctests, dependency canary 3/3 et `cargo test --workspace` PASS.
## 2. Objet
Cette tranche matérialise la frontière Config décidée par `pre.001` puis différée jusqu'après le lifecycle/reconnect Yellowstone :
```text
std.transport V3
Config -> YellowstoneGrpcTransportSettings
provider et protocol distincts
metadata publique et secrète avec provenance contrôlée
premier profil PublicNode Mainnet standard
backward V1/V2 strict
```
Elle profite aussi du changement de phase de la release pour remettre le plan `016` et la validation `012` dans un état maintenable. Les détails historiques ne sont pas supprimés : ils restent dans les deltas immuables `deltas/0.2.9/`.
## 3. Config Transport V3
### 3.1 Schema
`config/schemas/std.transport.schema.json` devient `urn:ksp:schema:std.transport:v3` et conserve trois branches strictes :
```text
V1 = HTTP-only
V2 = HTTP + WebSocket
V3 = HTTP + WebSocket + Yellowstone gRPC optionnel par profil
```
V3 ajoute :
```text
grpc_defaults
grpc_endpoints[]?
```
`grpc_defaults` mappe uniquement des knobs déjà possédés par `YellowstoneGrpcSessionSettings` : timeouts, reconnect, channel capacities et max message sizes. Les bounds des filtres Subscribe restent un contrat Transport et ne sont pas transformés en configuration arbitraire.
Chaque endpoint gRPC contient :
```text
name
enabled
provider
cluster
protocol = solana_yellowstone
url
metadata[]?
secret_metadata[]?
session?
```
### 3.2 Mapping Config -> Transport
`ResolvedTransportConfig` ajoute :
```text
grpc_settings()
into_all_transport_settings()
```
L'accesseur historique suivant est volontairement inchangé :
```text
into_transport_settings() -> (HttpTransportSettings, Option<WsTransportSettings>)
```
Cela évite une rupture source silencieuse des consumers V2.
V1 et V2 retournent toujours `None` pour gRPC. Un profil V3 qui n'a pas de `grpc_endpoints` retourne également `None` au lieu d'inventer un Transport gRPC vide.
### 3.3 Protocol vs provider
Config exige actuellement :
```text
protocol = solana_yellowstone
```
Le champ `provider` reste descriptif et mappe vers `YellowstoneGrpcProviderName`.
Donc :
```text
PublicNode != protocole
PublicNode != nouvelle façade Transport
PublicNode = environnement d'exécution du standard Yellowstone
```
Toute future divergence provider doit rester un delta N3 explicite.
### 3.4 Metadata et provenance
Deux classes sont matérialisées :
```text
metadata
secret_metadata
```
Règles avant mapping :
```text
metadata + provenance KSP_SECRET_*/KSPB_SECRET_* -> reject
secret_metadata sans provenance secret -> reject
secret_metadata + provenance non-secret env -> reject
secret_metadata + provenance secret -> accept
```
Les segments littéraux autour d'un placeholder secret restent permis. Config possède la provenance et la projection sûre ; Transport reçoit ensuite seulement les `YellowstoneGrpcMetadataEntry` déjà classifiées.
Aucun nom d'environnement n'est lu dans Transport.
## 4. Profil PublicNode Mainnet
La surface publique réauditée le 2026-08-24 confirme Yellowstone gRPC Solana Mainnet et affiche :
```text
solana-yellowstone-grpc.publicnode.com:443
```
`YellowstoneGrpcEndpointUrl` exige une URL `http/https`; le document Config représente donc ce même endpoint TLS comme `https://solana-yellowstone-grpc.publicnode.com:443`.
Le document committé ajoute :
```text
profile_id = publicnode_mainnet
provider = publicnode
cluster = mainnet-beta
protocol = solana_yellowstone
metadata = aucune
```
Le profil générique `mainnet_public` reste distinct et ne reçoit pas implicitement un endpoint PublicNode.
PublicNode expose également Solana Testnet gRPC, mais le hostname exact n'a pas été obtenu depuis une source suffisamment autoritative/indexable pendant cette tranche. Aucun endpoint supposé n'est versionné. `pre.012` réaudite cette valeur avant tout profil/smoke Testnet.
## 5. Tests ajoutés/étendus
La fixture V3 couvre :
```text
mapping gRPC valide
merge grpc_defaults + session overrides
metadata publique issue de KSP_PUBLIC_*
secret_metadata issue de KSP_SECRET_*
safe_value avec segment secret redacted
rejet public <- secret
rejet secret <- public
```
Le config committé couvre le profil `publicnode_mainnet` et vérifie la construction d'un `YellowstoneGrpcTransportSettings` valide sans URL dans son `Debug`.
Le canari public API inclut les nouveaux accessors sans supprimer les anciens.
## 6. Consolidation plan/validation
`docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md` et `docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md` sont réorganisés autour de :
```text
état courant
scope/architecture stabilisés
matrices protocole encore normatives
Config V3
PublicNode
lifecycle/replay
historique compact des gates fermés
forecast restant
critères de clôture
```
Les longues sections append-only propres à chaque prerelease sont remplacées par une table compacte renvoyant aux deltas immuables. Aucun delta historique n'est modifié.
Les tableaux Markdown sont reformattés selon la convention JetBrains RustRover utilisée dans le workspace : largeur calculée sur la cellule la plus large et un espace de padding autour du contenu.
## 7. Documentation Config synchronisée
La README et l'USAGE de `ksp-config-lib`, ainsi que les commentaires du registre, sont mis à jour pour ne plus présenter `std.transport` comme un document uniquement V2 HTTP + WebSocket.
La documentation finale spécifique Transport/Yellowstone reste un gate de `pre.012`.
## 8. Fichiers modifiés/ajoutés
```text
Cargo.toml
config/std.transport.json
config/schemas/std.transport.schema.json
crates/ksp-config-lib/README.md
crates/ksp-config-lib/USAGE.md
crates/ksp-config-lib/src/lib.rs
crates/ksp-config-lib/src/registry.rs
crates/ksp-config-lib/src/transport.rs
crates/ksp-config-lib/tests/public_api.rs
crates/ksp-config-lib/unit_tests/transport.rs
crates/ksp-config-lib/unit_tests/fixtures_v3/std.transport.json
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
deltas/0.2.9/pre.011.md
```
Aucune dépendance Cargo n'est ajoutée ou modifiée.
## 9. Validation source disponible avant livraison
Exécuté dans l'environnement de préparation :
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
JSON Schema Draft 2020-12 meta-validation: PASS
config/std.transport.json against V3 schema: PASS
fixture V2 against V3 schema: PASS
fixture V1 against V3 schema: PASS
fixture V3 against V3 schema: PASS
```
Cargo/Rustfmt ne sont pas disponibles dans l'environnement de préparation. Aucune compilation ou test Cargo n'est donc déclaré réussi pour `pre.011` avant gate opérateur.
## 10. Gate opérateur requis
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-config-lib
cargo test -p ksp-config-lib --test public_api
cargo test -p ksp-config-lib --test ownership
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
```
Attentes de candidate :
```text
Config unit 113 attendus (110 baseline + 3 V3)
Config public API 15 attendus
Config ownership 5 attendus
Transport unit >= 383
Transport public API >= 49
Transport completeness >= 43
Transport doctests 4
workspace dependencies 3
Clippy aucun warning
workspace PASS
```
Ces valeurs ne deviennent des résultats qu'après exécution opérateur.
## 11. Suite
Après fermeture de `pre.011` :
```text
pre.012 PublicNode live + Testnet endpoint re-audit + compliance finale + cargo graphs + docs/prompt suivant
rel.001 publication stable stricte
```
Si `pre.012` ne peut pas fermer proprement tous les gates dans une seule tranche, continuer avec `pre.013+` plutôt que compacter artificiellement la clôture.

View File

@@ -0,0 +1,103 @@
# Delta `0.2.9-pre.012-fix.001` — séparation normative de la fermeture
## Base requise
```text
livraison : 0.2.9-pre.012
Cargo : 0.2.9-pre.12
```
Ce correctif est purement documentaire/normatif. Conformément à `VER-ID-008`, `workspace.package.version` reste `0.2.9-pre.12`.
## Objectif
Éviter que les dernières prereleases d'une release mélangent smokes/live, réconciliation README/USAGE/validation et préparation CHANGELOG/ROADMAP/prompt.
La fermeture standard devient :
```text
pre.N-2 gate technique/live si nécessaire
pre.N-1 réconciliation documentaire finale
pre.N prompt suivant + CHANGELOG + ROADMAP uniquement
rel.001 publication stable
```
Les fichiers mécaniques `Cargo.toml` et delta restent autorisés lorsqu'ils sont exigés par le workflow.
## Preuve opérateur `pre.012` reçue
Le log opérateur du 2026-08-24 confirme :
```text
fmt/audit/check/clippy PASS sans warning
Markdown tables PASS, 87 tableaux / 252 fichiers
Config unit 113/113
Config public API 15/15
Config ownership 5/5
Transport unit 383/383
Transport public API 49/49
Transport completeness 43/43
Transport doctests 4/4
workspace dependencies 3/3
cargo test --workspace PASS
PublicNode live smoke non exécuté, test ignored
cargo tree final non présent dans le log fourni
```
## Décisions normatives
- la dernière prerelease avant `rel.NNN` est limitée fonctionnellement au prompt de la release suivante, `CHANGELOG.md` et `ROADMAP.md` ;
- la prerelease précédente possède la réconciliation finale des README/USAGE, plans, validations et références durables ;
- lorsqu'un smoke/live final existe, il possède une prerelease technique dédiée avant la réconciliation documentaire ;
- si aucun smoke n'est requis, seule cette tranche est omise ; les deux dernières responsabilités restent séparées ;
- un `fix` reste local au couloir de sa prerelease et n'absorbe jamais une autre responsabilité de fermeture ;
- si un défaut d'un couloir antérieur apparaît tard, une nouvelle prerelease dédiée est ouverte et les couloirs postérieurs sont rejoués ;
- `rel.NNN` ne sert pas de rattrapage technique ou documentaire.
## Recalibrage `0.2.9`
```text
pre.013 smoke PublicNode Mainnet + graphes Cargo finaux
pre.014 réconciliation plan/validation/README/USAGE/références
pre.015 finalisation prompt 0.2.10 + CHANGELOG + ROADMAP
rel.001 publication stable
```
Le prompt et les README/USAGE déjà préparés en `pre.012` ne sont pas supprimés de l'historique : ils seront respectivement finalisés en `pre.015` et réconciliés en `pre.014`.
## Fichiers modifiés
```text
docs/rules/PROMPT_STRUCTURE.md
docs/rules/VERSION_WORKFLOW.md
docs/rules/FILE_CONTRACTS.md
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
```
## Fichier ajouté
```text
deltas/0.2.9/pre.012-fix.001.md
```
## Fichiers supprimés
Aucun.
## Validations à exécuter pour ce fix
```bash
python3 scripts/audit_markdown_tables.py docs/rules/PROMPT_STRUCTURE.md docs/rules/VERSION_WORKFLOW.md docs/rules/FILE_CONTRACTS.md docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md deltas/0.2.9/pre.012-fix.001.md
```
L'audit Rust/Cargo complet n'est pas requis par ce fix documentaire lui-même ; le prochain gate technique est `pre.013`.
## Validations non exécutées dans ce delta
```text
smoke PublicNode Mainnet live
cargo tree final
```
Elles appartiennent désormais explicitement à `pre.013`.

257
deltas/0.2.9/pre.012.md Normal file
View File

@@ -0,0 +1,257 @@
<!-- file: deltas/0.2.9/pre.012.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.012` — fermeture technique Yellowstone + PublicNode + documentation
## Statut
Candidate de fermeture technique `0.2.9` construite sur `0.2.9-pre.011`, dont le gate opérateur est entièrement vert.
Cette tranche ne change pas la sémantique runtime Yellowstone fermée en `pre.010`. Elle ajoute le smoke live PublicNode Mainnet opt-in, ferme le réaudit documentaire final, synchronise README/USAGE, prépare le prompt `0.2.10 — OrbitFlare Yellowstone gRPC` et rend la mise en forme des tableaux Markdown normative et mécaniquement vérifiable.
## Version technique
```text
workspace.package.version = 0.2.9-pre.12
```
## Base opérateur confirmée
`pre.011` a été validée avec :
```text
cargo fmt --all PASS
scripts/audit_rust_workspace_rules.py PASS, 0 export candidate
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS sans warning
Config unit 113/113
Config public API 15/15
Config ownership 5/5
Transport unit 383/383
Transport public API 49/49
Transport release completeness 43/43
Transport doctests 4/4
workspace dependencies 3/3
cargo test --workspace PASS
```
## Réaudit Yellowstone final
Le réaudit du 2026-08-24 distingue désormais explicitement les numéros de `master` et les releases GitHub publiées :
```text
latest release GitHub indexée v14.2.2+solana.4.1.0
master yellowstone-grpc-geyser 15.1.2
master yellowstone-grpc-client 13.3.0
master yellowstone-grpc-proto 12.6.0
master Agave Geyser / storage 4.2.0
master Tonic / Prost 0.14.x
```
La surface N2 utilisée par KSP reste matériellement compatible : service `Geyser`, `Subscribe`, sept unary retenus, `SubscribeRequest` courant et neuf variantes `SubscribeUpdate`. `SubscribeDeshred` reste hors scope.
La licence du subtree `yellowstone-grpc-proto` reste déclarée Apache-2.0 dans `LICENSING.md` upstream.
## PublicNode Mainnet
Ajout d'un smoke Transport pur :
```text
crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs
```
Le smoke :
```text
est ignored par défaut
construit ses settings programmatiquement
n'utilise ni Config ni variable d'environnement
n'utilise aucun credential
cible https://solana-yellowstone-grpc.publicnode.com:443
ouvre le channel TLS Yellowstone standard
appelle GetVersion
appelle GetSlot avec commitment confirmed
```
Il ne crée aucune façade `PublicNodeGrpc*` et réutilise le contrat provider-neutral de `0.2.9`.
## PublicNode Testnet
Le service Testnet gRPC est annoncé par PublicNode, mais l'hostname exact n'est toujours pas disponible dans une source PublicNode suffisamment autoritative et exploitable.
Décision :
```text
aucun hostname déduit
aucun profil Testnet
aucun smoke Testnet
statut documentaire = EXTERNAL BLOCK / NOT GUESSED
```
Cette limite externe ne bloque pas la fondation Mainnet et ne doit pas être transformée en endpoint supposé.
## Règles des tableaux Markdown
`docs/rules/RULES_DOCUMENTATION.md` formalise désormais :
```text
pipe littéral ou échappé interdit dans le contenu d'une cellule
pipe réservé aux séparateurs structurels
largeur de colonne déterminée par le contenu le plus large
tous les séparateurs verticaux alignés
exactement un espace de marge autour du contenu le plus large
cellules plus courtes complétées uniquement pour conserver l'alignement
ligne séparatrice dimensionnée exactement à la colonne
tableau entier réaligné lorsqu'une ligne est touchée
```
Ajout du canari :
```text
scripts/audit_markdown_tables.py
```
Il audite les fichiers ou répertoires Markdown explicitement fournis, ignore les blocs de code fenced et rejette les tables qui ne respectent pas le contrat KSP.
La version de `docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md` corrigée manuellement par l'opérateur avant le commit `pre.011` a été conservée comme base ; `pre.012` ne réintroduit pas l'ancien format.
## Normalisation documentaire rétroactive modifiable
Le premier audit étendu de la nouvelle règle a révélé 26 écarts historiques dans neuf documents **modifiables** : principalement du padding antérieur au contrat actuel et un tableau HTTP utilisant des pipes échappés dans ses cellules.
Ces documents ont été normalisés sans changer leurs décisions :
```text
docs/formats/KSPWALLET_V1.md
docs/formats/KSPWALLET_V2.md
docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md
docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md
docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md
docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md
docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md
docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
```
Dans le plan HTTP `008`, les anciens `\|` utilisés pour exprimer une alternative ont été reformulés avec `/`, conformément à la nouvelle interdiction des pipes internes.
Un scan du repository a encore signalé uniquement onze écarts de padding dans d'anciens deltas `0.1.4`. Ils ne sont **pas** modifiés : les deltas déjà publiés sont immuables. `DOC-TABLE-006` formalise cette priorité ; tous les nouveaux deltas restent soumis au format courant.
## Documentation Transport
`README.md` et `USAGE.md` documentent maintenant la foundation Yellowstone complète :
```text
settings/channel/unary/Subscribe
reconnect/replay prudent
Config V3
PublicNode Mainnet
smoke live opt-in
limite Testnet non inventée
```
## Plan et validation
Le plan `016` et la validation `012` restent structurés par état courant plutôt que par appendices successifs.
Mises à jour principales :
```text
pre.011 fermé par gate opérateur
pre.012 active candidate
récconciliation master vs release GitHub
PublicNode Testnet fermé comme limite externe documentée
smoke Mainnet ajouté mais non déclaré exécuté
cargo tree final explicitement pending operator
workspace final explicitement pending operator
```
## Prompt suivant
Ajout :
```text
prompts/015-V0_2_10_START_PROMPT.md
```
La release suivante active est `0.2.10 — OrbitFlare Yellowstone gRPC`.
Le prompt impose un `pre.001` audit/sizing avant code provider et cible notamment les questions ouvertes suivantes :
```text
IP whitelist vs X_TOKEN vs control-plane X-ORBIT-KEY
endpoints régionaux / Devnet / transport security
support réel des unary/replay/from_slot
heartbeat proactif recommandé par OrbitFlare
réutilisation N1/N2 sans second actor/client
Config V3 et secret ownership
smoke architecture-safe
```
`0.2.11` reste Helius LaserStream gRPC.
## Fichiers ajoutés ou modifiés
```text
Cargo.toml
docs/rules/RULES_DOCUMENTATION.md
scripts/audit_markdown_tables.py
crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs
crates/ksp-onchain-transport-lib/README.md
crates/ksp-onchain-transport-lib/USAGE.md
docs/formats/KSPWALLET_V1.md
docs/formats/KSPWALLET_V2.md
docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md
docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md
docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md
docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md
docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md
docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
prompts/000-README.md
prompts/015-V0_2_10_START_PROMPT.md
deltas/0.2.9/pre.012.md
```
## Gate local disponible dans l'environnement de préparation
Exécuté avant livraison de l'archive :
```text
python3 scripts/audit_rust_workspace_rules.py
General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
Markdown table audit: clean (87 table(s), 116 file(s))
```
Cargo et Rustfmt ne sont pas disponibles dans l'environnement de préparation ; aucune commande Cargo n'est donc déclarée réussie pour la candidate `pre.012`.
## Gate opérateur requis
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-config-lib
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
cargo test --workspace
cargo tree -p ksp-onchain-transport-lib
cargo tree -p ksp-onchain-transport-lib --duplicates
cargo tree --duplicates
```
Smoke live séparé :
```bash
cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture
```
Si tous les gates locaux sont verts et que le smoke Mainnet est consigné, `0.2.9` peut passer à sa tranche de publication stable. Un défaut découvert dans cette candidate crée `pre.012-fix.001` ; l'historique précédent reste immuable.

View File

@@ -0,0 +1,182 @@
<!-- file: deltas/0.2.9/pre.013-fix.001.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.013-fix.001` — smoke PublicNode streaming + Testnet
## 1. Base
```text
livraison : 0.2.9-pre.013
Cargo : 0.2.9-pre.13
```
Le gate déterministe opérateur de `pre.013` est vert jusqu'au smoke live :
```text
cargo fmt --all PASS
Rust workspace audit clean / 0 export candidate
Markdown table audit clean, 87 tableaux / 254 fichiers
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS sans warning
Transport unit 383/383
Transport public API 49/49
Transport release completeness 43/43
Transport doctests 4/4
workspace dependency canary 3/3
```
Le smoke initial atteint le service PublicNode Mainnet mais `GetVersion` retourne `PERMISSION_DENIED`.
## 2. Correction du diagnostic
Cette réponse ne suffit pas à conclure qu'un `x-token` PublicNode est requis.
La surface Yellowstone upstream permet à un opérateur de désactiver les méthodes unary indépendamment du streaming `Subscribe`. PublicNode expose son service comme gateway Yellowstone gRPC public et aucune procédure d'authentification n'est retenue comme contrat KSP tant qu'elle n'est pas établie par une source provider suffisamment forte.
Conséquence :
```text
aucun x-token inventé
aucun secret PublicNode ajouté à Config
aucune variable d'environnement d'authentification ajoutée
smoke recentré sur Subscribe, fonctionnalité centrale de 0.2.9
```
## 3. Endpoint Testnet acquis
L'opérateur a confirmé le host/port Yellowstone gRPC Testnet :
```text
solana-testnet-yellowstone-grpc.publicnode.com:443
```
KSP utilise l'URI TLS :
```text
https://solana-testnet-yellowstone-grpc.publicnode.com:443
```
## 4. Config V3
`config/std.transport.json` conserve le profil Mainnet PublicNode sans metadata d'authentification et ajoute :
```text
profile_id = publicnode_testnet
provider = publicnode
cluster = testnet
protocol = solana_yellowstone
url = https://solana-testnet-yellowstone-grpc.publicnode.com:443
metadata = none
```
Comme pour le profil Mainnet, les axes HTTP/WS du profil restent des transports Solana standards distincts du provider gRPC.
Le canari Config existant couvre désormais les deux profils PublicNode et impose explicitement :
```text
metadata gRPC vide
provider/cluster/URL exacts
validation Transport verte
Debug sans URL endpoint
```
## 5. Smoke Transport pur
Le smoke opt-in ne teste plus les méthodes unary provider-dépendantes `GetVersion` / `GetSlot`.
Il ouvre maintenant la surface standard réellement visée par la release :
```text
TLS
Subscribe bidirectionnel
filtre slots standard
réception d'un YellowstoneSubscribeUpdate::Slot
slot > 0
close borné
```
Deux cas live sont présents :
```text
PublicNode Mainnet Yellowstone Subscribe sans metadata d'authentification
PublicNode Testnet Yellowstone Subscribe sans metadata d'authentification
```
Le test reste dans `ksp-onchain-transport-lib`, construit ses settings programmatiquement et ne dépend ni de Config ni d'un secret opérateur.
## 6. Signal de version
Le correctif modifie une config runtime et un test Rust :
```text
workspace.package.version = 0.2.9-pre.13.fix.1
commit attendu = v0.2.9-pre.013-fix.001
```
## 7. Gate opérateur
### 7.1 Déterministe
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-config-lib
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
```
Attendus minima :
```text
Config unit 113
Config public API 15
Config ownership 5
Transport unit 383
Transport public API 49
Transport release completeness 43
Transport doctests 4
workspace dependencies 3
```
### 7.2 Smoke live
```bash
cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture
```
Attendu :
```text
2 passed
0 failed
0 ignored
```
Aucun token ou secret ne doit être nécessaire pour ce smoke. Si `Subscribe` retourne à son tour `PERMISSION_DENIED`, l'hypothèse d'une authentification provider redevient ouverte et doit alors être établie avant tout nouveau correctif Config.
### 7.3 Graphes et workspace
```bash
cargo tree -p ksp-onchain-transport-lib
cargo tree -p ksp-onchain-transport-lib --duplicates
cargo tree --duplicates
cargo test --workspace
```
## 8. Frontières
Ce fix reste strictement dans le couloir technique/live de `pre.013` :
```text
aucun README/USAGE modifié
aucun plan/validation modifié
aucun CHANGELOG/ROADMAP modifié
aucun prompt modifié
aucun provider-specific engine ajouté
aucun secret committé
```
La prochaine tranche reste `0.2.9-pre.014` de réconciliation documentaire, uniquement après fermeture verte de ce fix.

View File

@@ -0,0 +1,176 @@
<!-- file: deltas/0.2.9/pre.013-fix.002.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.013-fix.002` — authentification PublicNode Yellowstone
## 1. Base
```text
livraison : 0.2.9-pre.013-fix.001
Cargo : 0.2.9-pre.13.fix.1
```
Le second smoke opérateur invalide l'hypothèse retenue dans `fix.001` :
```text
Mainnet SubscribeOpen -> PERMISSION_DENIED
Testnet SubscribeOpen -> PERMISSION_DENIED
```
Les deux endpoints sont donc atteignables au niveau TLS/gRPC mais refusent aussi la surface `Subscribe` sans autorisation.
## 2. Diagnostic retenu
Les éléments concordants sont désormais suffisants pour traiter PublicNode Yellowstone comme une surface à personal token :
```text
provider = publicnode
auth wire = metadata ASCII x-token
secret = oui
URL = sans credential
```
La page PublicNode expose les endpoints Yellowstone Mainnet/Testnet. Une capture récente de la page PublicNode expose en outre un lien `Get token` vers le flow Allnodes `https://www.allnodes.com/publicnode`. Des implémentations Yellowstone récentes visant explicitement PublicNode rapportent le même comportement `PERMISSION_DENIED` sans personal token et utilisent `x-token`.
Le provisioning effectif du token reste un gate externe/opérateur : KSP ne génère, ne devine et ne versionne aucun credential provider.
## 3. Config V3
Les profils :
```text
publicnode_mainnet
publicnode_testnet
```
conservent leurs URLs TLS sans secret et ajoutent chacun :
```json
"secret_metadata": [
{
"key": "x-token",
"value": "${KSP_SECRET_PUBLICNODE_GRPC_X_TOKEN}"
}
]
```
L'absence de `KSP_SECRET_PUBLICNODE_GRPC_X_TOKEN` rend donc volontairement le profil PublicNode explicite non résolvable au lieu d'envoyer silencieusement une requête anonyme vouée à `PERMISSION_DENIED`.
`.env.example` inventorie la variable sans valeur réelle.
## 4. Canari Config
Le canari des profils PublicNode fournit un canary secret via `ConfigEnvironment`, vérifie :
```text
Mainnet/Testnet exacts
metadata count = 1
metadata key = x-token
metadata class = secret
Debug = sans canary
Transport validation = PASS
```
Les règles génériques V3 de provenance `secret_metadata` restent inchangées.
## 5. Smoke Transport pur
Le smoke reste dans `ksp-onchain-transport-lib` et ne dépend pas de Config.
Pour ne pas violer la frontière `Transport -X-> process environment` et pour ne pas exposer le token dans les arguments/process list, le harness lit une seule ligne secrète depuis son `stdin`. Le même personal token est réutilisé par les deux tests Mainnet/Testnet via un `OnceLock`, puis injecté avec :
```text
YellowstoneGrpcMetadataEntry::secret("x-token", ...)
```
Le token n'est jamais loggé et un canari runtime vérifie qu'il n'apparaît pas dans `Debug`.
Le smoke continue de valider :
```text
TLS
Subscribe bidirectionnel
filtre slots
première update Slot
slot > 0
close borné
```
## 6. Signal de version
Le correctif modifie Config runtime et un test Rust :
```text
workspace.package.version = 0.2.9-pre.13.fix.2
commit attendu = v0.2.9-pre.013-fix.002
```
## 7. Gate opérateur
### 7.1 Déterministe
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-config-lib
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
```
### 7.2 Provisioning externe
Obtenir le personal token PublicNode via le flow Allnodes/PublicNode :
```text
https://www.allnodes.com/publicnode
```
Ne jamais le committer, le coller dans l'URL ou le passer comme argument de processus.
### 7.3 Smoke live
Le token est saisi dans une variable shell non exportée, puis envoyé au harness via stdin :
```bash
read -rsp 'PublicNode Yellowstone x-token: ' PUBLICNODE_TOKEN
echo
printf '%s\n' "$PUBLICNODE_TOKEN" | cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture --test-threads=1
unset PUBLICNODE_TOKEN
```
Attendu après provisioning valide :
```text
2 passed
0 failed
0 ignored
```
Si le provisioning provider est inaccessible ou si le token reste refusé, le smoke reste `EXTERNAL BLOCK`; ne pas transformer ce blocage en faux PASS.
### 7.4 Graphes et workspace
```bash
cargo tree -p ksp-onchain-transport-lib
cargo tree -p ksp-onchain-transport-lib --duplicates
cargo tree --duplicates
cargo test --workspace
```
## 8. Frontières
Ce fix reste strictement dans le couloir technique/live de `pre.013` :
```text
aucun README/USAGE modifié
aucun plan/validation modifié
aucun CHANGELOG/ROADMAP modifié
aucun prompt modifié
aucun provider-specific engine ajouté
aucun credential committé
```
La prochaine tranche reste `0.2.9-pre.014` de réconciliation documentaire uniquement après fermeture du gate technique, ou après qualification explicite d'un blocage provider externe.

View File

@@ -0,0 +1,116 @@
<!-- file: deltas/0.2.9/pre.013-fix.003.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.013-fix.003` — credentials PublicNode par réseau + fermeture live bornée
## 1. Base
```text
0.2.9-pre.013-fix.002
```
Cette tranche reste strictement dans le couloir technique/live de fin de release. Elle ne modifie ni README/USAGE, ni plan/validation, ni CHANGELOG/ROADMAP, ni le prompt de la release suivante.
## 2. Motif opérateur
Deux exécutions live de `yellowstone_publicnode_smoke` avec deux personal tokens PublicNode différents ont montré que l'hypothèse de `fix.002` « un token commun Mainnet/Testnet » n'est pas suffisamment générale :
- avec le premier token fourni au smoke, `SubscribeOpen` a retourné `PERMISSION_DENIED` sur Mainnet et Testnet ;
- avec le second token, Mainnet a encore retourné `PERMISSION_DENIED` ;
- avec ce même second token, Testnet a franchi `SubscribeOpen`, reçu le slot attendu, puis `close()` a retourné le timeout borné de cinq secondes parce que le provider n'a pas terminé son half-close avant la deadline KSP.
Ces observations ne suffisent pas à affirmer que PublicNode documente des tokens obligatoirement liés à un réseau. Elles imposent en revanche que KSP puisse fournir des credentials distincts à Mainnet et Testnet et que le smoke ne suppose plus leur interchangeabilité.
## 3. Version technique
`workspace.package.version` devient :
```text
0.2.9-pre.13.fix.3
```
## 4. Config V3 — secrets PublicNode séparés
Le placeholder partagé est supprimé au profit de deux variables indépendantes :
```text
KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN
KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN
```
Le profil `publicnode_mainnet` consomme uniquement la première ; le profil `publicnode_testnet` consomme uniquement la seconde. Les deux restent injectées comme metadata gRPC secrète `x-token`, jamais dans l'URL.
`.env.example` inventorie les deux noms sans valeur réelle. Le test Config construit chaque profil avec uniquement son propre canary d'environnement afin de vérifier cette séparation.
## 5. Smoke PublicNode
Le smoke Transport reste programmatique et indépendant de Config/env KSP. Son stdin contient maintenant exactement deux lignes :
```text
ligne 1 = token Mainnet
ligne 2 = token Testnet
```
Les deux valeurs sont lues une seule fois dans un `OnceLock` et sont affectées explicitement au réseau correspondant. L'ordre d'exécution des deux tests n'influence donc pas l'association token/réseau.
Après ouverture de `Subscribe` et réception d'un `YellowstoneSubscribeUpdate::Slot` avec `slot > 0`, la fermeture live accepte deux résultats :
1. fermeture gracieuse `Ok(())` du provider ;
2. `ERROR_CODE_TIMEOUT` produit par la deadline KSP de half-close.
Le second résultat ne masque pas un échec fonctionnel : il n'est accepté qu'après réception prouvée d'un slot et un `timeout` externe de sept secondes vérifie que `session.close()` reste lui-même borné. Toute autre erreur de fermeture reste un échec du smoke.
Le runtime Yellowstone n'est pas modifié par cette adaptation provider/live ; ses contrats déterministes de fermeture restent inchangés.
## 6. Gate opérateur ciblé
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-config-lib
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
```
Puis, avec les deux personal tokens réellement générés :
```bash
read -rsp 'PublicNode Mainnet Yellowstone x-token: ' PUBLICNODE_MAINNET_TOKEN
echo
read -rsp 'PublicNode Testnet Yellowstone x-token: ' PUBLICNODE_TESTNET_TOKEN
echo
printf '%s\n%s\n' "$PUBLICNODE_MAINNET_TOKEN" "$PUBLICNODE_TESTNET_TOKEN" \
| cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture
unset PUBLICNODE_MAINNET_TOKEN PUBLICNODE_TESTNET_TOKEN
```
Attendu :
```text
2 passed
0 failed
0 ignored
```
Puis les graphes et le gate workspace de fermeture technique :
```bash
cargo tree -p ksp-onchain-transport-lib
cargo tree -p ksp-onchain-transport-lib --duplicates
cargo tree --duplicates
cargo test --workspace
```
## 7. Fichiers modifiés/ajoutés
```text
.env.example
Cargo.toml
config/std.transport.json
crates/ksp-config-lib/unit_tests/transport.rs
crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs
deltas/0.2.9/pre.013-fix.003.md
```

View File

@@ -0,0 +1,136 @@
<!-- file: deltas/0.2.9/pre.013-fix.004.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.013-fix.004` — conformité Clippy du smoke PublicNode final
## 1. Base
```text
0.2.9-pre.013-fix.003
```
Cette tranche reste strictement dans le couloir technique/live de fin de release. Elle ne modifie ni Config, ni README/USAGE, ni plan/validation, ni CHANGELOG/ROADMAP, ni le prompt de la release suivante.
## 2. Motif opérateur
La validation opérateur de `pre.013-fix.003` confirme le comportement live final du smoke PublicNode :
```text
Mainnet Subscribe + slot PASS
Testnet Subscribe + slot PASS
2 passed / 0 failed
```
Elle confirme aussi :
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py clean / 0 export candidate / clean
cargo check --workspace PASS
cargo test -p ksp-config-lib 113 unit + 5 ownership + 15 public API PASS
cargo test -p ksp-onchain-transport-lib 383 unit + 49 public API + 43 completeness + 4 doctests PASS
cargo test -p ksp-core-lib --test workspace_dependencies 3/3 PASS
cargo test --workspace PASS
cargo tree -p ksp-onchain-transport-lib exécuté
cargo tree -p ksp-onchain-transport-lib --duplicates exécuté
cargo tree --duplicates exécuté
```
Le seul gate en échec est :
```text
cargo clippy --workspace --all-targets
```
Clippy relève onze violations localisées exclusivement dans `tests/yellowstone_publicnode_smoke.rs` : huit `clippy::expect_used`, deux `clippy::question_mark_used` et un `clippy::implicit_return`.
Aucun défaut fonctionnel du moteur Yellowstone, de Config ou du live PublicNode n'est révélé par ce gate.
## 3. Version technique
`workspace.package.version` devient :
```text
0.2.9-pre.13.fix.4
```
## 4. Correction du smoke
Le smoke conserve exactement le même contrat fonctionnel et opérateur que `fix.003` :
- deux lignes de token sur stdin, Mainnet puis Testnet ;
- metadata secrète `x-token` ;
- ouverture TLS puis `Subscribe` ;
- attente bornée d'un `YellowstoneSubscribeUpdate::Slot` ;
- `slot > 0` obligatoire ;
- fermeture gracieuse ou timeout KSP de half-close accepté uniquement après réception du slot ;
- aucune lecture Config/env KSP depuis Transport ;
- aucun secret dans URL, arguments ou diagnostics.
Seule la forme Rust du harness est corrigée :
- tous les `expect()` des helpers non annotés `#[test]` sont remplacés par des `match` explicites ;
- les deux propagations `?` sont remplacées par des branches `Result::Err` explicites ;
- la sortie du `async` d'attente de slot utilise `return update.slot()` ;
- la borne externe de fermeture est contrôlée par `match` plutôt que `expect()` ;
- aucun `#[allow(clippy::expect_used)]`, `#[allow(clippy::question_mark_used)]` ou `#[allow(clippy::implicit_return)]` n'est ajouté.
Le runtime Transport n'est pas modifié.
## 5. Inspection finale du graphe déjà exécutée
Le graphe Transport communiqué par l'opérateur confirme la stack gRPC attendue :
```text
yellowstone-grpc-proto 12.6.0
tonic 0.14.6
tonic-prost 0.14.6
prost 0.14.4
prost-types 0.14.4
```
Aucun `yellowstone-grpc-client` n'est introduit comme seconde abstraction runtime. Les doublons signalés par Cargo concernent notamment les contextes build/runtime et des transitifs génériques ; le graphe communiqué ne montre pas de seconde version concurrente de `tonic` ou `prost` dans la stack Yellowstone Transport.
## 6. Gate opérateur ciblé à rejouer
Le correctif ne changeant que le harness live et la version Cargo, le gate minimal est :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test --workspace
```
Puis le smoke live final, avec les credentials déjà validés :
```bash
read -rsp 'PublicNode Mainnet Yellowstone x-token: ' PUBLICNODE_MAINNET_TOKEN
echo
read -rsp 'PublicNode Testnet Yellowstone x-token: ' PUBLICNODE_TESTNET_TOKEN
echo
printf '%s\n%s\n' "$PUBLICNODE_MAINNET_TOKEN" "$PUBLICNODE_TESTNET_TOKEN" \
| cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture
unset PUBLICNODE_MAINNET_TOKEN PUBLICNODE_TESTNET_TOKEN
```
Attendu :
```text
2 passed
0 failed
0 ignored
```
Les trois `cargo tree` n'ont pas besoin d'être rejoués si le payload appliqué correspond exactement à ce delta : aucune dépendance ni feature Cargo n'est modifiée.
## 7. Fichiers modifiés/ajoutés
```text
Cargo.toml
crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs
deltas/0.2.9/pre.013-fix.004.md
```

183
deltas/0.2.9/pre.013.md Normal file
View File

@@ -0,0 +1,183 @@
# Delta `0.2.9-pre.013` — gate technique/live final
## Base requise
```text
livraison : 0.2.9-pre.012-fix.001
Cargo : 0.2.9-pre.12
```
## Identité de la tranche
```text
livraison : 0.2.9-pre.013
Cargo : 0.2.9-pre.13
```
Cette prerelease applique le couloir technique/live introduit par `pre.012-fix.001`. Elle ne porte aucune réconciliation README/USAGE/plan/validation et aucune finalisation CHANGELOG/ROADMAP/prompt.
## Objectif
Fermer les dernières preuves techniques externes de `0.2.9` avant la réconciliation documentaire :
```text
smoke PublicNode Mainnet Yellowstone gRPC live
inspection du graphe Transport direct
inspection des dépendances dupliquées Transport
inspection des dépendances dupliquées workspace
```
Le smoke existe déjà dans la base sous :
```text
crates/ksp-onchain-transport-lib/tests/yellowstone_publicnode_smoke.rs
```
Il reste opt-in et ne contient aucun secret. `pre.013` ne crée pas un second smoke et ne déplace pas ce test vers Config.
## Vérification externe de l'endpoint
Le 2026-08-24, la surface officielle PublicNode expose toujours Solana Mainnet Yellowstone GRPC avec le host/port :
```text
solana-yellowstone-grpc.publicnode.com:443
```
KSP conserve l'URI TLS :
```text
https://solana-yellowstone-grpc.publicnode.com:443
```
Aucun endpoint Testnet n'est ajouté ou déduit dans cette tranche.
## Modifications
```text
Cargo.toml
workspace.package.version : 0.2.9-pre.12 -> 0.2.9-pre.13
deltas/0.2.9/pre.013.md
ajout du gate technique/live et des commandes opérateur
```
Aucun code runtime, test, schema, config, README, USAGE, plan, validation, CHANGELOG, ROADMAP ou prompt n'est modifié.
## Baseline déterministe héritée de `pre.012`
Le log opérateur reçu avant l'ouverture de cette tranche confirme :
```text
cargo fmt --all PASS
Rust workspace audit clean / 0 export candidate
Markdown table audit clean
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS sans warning
Config unit 113/113
Config public API 15/15
Config ownership 5/5
Transport unit 383/383
Transport public API 49/49
Transport release completeness 43/43
Transport doctests 4/4
workspace dependency canary 3/3
cargo test --workspace PASS
```
Le test PublicNode était alors uniquement compilé et listé comme `ignored`; cela ne vaut pas exécution live.
## Gate opérateur `pre.013`
### 1. Gate déterministe après changement de version
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test -p ksp-onchain-transport-lib
cargo test -p ksp-core-lib --test workspace_dependencies
```
Seuils de non-régression :
```text
Transport unit 383 minimum
Transport public API 49 minimum
Transport release completeness 43 minimum
Transport doctests 4 minimum
workspace dependencies 3 minimum
```
### 2. Smoke PublicNode Mainnet live
```bash
cargo test -p ksp-onchain-transport-lib --test yellowstone_publicnode_smoke -- --ignored --nocapture
```
Le verdict attendu est :
```text
1 passed
0 failed
0 ignored
```
Le test doit démontrer sans credentials :
```text
TLS vers PublicNode Mainnet
GetVersion réussi
GetSlot Confirmed réussi
slot > 0
```
Un échec de compilation, mapping, TLS, protocole ou appel unary imputable à KSP ouvre `pre.013-fix.NNN`.
Un échec démontré comme incident externe PublicNode/réseau est documenté comme `EXTERNAL BLOCK` et n'est jamais maquillé en PASS.
### 3. Graphes Cargo finaux
```bash
cargo tree -p ksp-onchain-transport-lib
cargo tree -p ksp-onchain-transport-lib --duplicates
cargo tree --duplicates
```
Points à vérifier :
```text
pas de yellowstone-grpc-client runtime
pas de dépendance Transport -> Config
pas de tracing direct Transport
Tonic KSP-owned conservé
yellowstone-grpc-proto présent comme proto officiel
aucune nouvelle duplication introduite par pre.013
```
### 4. Workspace complet
Après le smoke et l'inspection des graphes :
```bash
cargo test --workspace
```
## Critère de fermeture
`pre.013` est fermée seulement lorsque :
```text
gate déterministe vert sans warning
smoke PublicNode Mainnet réellement exécuté et vert, ou incident externe précisément établi
graphes Cargo inspectés
workspace complet vert
aucun correctif documentaire de clôture mélangé dans cette tranche
```
La tranche suivante reste :
```text
0.2.9-pre.014 — réconciliation documentaire finale
```

140
deltas/0.2.9/pre.014.md Normal file
View File

@@ -0,0 +1,140 @@
<!-- file: deltas/0.2.9/pre.014.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.014` — réconciliation documentaire finale Yellowstone
## 1. Base
```text
0.2.9-pre.013-fix.004
```
Cette tranche est exclusivement le couloir de réconciliation documentaire défini par `VER-LIFECYCLE-006`. Elle ne modifie aucun code, test, schema, config exécutable, README global de publication, `CHANGELOG.md`, `ROADMAP.md` ou prompt suivant.
## 2. Version technique
Comme toute prerelease non-fix, la version Cargo est synchronisée mécaniquement :
```text
workspace.package.version = 0.2.9-pre.14
```
Aucune sémantique runtime ne change.
## 3. Preuve technique réconciliée
Le couloir `pre.013` est fermé par `pre.013-fix.004` avec :
```text
cargo fmt --all PASS
Rust workspace audit PASS, 0 export candidate
Markdown audit PASS, 87 tableaux / 258 fichiers
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS sans warning
Transport unit 383/383 PASS
Transport public API 49/49 PASS
Transport release completeness 43/43 PASS
Transport doctests 4/4 PASS
cargo test --workspace PASS
PublicNode Mainnet Subscribe + Slot PASS
PublicNode Testnet Subscribe + Slot PASS
live smoke total 2/2 PASS
```
Les graphes Cargo finaux ont été inspectés dans le couloir technique. `fix.004` n'ayant modifié ni dépendance ni feature, leur verdict reste applicable :
```text
yellowstone-grpc-proto 12.6.0
tonic / tonic-prost 0.14.6
prost / prost-types 0.14.4
yellowstone-grpc-client absent du runtime KSP
```
## 4. PublicNode final
Endpoints validés :
```text
Mainnet https://solana-yellowstone-grpc.publicnode.com:443
Testnet https://solana-testnet-yellowstone-grpc.publicnode.com:443
```
Les essais live ont montré qu'une connexion sans credential peut atteindre le service mais recevoir `PERMISSION_DENIED`. Les profils finaux utilisent donc une metadata gRPC secrète `x-token`.
Config conserve :
```text
KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN
KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN
```
L'opérateur a prouvé qu'un même personal token peut être utilisé pour les deux endpoints. Les variables restent distinctes par flexibilité opérationnelle ; KSP ne transforme pas ce choix en affirmation provider de tokens network-scoped.
Le smoke Transport pur reste indépendant de Config : ses deux tokens sont fournis sur stdin au harness et ne sont jamais versionnés, placés dans l'URL ou journalisés.
## 5. Smoke final documenté
Le smoke PublicNode ne teste plus `GetVersion/GetSlot` comme preuve provider, car ces unary peuvent être restreintes indépendamment du service de streaming. La preuve représentative de `0.2.9` est :
```text
TLS
-> metadata secret x-token
-> Subscribe
-> filtre slots
-> YellowstoneSubscribeUpdate::Slot
-> slot > 0
-> fermeture bornée
```
Mainnet et Testnet sont tous deux passés en live. Un timeout KSP de half-close n'est accepté par le harness qu'après réception prouvée du slot ; le runtime conserve son erreur explicite de timeout.
## 6. Documents réconciliés
```text
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
crates/ksp-onchain-transport-lib/README.md
crates/ksp-onchain-transport-lib/USAGE.md
docs/000-README.md
```
Les anciennes affirmations devenues fausses sont supprimées : PublicNode sans credential, smoke unary Mainnet, Testnet non confirmé/non versionné et gate `pre.013` encore pending.
## 7. Frontière avec `pre.015`
Cette tranche ne touche pas :
```text
prompts/015-V0_2_10_START_PROMPT.md
CHANGELOG.md
ROADMAP.md
```
Ces trois surfaces appartiennent exclusivement à `pre.015`, hors `Cargo.toml` et son delta mécanique.
Si un défaut de code, smoke, Config ou autre document durable est découvert après le gate de `pre.014`, il ne doit pas être glissé dans `pre.015` : une nouvelle prerelease du couloir approprié doit être ouverte conformément aux règles de fermeture.
## 8. Fichiers modifiés/ajoutés
```text
Cargo.toml
docs/000-README.md
docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md
docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md
crates/ksp-onchain-transport-lib/README.md
crates/ksp-onchain-transport-lib/USAGE.md
deltas/0.2.9/pre.014.md
```
## 9. Gate opérateur
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test --workspace
```
Aucun smoke live ni `cargo tree` n'est à rejouer dans `pre.014` si le delta est appliqué exactement : aucune surface technique ou dépendance n'est modifiée.

104
deltas/0.2.9/pre.015.md Normal file
View File

@@ -0,0 +1,104 @@
<!-- file: deltas/0.2.9/pre.015.md -->
<!-- version: 1 -->
# Delta `0.2.9-pre.015` — préparation de publication minimale
## 1. Base
```text
0.2.9-pre.014
```
Cette tranche est exclusivement le couloir de préparation de publication défini par les règles de fermeture KSP. Elle ne corrige aucun README, USAGE, plan, validation, règle, code, test, schema ou config.
## 2. Gate `pre.014` reçu
Le gate opérateur de `0.2.9-pre.014` reçu le 2026-08-24 confirme :
```text
cargo fmt --all PASS
Rust workspace audit PASS, 0 export candidate
Markdown audit PASS, 87 tableaux / 259 fichiers
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS sans warning
cargo test --workspace PASS
```
`pre.014` n'ayant modifié que la version Cargo et la documentation, le smoke PublicNode 2/2 et les graphes Cargo fermés dans `pre.013` n'ont pas été rejoués.
## 3. Version technique
```text
workspace.package.version = 0.2.9-pre.15
```
La modification est mécanique ; aucune sémantique runtime ne change.
## 4. `CHANGELOG.md`
Le changelog stable est préparé pour `0.2.9 — Yellowstone gRPC standard/provider-neutral` avec les faits consolidés : moteur Yellowstone partagé, surface Solana standard, Config V3, PublicNode Mainnet/Testnet avec `x-token`, smoke live 2/2, non-régressions et graphes finaux.
## 5. `ROADMAP.md`
`0.2.9` passe à l'état réalisé/validé dans la roadmap globale. La séquence suivante reste :
```text
0.2.10 OrbitFlare Yellowstone gRPC
0.2.11 Helius LaserStream gRPC
0.2.12 off-chain price transport
0.2.13 Price Desk + intégration prix Wallet Desk
0.2.14 interface/wire foundation
0.2.15 program-api foundation
```
## 6. Prompt `0.2.10`
Le draft `prompts/015-V0_2_10_START_PROMPT.md` est finalisé. Il intègre l'état réel de `v0.2.9` :
```text
PublicNode Mainnet + Testnet validés
metadata x-token secrète via Config
Subscribe -> Slot 2/2 PASS
unary standard != entitlement garanti chez chaque provider
```
Sa prévision souple est également corrigée pour réserver explicitement les trois couloirs de fermeture applicables :
```text
gate technique/live final éventuel
réconciliation documentaire finale
préparation de publication minimale prompt + CHANGELOG + ROADMAP
```
Le prompt rappelle qu'un `fix` reste local à son couloir et que `rel.001` ne sert jamais de rattrapage.
## 7. Fichiers modifiés/ajoutés
```text
Cargo.toml
CHANGELOG.md
ROADMAP.md
prompts/015-V0_2_10_START_PROMPT.md
deltas/0.2.9/pre.015.md
```
Aucun autre fichier ne doit changer dans cette tranche.
## 8. Gate opérateur
Comme la tranche ne modifie aucun code/runtime/config/schema :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test --workspace
```
Aucun smoke live ni `cargo tree` n'est à rejouer si le delta est appliqué exactement.
## 9. Suite
Si ce gate est vert, `0.2.9-rel.001` est la seule étape restante. Elle est limitée à la mécanique de publication stable : version Cargo `0.2.9`, delta `rel.001`, commit final et tag `v0.2.9`, sans rattrapage technique ou documentaire.

181
deltas/0.2.9/rel.001.md Normal file
View File

@@ -0,0 +1,181 @@
<!-- file: deltas/0.2.9/rel.001.md -->
<!-- version: 1 -->
# Delta `0.2.9-rel.001` — publication stable Yellowstone gRPC standard/provider-neutral
## 1. Base requise
Base directe attendue :
```text
0.2.9-pre.015
workspace.package.version = 0.2.9-pre.15
```
Commit attendu pour cette livraison :
```text
v0.2.9-rel.001
```
Tag stable attendu après validation :
```text
v0.2.9
```
## 2. Responsabilité de `rel.001`
Cette livraison effectue uniquement la mécanique de publication stable définie par `VER-LIFECYCLE-012`.
Elle :
- passe `workspace.package.version` de `0.2.9-pre.15` à `0.2.9` ;
- ajoute le présent delta `rel.001` ;
- ne corrige ni code, ni test, ni config/schema, ni README/USAGE, ni plan/validation, ni règle, ni prompt, ni `CHANGELOG.md`, ni `ROADMAP.md`.
Tout défaut appartenant à un couloir antérieur doit renvoyer vers une prerelease appropriée ; `rel.001` n'est jamais une tranche de rattrapage.
## 3. Gate final de `pre.015` acquis
Le gate opérateur reçu le **25 août 2026** sur `0.2.9-pre.15` est intégralement vert :
```text
cargo fmt --all PASS
python3 scripts/audit_rust_workspace_rules.py PASS
General Rust rule audit clean
Rust export completeness audit 0 candidate(s)
KSP workspace Rust rule audit clean
python3 scripts/audit_markdown_tables.py PASS, 87 tableaux / 260 fichiers
cargo check --workspace PASS
cargo clippy --workspace --all-targets PASS sans warning
cargo test --workspace PASS
```
Les suites Transport restent notamment :
```text
unit 383 passed
public API 49 passed
release completeness 43 passed
doctests 4 passed
```
Le gate technique/live final avait déjà été fermé dans le couloir `pre.013` avec :
```text
PublicNode Mainnet Yellowstone Subscribe -> Slot PASS
PublicNode Testnet Yellowstone Subscribe -> Slot PASS
smoke live total 2/2 PASS
```
Les graphes Cargo finaux avaient également été inspectés dans ce couloir. `pre.014` et `pre.015` n'ont ensuite modifié aucune dépendance ni source runtime.
## 4. Version stable publiée
La version Cargo devient :
```text
workspace.package.version = 0.2.9
```
La release stable publiée est :
```text
0.2.9 — Yellowstone gRPC standard/provider-neutral
```
Les acquis fonctionnels et documentaires ont été finalisés avant `rel.001`, notamment :
```text
moteur Yellowstone gRPC partagé et provider-neutral
surface Solana Yellowstone standard typée
Config Transport V3
PublicNode Mainnet + Testnet
metadata secrète x-token
Subscribe -> Slot live 2/2 PASS
reconnect/replay borné avec observabilité conservative
pas de promesse exactly-once / lossless / gapless globale
```
## 5. Fichiers ajoutés
```text
deltas/0.2.9/rel.001.md
```
## 6. Fichiers modifiés
```text
Cargo.toml
```
## 7. Fichiers supprimés
```text
aucun
```
## 8. Validations exécutées pour préparer le delta
Contrôles statiques de l'overlay :
```text
Cargo.toml version 0.2.9
payload rel.001 2 fichiers exactement
aucun fichier de rattrapage présent
```
Aucune compilation supplémentaire n'est exécutée par la préparation d'archive elle-même ; la preuve complète de `pre.015` est acquise ci-dessus et le gate stable final reste à exécuter après application du delta.
## 9. Validation stable après application
Le changement étant limité au signal de version Cargo et au présent delta, exécuter avant commit/tag :
```bash
cargo fmt --all
python3 scripts/audit_rust_workspace_rules.py
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
cargo check --workspace
cargo clippy --workspace --all-targets
cargo test --workspace
```
Les smokes PublicNode et les `cargo tree` n'ont pas à être rejoués si l'overlay appliqué est exactement `0.2.9-rel.001`, car aucune dépendance, Config, source ou logique de smoke ne change.
## 10. Commit et tag stable
Après succès du gate stable :
```text
commit : v0.2.9-rel.001
tag : v0.2.9
```
Aucun tag intermédiaire `rel.001` n'est requis.
Commandes Git typiques :
```bash
git add Cargo.toml deltas/0.2.9/rel.001.md
git commit -m "v0.2.9-rel.001"
git tag v0.2.9
git push
git push origin v0.2.9
```
## 11. Suite
Après publication du tag stable `v0.2.9`, ouvrir :
```text
0.2.10-pre.001 — OrbitFlare Yellowstone gRPC
```
avec :
```text
prompts/015-V0_2_10_START_PROMPT.md
```
La première tranche `0.2.10-pre.001` reste un audit/brainstorming/sizing avant toute implémentation lourde et sa prévision souple réserve explicitement les couloirs technique/live, documentaire et publication.

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
<!-- file: docs/formats/KSPWALLET_V1.md --> <!-- file: docs/formats/KSPWALLET_V1.md -->
<!-- version: 14 --> <!-- version: 15 -->
# `.kspwallet` V1 — spécification du format natif Wallet KSP # `.kspwallet` V1 — spécification du format natif Wallet KSP

View File

@@ -1,5 +1,5 @@
<!-- file: docs/formats/KSPWALLET_V2.md --> <!-- file: docs/formats/KSPWALLET_V2.md -->
<!-- version: 3 --> <!-- version: 4 -->
# `.kspwallet` V2 — spécification du wire binaire natif KSP # `.kspwallet` V2 — spécification du wire binaire natif KSP

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/000-README.md --> <!-- file: docs/plans/000-README.md -->
<!-- version: 59 --> <!-- version: 60 -->
# Plans KSP # Plans KSP
@@ -24,6 +24,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
- [`013-V0_2_6_WALLET_DESK_PLAN.md`](013-V0_2_6_WALLET_DESK_PLAN.md) — plan historique clôturé de la release stable `0.2.6 — Wallet Desk`, ouvert par `pre.001`, étendu en `pre.015``pre.017` au wire binaire `.kspwallet` V2, aux APIs multi-version et à la migration V1 -> V2, puis fermé par `pre.018`/`fix.001` avec le runtime Tauri packagé et le build final vert avant publication `rel.001`. - [`013-V0_2_6_WALLET_DESK_PLAN.md`](013-V0_2_6_WALLET_DESK_PLAN.md) — plan historique clôturé de la release stable `0.2.6 — Wallet Desk`, ouvert par `pre.001`, étendu en `pre.015``pre.017` au wire binaire `.kspwallet` V2, aux APIs multi-version et à la migration V1 -> V2, puis fermé par `pre.018`/`fix.001` avec le runtime Tauri packagé et le build final vert avant publication `rel.001`.
- [`014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md`](014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md) — plan historique clôturé de la release stable `0.2.7 — WebSocket Solana standard`, ouvert par `pre.001`, exécuté jusquà `pre.014`, corrigé documentairement par `pre.014-fix.001` puis publié par `rel.001`; il conserve linventaire officiel 18 méthodes, le modèle session/subscription, le threat model, les preuves de compliance/smoke/dépendances et la préparation de `0.2.8`. - [`014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md`](014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md) — plan historique clôturé de la release stable `0.2.7 — WebSocket Solana standard`, ouvert par `pre.001`, exécuté jusquà `pre.014`, corrigé documentairement par `pre.014-fix.001` puis publié par `rel.001`; il conserve linventaire officiel 18 méthodes, le modèle session/subscription, le threat model, les preuves de compliance/smoke/dépendances et la préparation de `0.2.8`.
- [`015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md`](015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md) — plan historique clôturé de la release stable `0.2.8 — Helius LaserStream WebSocket`, ouvert par `pre.001`, fermé techniquement/documentairement par `pre.011` puis publié par `rel.001`; il conserve la surface finale Helius `7 standard + transaction`, heartbeat/Config/secrets, lhistorique des fixes heartbeat, la stratégie live architecture-safe, les graphes Cargo finaux et la préparation du prompt `0.2.9`. - [`015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md`](015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md) — plan historique clôturé de la release stable `0.2.8 — Helius LaserStream WebSocket`, ouvert par `pre.001`, fermé techniquement/documentairement par `pre.011` puis publié par `rel.001`; il conserve la surface finale Helius `7 standard + transaction`, heartbeat/Config/secrets, lhistorique des fixes heartbeat, la stratégie live architecture-safe, les graphes Cargo finaux et la préparation du prompt `0.2.9`.
- [`016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md`](016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md) — plan actif de `0.2.9 — Yellowstone gRPC standard/provider-neutral`, ouvert par `pre.001`; il fixe la stratégie `yellowstone-grpc-proto + Tonic KSP`, la surface `Subscribe + 7 unary`, lexclusion de `SubscribeDeshred`, Config V3, les règles replay/backpressure et les smokes gratuits PublicNode puis OrbitFlare.
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre. Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md --> <!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
<!-- version: 85 --> <!-- version: 88 -->
# Séquence des releases fonctionnelles KSP # Séquence des releases fonctionnelles KSP
@@ -363,11 +363,13 @@ Par défaut :
0.2.6 Wallet Desk 0.2.6 Wallet Desk
0.2.7 standard Solana WebSocket 0.2.7 standard Solana WebSocket
0.2.8 Helius LaserStream WebSocket 0.2.8 Helius LaserStream WebSocket
0.2.9 Yellowstone gRPC standard foundation 0.2.9 Yellowstone gRPC engine + Solana standard + PublicNode
0.2.10 off-chain price transport 0.2.10 OrbitFlare Yellowstone gRPC
0.2.11 price visualization desk + intégration prix dans Wallet Desk 0.2.11 Helius LaserStream gRPC
0.2.12 interface/wire foundation 0.2.12 off-chain price transport
0.2.13 program-api foundation 0.2.13 price visualization desk + intégration prix dans Wallet Desk
0.2.14 interface/wire foundation
0.2.15 program-api foundation
``` ```
`0.2.1-pre.001` a appliqué le gate de sizing et refusé le scope HTTP monolithique initial : l'inventaire du 2026-08-17 contient 52 méthodes courantes et 14 méthodes Deprecated historiques. Ce premier delta avait réparti la couverture typée sur `0.2.1``0.2.6`. `0.2.1-pre.001-fix.001` recalibre ensuite les 48 méthodes restantes sur trois releases complémentaires `0.2.2``0.2.4`, soit trois sessions nominales au maximum si chaque release utilise sa session complète. Si une release se clôt plus vite que prévu, la même session peut enchaîner la suivante après clôture complète de la précédente et nouveau gate de sizing positif. Un Wallet Desk utile doit pouvoir lire le solde du wallet : `getBalance` fait donc partie des quatre canaris de la foundation `0.2.1`, avant Wallet. Les transports live arrivent ensuite ; Interface/Program restent préparés avant les couches de données décodées. `0.2.1-pre.001` a appliqué le gate de sizing et refusé le scope HTTP monolithique initial : l'inventaire du 2026-08-17 contient 52 méthodes courantes et 14 méthodes Deprecated historiques. Ce premier delta avait réparti la couverture typée sur `0.2.1``0.2.6`. `0.2.1-pre.001-fix.001` recalibre ensuite les 48 méthodes restantes sur trois releases complémentaires `0.2.2``0.2.4`, soit trois sessions nominales au maximum si chaque release utilise sa session complète. Si une release se clôt plus vite que prévu, la même session peut enchaîner la suivante après clôture complète de la précédente et nouveau gate de sizing positif. Un Wallet Desk utile doit pouvoir lire le solde du wallet : `getBalance` fait donc partie des quatre canaris de la foundation `0.2.1`, avant Wallet. Les transports live arrivent ensuite ; Interface/Program restent préparés avant les couches de données décodées.
@@ -471,29 +473,57 @@ Le réaudit final Helius du 23 août 2026 retient `helius_laserstream` comme pro
`pre.010` ferme README/USAGE, la stratégie smoke live architecture-safe et les graphes Cargo finaux ; `pre.011` ferme la candidate documentaire et prépare [`../../prompts/014-V0_2_9_START_PROMPT.md`](../../prompts/014-V0_2_9_START_PROMPT.md). `0.2.8-rel.001` publie ensuite cette surface stable sans nouvelle capacité runtime. Aucun SDK Helius, aucune dépendance gRPC et aucun replay historique WebSocket ne sont introduits. `pre.010` ferme README/USAGE, la stratégie smoke live architecture-safe et les graphes Cargo finaux ; `pre.011` ferme la candidate documentaire et prépare [`../../prompts/014-V0_2_9_START_PROMPT.md`](../../prompts/014-V0_2_9_START_PROMPT.md). `0.2.8-rel.001` publie ensuite cette surface stable sans nouvelle capacité runtime. Aucun SDK Helius, aucune dépendance gRPC et aucun replay historique WebSocket ne sont introduits.
### `0.2.9` — Yellowstone gRPC standard ### `0.2.9` — Yellowstone gRPC engine + standard Solana + PublicNode
Mission : introduire un backend Yellowstone standard/provider-neutral. Mission : construire un moteur client Yellowstone gRPC partagé dans `ksp-onchain-transport-lib`, exposer la façade Solana Yellowstone standard puis valider une première intégration concrète PublicNode Mainnet/Testnet sans recopier le moteur.
Le `pre.001` est un gate de sizing : inventorier toute la surface normative cible et scinder la release avant implémentation si sa clôture dans une session paraît incertaine. Le `pre.001` est un gate de sizing : chaque prerelease vise 1520 minutes de travail effectif et la release complète doit rester clôturable dans une seule session. Si cette clôture devient incertaine, `0.2.9` est scindée avant implémentation lourde supplémentaire.
Les profiles/adapters Helius/Triton/ERPC/Chainstack/Shyft sont reportés après les priorités fondatrices. La séparation cible reprend le modèle WebSocket : moteur physique partagé -> protocole standard -> adaptations provider. PublicNode est la première intégration provider. Le moteur n'est jamais dupliqué ; le standard n'est réutilisé que pour les capacités réellement compatibles. Une couche provider peut restreindre le standard ou porter des extensions wire/lifecycle explicites.
### `0.2.10` / `0.2.11` — Off-chain price + app ### `0.2.10``0.2.11` — providers Yellowstone retenus
`0.2.10` introduit `ksp-offchain-transport-lib` avec au minimum SOL/USD et SOL/EUR via une abstraction indépendante du premier provider. ```text
0.2.10 OrbitFlare Yellowstone gRPC
0.2.11 Helius LaserStream gRPC
```
`0.2.11` ajoute une petite application desk de visualisation/validation. Après stabilisation de cette application spécialisée, la même release doit intégrer la capacité de prix offchain dans `ksp-app-wallet-desk` sans dupliquer la récupération/normalisation appartenant au composant spécialisé. Chaque release réutilise le moteur de `0.2.9`, audite son delta avec Yellowstone upstream et matérialise uniquement les restrictions/extensions réelles du provider.
### TODO/IDEAS — providers Yellowstone en attente
Aucune release n'est réservée pour :
```text
TODO eRPC
TODO Triton
TODO Alchemy
TODO QuickNode
TODO Chainstack
IDEAS Tatum
IDEAS Shyft
IDEAS Solinfra
IDEAS NodeFlare
IDEAS autres providers à réauditer
```
Ces providers ne déplacent pas la séquence active et ne reçoivent ni façade, ni Config profile, ni smoke tant qu'une décision explicite d'implémentation n'est pas prise.
### `0.2.12` / `0.2.13` — Off-chain price + app
`0.2.12` introduit `ksp-offchain-transport-lib` avec au minimum SOL/USD et SOL/EUR via une abstraction indépendante du premier provider.
`0.2.13` ajoute une petite application desk de visualisation/validation. Après stabilisation de cette application spécialisée, la même release doit intégrer la capacité de prix offchain dans `ksp-app-wallet-desk` sans dupliquer la récupération/normalisation appartenant au composant spécialisé.
Metadata HTTP/IPFS/Arweave viendra au premier besoin Metadata réel. Metadata HTTP/IPFS/Arweave viendra au premier besoin Metadata réel.
### `0.2.12` — Interface foundation ### `0.2.14` — Interface foundation
`ksp-interface-lib` devient la façade wire officielle et expose une API publique wire utilisable par les implementations officielles et externes. `ksp-interface-lib` devient la façade wire officielle et expose une API publique wire utilisable par les implementations officielles et externes.
Aucune `ksp-interface-api` séparée n'est retenue pour l'instant. Aucune `ksp-interface-api` séparée n'est retenue pour l'instant.
### `0.2.13` — Program API foundation ### `0.2.15` — Program API foundation
Introduire `ksp-program-api`, sans suffixe `-lib`, comme contrat d'extension Program. Introduire `ksp-program-api`, sans suffixe `-lib`, comme contrat d'extension Program.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md --> <!-- file: docs/plans/005-V0_1_3_CONFIG_FOUNDATION_PLAN.md -->
<!-- version: 19 --> <!-- version: 20 -->
# Plan `0.1.3` — Configuration foundation # Plan `0.1.3` — Configuration foundation

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md --> <!-- file: docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md -->
<!-- version: 28 --> <!-- version: 29 -->
# Plan `0.1.4` — `ksp-app-config-desk` # Plan `0.1.4` — `ksp-app-config-desk`

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md --> <!-- file: docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md -->
<!-- version: 12 --> <!-- version: 13 -->
# `0.2.1-pre.001` — plan `ksp-onchain-transport-lib` HTTP Solana foundation # `0.2.1-pre.001` — plan `ksp-onchain-transport-lib` HTTP Solana foundation
@@ -230,30 +230,30 @@ Colonnes : nom, catégorie, paramètres/config à préserver, forme de résultat
| Méthode | Catégorie | Params / config | Résultat | Statut | Runtime | Bot3 | Release | Tests minimaux | Source | | Méthode | Catégorie | Params / config | Résultat | Statut | Runtime | Bot3 | Release | Tests minimaux | Source |
|-------------------------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------|---------------------------------|----------|---------------|---------|--------------------------------------------------------------|------------------------------------------------------------------------------| |-------------------------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------|---------------------------------|----------|---------------|---------|--------------------------------------------------------------|------------------------------------------------------------------------------|
| `getAccountInfo` | Accounts | pubkey ; config? {commitment, encoding, dataSlice, minContextSlot} | RpcResponse<Account\|null> | Stable | Supporté | typed_adapter | `0.2.2` | params/encoding/dataSlice ; account/null ; RPC error | [Solana](https://solana.com/docs/rpc/http/getaccountinfo) | | `getAccountInfo` | Accounts | pubkey ; config? {commitment, encoding, dataSlice, minContextSlot} | RpcResponse<Account / null> | Stable | Supporté | typed_adapter | `0.2.2` | params/encoding/dataSlice ; account/null ; RPC error | [Solana](https://solana.com/docs/rpc/http/getaccountinfo) |
| `getBalance` | Accounts | pubkey ; config? {commitment, minContextSlot} | RpcResponse<u64> | Stable | Supporté | typed_adapter | `0.2.1` | params/config ; value u64 ; context/error | [Solana](https://solana.com/docs/rpc/http/getbalance) | | `getBalance` | Accounts | pubkey ; config? {commitment, minContextSlot} | RpcResponse<u64> | Stable | Supporté | typed_adapter | `0.2.1` | params/config ; value u64 ; context/error | [Solana](https://solana.com/docs/rpc/http/getbalance) |
| `getLargestAccounts` | Accounts | config? {commitment, filter, sortResults} | RpcResponse<[LargestAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | config/filter/sort ; array | [Solana](https://solana.com/docs/rpc/http/getlargestaccounts) | | `getLargestAccounts` | Accounts | config? {commitment, filter, sortResults} | RpcResponse<[LargestAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | config/filter/sort ; array | [Solana](https://solana.com/docs/rpc/http/getlargestaccounts) |
| `getMinimumBalanceForRentExemption` | Accounts | data_len ; config? {commitment} | u64 | Stable | Supporté | typed_adapter | `0.2.2` | data length/config ; u64 | [Solana](https://solana.com/docs/rpc/http/getminimumbalanceforrentexemption) | | `getMinimumBalanceForRentExemption` | Accounts | data_len ; config? {commitment} | u64 | Stable | Supporté | typed_adapter | `0.2.2` | data length/config ; u64 | [Solana](https://solana.com/docs/rpc/http/getminimumbalanceforrentexemption) |
| `getMultipleAccounts` | Accounts | pubkeys <= 100 ; config? {commitment, minContextSlot, dataSlice, encoding} | RpcResponse<[Account\|null]> | Stable | Supporté | typed_adapter | `0.2.2` | max/params ; mixed account/null ; encoding | [Solana](https://solana.com/docs/rpc/http/getmultipleaccounts) | | `getMultipleAccounts` | Accounts | pubkeys <= 100 ; config? {commitment, minContextSlot, dataSlice, encoding} | RpcResponse<[Account / null]> | Stable | Supporté | typed_adapter | `0.2.2` | max/params ; mixed account/null ; encoding | [Solana](https://solana.com/docs/rpc/http/getmultipleaccounts) |
| `getProgramAccounts` | Accounts | program pubkey ; config? {commitment, minContextSlot, withContext, encoding, dataSlice, filters, sortResults} | [KeyedAccount] ou RpcResponse<[KeyedAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | filters/dataSlice ; withContext false/true ; errors | [Solana](https://solana.com/docs/rpc/http/getprogramaccounts) | | `getProgramAccounts` | Accounts | program pubkey ; config? {commitment, minContextSlot, withContext, encoding, dataSlice, filters, sortResults} | [KeyedAccount] ou RpcResponse<[KeyedAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | filters/dataSlice ; withContext false/true ; errors | [Solana](https://solana.com/docs/rpc/http/getprogramaccounts) |
| `getTokenAccountBalance` | Tokens | token account ; config? {commitment} | RpcResponse<TokenAmount> | Stable | Supporté | typed_adapter | `0.2.2` | config ; token amount | [Solana](https://solana.com/docs/rpc/http/gettokenaccountbalance) | | `getTokenAccountBalance` | Tokens | token account ; config? {commitment} | RpcResponse<TokenAmount> | Stable | Supporté | typed_adapter | `0.2.2` | config ; token amount | [Solana](https://solana.com/docs/rpc/http/gettokenaccountbalance) |
| `getTokenAccountsByDelegate` | Tokens | delegate ; filter {mint\|programId} ; config? {commitment,minContextSlot,dataSlice,encoding} | RpcResponse<[KeyedAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | mint/programId exclusifs ; config ; array | [Solana](https://solana.com/docs/rpc/http/gettokenaccountsbydelegate) | | `getTokenAccountsByDelegate` | Tokens | delegate ; filter {mint / programId} ; config? {commitment,minContextSlot,dataSlice,encoding} | RpcResponse<[KeyedAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | mint/programId exclusifs ; config ; array | [Solana](https://solana.com/docs/rpc/http/gettokenaccountsbydelegate) |
| `getTokenAccountsByOwner` | Tokens | owner ; filter {mint\|programId} ; config? {commitment,minContextSlot,dataSlice,encoding} | RpcResponse<[KeyedAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | mint/programId exclusifs ; config ; array | [Solana](https://solana.com/docs/rpc/http/gettokenaccountsbyowner) | | `getTokenAccountsByOwner` | Tokens | owner ; filter {mint / programId} ; config? {commitment,minContextSlot,dataSlice,encoding} | RpcResponse<[KeyedAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | mint/programId exclusifs ; config ; array | [Solana](https://solana.com/docs/rpc/http/gettokenaccountsbyowner) |
| `getTokenLargestAccounts` | Tokens | mint ; config? {commitment} | RpcResponse<[TokenLargestAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | config ; array | [Solana](https://solana.com/docs/rpc/http/gettokenlargestaccounts) | | `getTokenLargestAccounts` | Tokens | mint ; config? {commitment} | RpcResponse<[TokenLargestAccount]> | Stable | Supporté | typed_adapter | `0.2.2` | config ; array | [Solana](https://solana.com/docs/rpc/http/gettokenlargestaccounts) |
| `getTokenSupply` | Tokens | mint ; config? {commitment} | RpcResponse<TokenAmount> | Stable | Supporté | typed_adapter | `0.2.2` | config ; TokenAmount | [Solana](https://solana.com/docs/rpc/http/gettokensupply) | | `getTokenSupply` | Tokens | mint ; config? {commitment} | RpcResponse<TokenAmount> | Stable | Supporté | typed_adapter | `0.2.2` | config ; TokenAmount | [Solana](https://solana.com/docs/rpc/http/gettokensupply) |
| `getFeeForMessage` | Transactions | message base64 ; config? {commitment,minContextSlot} | RpcResponse<u64\|null> | Stable | Supporté | typed_adapter | `0.2.3` | message/config ; fee/null ; error | [Solana](https://solana.com/docs/rpc/http/getfeeformessage) | | `getFeeForMessage` | Transactions | message base64 ; config? {commitment,minContextSlot} | RpcResponse<u64 / null> | Stable | Supporté | typed_adapter | `0.2.3` | message/config ; fee/null ; error | [Solana](https://solana.com/docs/rpc/http/getfeeformessage) |
| `getLatestBlockhash` | Transactions | config? {commitment,minContextSlot} | RpcResponse<{blockhash,lastValidBlockHeight}> | Stable | Supporté | typed_adapter | `0.2.3` | config ; object | [Solana](https://solana.com/docs/rpc/http/getlatestblockhash) | | `getLatestBlockhash` | Transactions | config? {commitment,minContextSlot} | RpcResponse<{blockhash,lastValidBlockHeight}> | Stable | Supporté | typed_adapter | `0.2.3` | config ; object | [Solana](https://solana.com/docs/rpc/http/getlatestblockhash) |
| `getRecentPrioritizationFees` | Transactions | pubkeys? <= 128 | [{slot,prioritizationFee}] | Stable | Supporté | typed_adapter | `0.2.3` | no params / addresses ; max ; array | [Solana](https://solana.com/docs/rpc/http/getrecentprioritizationfees) | | `getRecentPrioritizationFees` | Transactions | pubkeys? <= 128 | [{slot,prioritizationFee}] | Stable | Supporté | typed_adapter | `0.2.3` | no params / addresses ; max ; array | [Solana](https://solana.com/docs/rpc/http/getrecentprioritizationfees) |
| `getSignaturesForAddress` | Transactions | address ; config? {commitment,minContextSlot,limit,before,until} | [SignatureInfo] | Stable | Supporté | typed_adapter | `0.2.3` | pagination/config ; nullable memo/blockTime/status | [Solana](https://solana.com/docs/rpc/http/getsignaturesforaddress) | | `getSignaturesForAddress` | Transactions | address ; config? {commitment,minContextSlot,limit,before,until} | [SignatureInfo] | Stable | Supporté | typed_adapter | `0.2.3` | pagination/config ; nullable memo/blockTime/status | [Solana](https://solana.com/docs/rpc/http/getsignaturesforaddress) |
| `getSignatureStatuses` | Transactions | signatures <= 256 ; config? {searchTransactionHistory} | RpcResponse<[SignatureStatus\|null]> | Stable | Supporté | typed_adapter | `0.2.3` | max ; cache/history ; object/null | [Solana](https://solana.com/docs/rpc/http/getsignaturestatuses) | | `getSignatureStatuses` | Transactions | signatures <= 256 ; config? {searchTransactionHistory} | RpcResponse<[SignatureStatus / null]> | Stable | Supporté | typed_adapter | `0.2.3` | max ; cache/history ; object/null | [Solana](https://solana.com/docs/rpc/http/getsignaturestatuses) |
| `getTransaction` | Transactions | signature ; config? {commitment,maxSupportedTransactionVersion,encoding} ; legacy bare encoding deprecated | TransactionResponse\|null | Stable; forme legacy deprecated | Supporté | typed_adapter | `0.2.3` | config + legacy warning ; all encodings ; null ; error | [Solana](https://solana.com/docs/rpc/http/gettransaction) | | `getTransaction` | Transactions | signature ; config? {commitment,maxSupportedTransactionVersion,encoding} ; legacy bare encoding deprecated | TransactionResponse / null | Stable; forme legacy deprecated | Supporté | typed_adapter | `0.2.3` | config + legacy warning ; all encodings ; null ; error | [Solana](https://solana.com/docs/rpc/http/gettransaction) |
| `getTransactionCount` | Transactions | config? {commitment,minContextSlot} | u64 | Stable | Supporté | typed_adapter | `0.2.3` | config ; u64 | [Solana](https://solana.com/docs/rpc/http/gettransactioncount) | | `getTransactionCount` | Transactions | config? {commitment,minContextSlot} | u64 | Stable | Supporté | typed_adapter | `0.2.3` | config ; u64 | [Solana](https://solana.com/docs/rpc/http/gettransactioncount) |
| `isBlockhashValid` | Transactions | blockhash ; config? {commitment,minContextSlot} | RpcResponse<bool> | Stable | Supporté | typed_adapter | `0.2.3` | hash/config ; bool | [Solana](https://solana.com/docs/rpc/http/isblockhashvalid) | | `isBlockhashValid` | Transactions | blockhash ; config? {commitment,minContextSlot} | RpcResponse<bool> | Stable | Supporté | typed_adapter | `0.2.3` | hash/config ; bool | [Solana](https://solana.com/docs/rpc/http/isblockhashvalid) |
| `requestAirdrop` | Transactions | pubkey ; lamports ; config? {commitment,recentBlockhash} | signature | Stable | Supporté | typed_adapter | `0.2.3` | serialization ; signature/error ; no-resend policy | [Solana](https://solana.com/docs/rpc/http/requestairdrop) | | `requestAirdrop` | Transactions | pubkey ; lamports ; config? {commitment,recentBlockhash} | signature | Stable | Supporté | typed_adapter | `0.2.3` | serialization ; signature/error ; no-resend policy | [Solana](https://solana.com/docs/rpc/http/requestairdrop) |
| `sendTransaction` | Transactions | signed transaction ; config? {encoding,skipPreflight,preflightCommitment,maxRetries,minContextSlot} | signature | Stable | Supporté | typed_adapter | `0.2.3` | all configs ; RPC error ; timeout/no-resend | [Solana](https://solana.com/docs/rpc/http/sendtransaction) | | `sendTransaction` | Transactions | signed transaction ; config? {encoding,skipPreflight,preflightCommitment,maxRetries,minContextSlot} | signature | Stable | Supporté | typed_adapter | `0.2.3` | all configs ; RPC error ; timeout/no-resend | [Solana](https://solana.com/docs/rpc/http/sendtransaction) |
| `simulateTransaction` | Transactions | transaction ; config? {commitment,encoding,replaceRecentBlockhash,sigVerify,minContextSlot,innerInstructions,accounts} | RpcResponse<SimulationResult> | Stable | Supporté | typed_adapter | `0.2.3` | configs compatibles/incompatibles ; logs/accounts/null/error | [Solana](https://solana.com/docs/rpc/http/simulatetransaction) | | `simulateTransaction` | Transactions | transaction ; config? {commitment,encoding,replaceRecentBlockhash,sigVerify,minContextSlot,innerInstructions,accounts} | RpcResponse<SimulationResult> | Stable | Supporté | typed_adapter | `0.2.3` | configs compatibles/incompatibles ; logs/accounts/null/error | [Solana](https://solana.com/docs/rpc/http/simulatetransaction) |
| `getBlock` | Blocks | slot ; config? {commitment,encoding,transactionDetails,maxSupportedTransactionVersion,rewards} ; legacy bare encoding deprecated | BlockResponse\|null | Stable; forme legacy deprecated | Supporté | typed_adapter | `0.2.4` | config + legacy warning ; transactionDetails variants ; null | [Solana](https://solana.com/docs/rpc/http/getblock) | | `getBlock` | Blocks | slot ; config? {commitment,encoding,transactionDetails,maxSupportedTransactionVersion,rewards} ; legacy bare encoding deprecated | BlockResponse / null | Stable; forme legacy deprecated | Supporté | typed_adapter | `0.2.4` | config + legacy warning ; transactionDetails variants ; null | [Solana](https://solana.com/docs/rpc/http/getblock) |
| `getBlockCommitment` | Blocks | slot | {commitment:[u64]\|null,totalStake:u64} | Stable | Supporté | typed_adapter | `0.2.4` | slot ; commitment null/array | [Solana](https://solana.com/docs/rpc/http/getblockcommitment) | | `getBlockCommitment` | Blocks | slot | {commitment:[u64] / null,totalStake:u64} | Stable | Supporté | typed_adapter | `0.2.4` | slot ; commitment null/array | [Solana](https://solana.com/docs/rpc/http/getblockcommitment) |
| `getBlockHeight` | Blocks | config? {commitment,minContextSlot} | u64 | Stable | Supporté | typed_adapter | `0.2.4` | config ; u64 | [Solana](https://solana.com/docs/rpc/http/getblockheight) | | `getBlockHeight` | Blocks | config? {commitment,minContextSlot} | u64 | Stable | Supporté | typed_adapter | `0.2.4` | config ; u64 | [Solana](https://solana.com/docs/rpc/http/getblockheight) |
| `getBlockProduction` | Blocks | config? {commitment,identity,range} | RpcResponse<BlockProduction> | Stable | Supporté | typed_adapter | `0.2.4` | identity/range/config ; map | [Solana](https://solana.com/docs/rpc/http/getblockproduction) | | `getBlockProduction` | Blocks | config? {commitment,identity,range} | RpcResponse<BlockProduction> | Stable | Supporté | typed_adapter | `0.2.4` | identity/range/config ; map | [Solana](https://solana.com/docs/rpc/http/getblockproduction) |
| `getBlocks` | Blocks | startSlot ; endSlot? ou config? ; config? {commitment,minContextSlot} | [u64] | Stable | Supporté | typed_adapter | `0.2.4` | overload 1/2/3 params ; max range 500k ; empty | [Solana](https://solana.com/docs/rpc/http/getblocks) | | `getBlocks` | Blocks | startSlot ; endSlot? ou config? ; config? {commitment,minContextSlot} | [u64] | Stable | Supporté | typed_adapter | `0.2.4` | overload 1/2/3 params ; max range 500k ; empty | [Solana](https://solana.com/docs/rpc/http/getblocks) |
@@ -267,19 +267,19 @@ Colonnes : nom, catégorie, paramètres/config à préserver, forme de résultat
| `getEpochSchedule` | Cluster | aucun | EpochSchedule | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; object | [Solana](https://solana.com/docs/rpc/http/getepochschedule) | | `getEpochSchedule` | Cluster | aucun | EpochSchedule | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; object | [Solana](https://solana.com/docs/rpc/http/getepochschedule) |
| `getGenesisHash` | Cluster | aucun | string base58 | Stable | Supporté | typed_adapter | `0.2.1` | empty params ; hash string ; error | [Solana](https://solana.com/docs/rpc/http/getgenesishash) | | `getGenesisHash` | Cluster | aucun | string base58 | Stable | Supporté | typed_adapter | `0.2.1` | empty params ; hash string ; error | [Solana](https://solana.com/docs/rpc/http/getgenesishash) |
| `getHealth` | Cluster | aucun | "ok" ou RPC unhealthy error | Stable | Supporté | typed_adapter | `0.2.1` | healthy ; unhealthy RPC error | [Solana](https://solana.com/docs/rpc/http/gethealth) | | `getHealth` | Cluster | aucun | "ok" ou RPC unhealthy error | Stable | Supporté | typed_adapter | `0.2.1` | healthy ; unhealthy RPC error | [Solana](https://solana.com/docs/rpc/http/gethealth) |
| `getHighestSnapshotSlot` | Cluster | aucun | {full:u64,incremental:u64\|null} | Stable | Supporté | typed_adapter | `0.2.2` | incremental null ; no snapshot/error | [Solana](https://solana.com/docs/rpc/http/gethighestsnapshotslot) | | `getHighestSnapshotSlot` | Cluster | aucun | {full:u64,incremental:u64 / null} | Stable | Supporté | typed_adapter | `0.2.2` | incremental null ; no snapshot/error | [Solana](https://solana.com/docs/rpc/http/gethighestsnapshotslot) |
| `getIdentity` | Cluster | aucun | {identity:string} | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; identity | [Solana](https://solana.com/docs/rpc/http/getidentity) | | `getIdentity` | Cluster | aucun | {identity:string} | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; identity | [Solana](https://solana.com/docs/rpc/http/getidentity) |
| `getLeaderSchedule` | Cluster | slot? \| config? \| null ; config? {commitment,identity} | map identity->[slot_index] \| null | Stable | Supporté | typed_adapter | `0.2.2` | all overloads ; filter ; null | [Solana](https://solana.com/docs/rpc/http/getleaderschedule) | | `getLeaderSchedule` | Cluster | slot? / config? / null ; config? {commitment,identity} | map identity->[slot_index] / null | Stable | Supporté | typed_adapter | `0.2.2` | all overloads ; filter ; null | [Solana](https://solana.com/docs/rpc/http/getleaderschedule) |
| `getMaxRetransmitSlot` | Cluster | aucun | u64 | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; u64 | [Solana](https://solana.com/docs/rpc/http/getmaxretransmitslot) | | `getMaxRetransmitSlot` | Cluster | aucun | u64 | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; u64 | [Solana](https://solana.com/docs/rpc/http/getmaxretransmitslot) |
| `getMaxShredInsertSlot` | Cluster | aucun | u64 | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; u64 | [Solana](https://solana.com/docs/rpc/http/getmaxshredinsertslot) | | `getMaxShredInsertSlot` | Cluster | aucun | u64 | Stable | Supporté | typed_adapter | `0.2.2` | empty params ; u64 | [Solana](https://solana.com/docs/rpc/http/getmaxshredinsertslot) |
| `getSlot` | Cluster | config? {commitment,minContextSlot} | u64 | Stable | Supporté | typed_adapter | `0.2.2` | config ; u64 | [Solana](https://solana.com/docs/rpc/http/getslot) | | `getSlot` | Cluster | config? {commitment,minContextSlot} | u64 | Stable | Supporté | typed_adapter | `0.2.2` | config ; u64 | [Solana](https://solana.com/docs/rpc/http/getslot) |
| `getSlotLeader` | Cluster | config? {commitment,minContextSlot} | string pubkey | Stable | Supporté | typed_adapter | `0.2.2` | config ; pubkey | [Solana](https://solana.com/docs/rpc/http/getslotleader) | | `getSlotLeader` | Cluster | config? {commitment,minContextSlot} | string pubkey | Stable | Supporté | typed_adapter | `0.2.2` | config ; pubkey | [Solana](https://solana.com/docs/rpc/http/getslotleader) |
| `getSlotLeaders` | Cluster | startSlot ; limit 1..5000 | [string pubkey] | Stable | Supporté | typed_adapter | `0.2.2` | limit boundaries ; array | [Solana](https://solana.com/docs/rpc/http/getslotleaders) | | `getSlotLeaders` | Cluster | startSlot ; limit 1..5000 | [string pubkey] | Stable | Supporté | typed_adapter | `0.2.2` | limit boundaries ; array | [Solana](https://solana.com/docs/rpc/http/getslotleaders) |
| `getVersion` | Cluster | aucun | {solana-core:string,feature-set:u32\|null} | Stable | Supporté | typed_adapter | `0.2.1` | empty params ; feature-set present/null | [Solana](https://solana.com/docs/rpc/http/getversion) | | `getVersion` | Cluster | aucun | {solana-core:string,feature-set:u32 / null} | Stable | Supporté | typed_adapter | `0.2.1` | empty params ; feature-set present/null | [Solana](https://solana.com/docs/rpc/http/getversion) |
| `getVoteAccounts` | Cluster | config? {commitment,votePubkey,keepUnstakedDelinquents,delinquentSlotDistance} | {current:[VoteAccount],delinquent:[VoteAccount]} | Stable | Supporté | typed_adapter | `0.2.2` | filters/config ; both sets | [Solana](https://solana.com/docs/rpc/http/getvoteaccounts) | | `getVoteAccounts` | Cluster | config? {commitment,votePubkey,keepUnstakedDelinquents,delinquentSlotDistance} | {current:[VoteAccount],delinquent:[VoteAccount]} | Stable | Supporté | typed_adapter | `0.2.2` | filters/config ; both sets | [Solana](https://solana.com/docs/rpc/http/getvoteaccounts) |
| `getInflationGovernor` | Economics | config? {commitment} | InflationGovernor | Stable | Supporté | typed_adapter | `0.2.4` | config ; f64 fields | [Solana](https://solana.com/docs/rpc/http/getinflationgovernor) | | `getInflationGovernor` | Economics | config? {commitment} | InflationGovernor | Stable | Supporté | typed_adapter | `0.2.4` | config ; f64 fields | [Solana](https://solana.com/docs/rpc/http/getinflationgovernor) |
| `getInflationRate` | Economics | aucun | {total,validator,foundation:f64,epoch:u64} | Stable | Supporté | typed_adapter | `0.2.4` | empty params ; object | [Solana](https://solana.com/docs/rpc/http/getinflationrate) | | `getInflationRate` | Economics | aucun | {total,validator,foundation:f64,epoch:u64} | Stable | Supporté | typed_adapter | `0.2.4` | empty params ; object | [Solana](https://solana.com/docs/rpc/http/getinflationrate) |
| `getInflationReward` | Economics | addresses ; config? {commitment,epoch,minContextSlot} | [InflationReward\|null] | Stable | Supporté | typed_adapter | `0.2.4` | addresses/config ; reward/null ; commission null | [Solana](https://solana.com/docs/rpc/http/getinflationreward) | | `getInflationReward` | Economics | addresses ; config? {commitment,epoch,minContextSlot} | [InflationReward / null] | Stable | Supporté | typed_adapter | `0.2.4` | addresses/config ; reward/null ; commission null | [Solana](https://solana.com/docs/rpc/http/getinflationreward) |
| `getStakeMinimumDelegation` | Economics | config? {commitment,minContextSlot} | RpcResponse<u64> | Stable | Supporté | typed_adapter | `0.2.4` | config ; value | [Solana](https://solana.com/docs/rpc/http/getstakeminimumdelegation) | | `getStakeMinimumDelegation` | Economics | config? {commitment,minContextSlot} | RpcResponse<u64> | Stable | Supporté | typed_adapter | `0.2.4` | config ; value | [Solana](https://solana.com/docs/rpc/http/getstakeminimumdelegation) |
| `getSupply` | Economics | config? {commitment,excludeNonCirculatingAccountsList} | RpcResponse<Supply> | Stable | Supporté | typed_adapter | `0.2.4` | config true/false ; list semantics | [Solana](https://solana.com/docs/rpc/http/getsupply) | | `getSupply` | Economics | config? {commitment,excludeNonCirculatingAccountsList} | RpcResponse<Supply> | Stable | Supporté | typed_adapter | `0.2.4` | config true/false ; list semantics | [Solana](https://solana.com/docs/rpc/http/getsupply) |

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md --> <!-- file: docs/plans/012-V0_2_5_WALLET_FOUNDATION_PLAN.md -->
<!-- version: 15 --> <!-- version: 16 -->
# Plan `0.2.5` — Wallet foundation # Plan `0.2.5` — Wallet foundation

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md --> <!-- file: docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md -->
<!-- version: 17 --> <!-- version: 18 -->
# Plan `0.2.7` — WebSocket Solana standard # Plan `0.2.7` — WebSocket Solana standard

View File

@@ -0,0 +1,808 @@
<!-- file: docs/plans/016-V0_2_9_YELLOWSTONE_GRPC_PLAN.md -->
<!-- version: 25 -->
# Plan `0.2.9` — moteur Yellowstone gRPC + standard Solana + PublicNode
**Statut courant : le couloir technique `0.2.9-pre.013` est fermé par `pre.013-fix.004` : gates déterministes et workspace verts, graphes Cargo finaux inspectés, et smoke Yellowstone PublicNode authentifié exécuté avec succès sur Mainnet et Testnet (`2/2 PASS`). `pre.014` est la réconciliation documentaire finale ; seule `pre.015` restera ensuite pour le prompt `0.2.10`, `CHANGELOG.md` et `ROADMAP.md`, avant `rel.001`.**
## 1. Objet et autorité de la release
Base stable d'ouverture :
```text
v0.2.8
```
Release :
```text
0.2.9 — Yellowstone gRPC standard/provider-neutral
```
Le gate `pre.001`, ses fixes, les deltas techniques suivants et le code réel ont affiné le prompt de démarrage. L'ordre d'autorité utilisé pendant la release est :
```text
règles normatives KSP
code et fichiers réellement livrés
nouveaux deltas immuables de 0.2.9
décisions courantes consolidées dans ce plan et la validation 012
prompt de démarrage pour les contraintes qui n'ont pas été explicitement supersédées
```
Les deltas historiques ne sont jamais réécrits pour refléter une décision ultérieure.
## 2. Résultat final attendu
`0.2.9` doit fermer une première fondation Yellowstone gRPC exploitable sans devenir un SDK fournisseur :
```text
backend gRPC distinct de HTTP et WebSocket
moteur Tonic/Protobuf privé dans ksp-onchain-transport-lib
façade Yellowstone standard provider-neutral
7 unary standard retenus
Subscribe standard avec les familles/accounts/slots/transactions/blocks retenues
9 variantes SubscribeUpdate courantes
stream bidirectionnel borné
backpressure, half-close et shutdown déterministes
reconnect KSP-owned et replay/from_slot prudent
aucune promesse exactly-once/lossless non prouvée
Config Transport V3 backward-readable V1/V2
provider et protocol distincts dans Config
première intégration PublicNode strictement standard
smoke live opt-in avec credential secret non versionné
non-régressions HTTP, WebSocket standard et Helius WebSocket
```
## 3. Scope fermé par `pre.001`
### 3.1 Inclus
```text
N1 — moteur Yellowstone gRPC
N2 — standard Solana Yellowstone
N3 — première intégration PublicNode quand elle réutilise le standard sans divergence wire
TLS et metadata provider-neutral
7 unary standards retenus
Subscribe standard retenu
reconnect/replay observables mais non lossless
Config V3 si le mapping reste Config -> Transport
smokes PublicNode architecture-safe
```
### 3.2 Hors scope `0.2.9`
```text
SubscribeDeshred / pré-exécution
extensions Triton spécifiques
adapter Helius LaserStream gRPC spécifique
adapter OrbitFlare spécifique
pool/scheduler automatique complexe de sessions gRPC
serveur Geyser/plugin validator
Store/persistence/backfill historique
workers/jobs d'acquisition
replay lossless garanti
refonte HTTP ou WebSocket
```
Les intégrations provider futures ne dupliquent jamais le moteur N1. Une façade provider n'existe que si elle porte une divergence réelle : auth, capabilities, restriction, extension wire ou policy lifecycle.
## 4. Audit upstream et dépendances retenues
### 4.1 Snapshot normatif et réaudit final
Le gate d'ouverture puis le réaudit final du 2026-08-24 confirment que les numéros du plugin, du client, du proto et des releases GitHub évoluent indépendamment. Il ne faut donc pas transformer un numéro de `master` en numéro de release publié.
État observé au réaudit final :
```text
latest release GitHub indexée v14.2.2+solana.4.1.0 — 2026-07-27
master yellowstone-grpc-geyser 15.1.2
master yellowstone-grpc-client 13.3.0
master yellowstone-grpc-proto 12.6.0
master Agave Geyser / storage 4.2.0
master prost / prost-types / tonic 0.14.x
KSP yellowstone-grpc-proto direct ^12.6
```
Le service `Geyser`, la shape de `SubscribeRequest` et les neuf variantes `SubscribeUpdate` retenues par KSP n'ont pas changé matériellement par rapport au gate `pre.001`. `SubscribeDeshred` reste explicitement hors scope `0.2.9`.
Les sources primaires restent :
```text
https://github.com/rpcpool/yellowstone-grpc/releases
https://github.com/rpcpool/yellowstone-grpc/blob/master/CHANGELOG.md
https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/geyser.proto
https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/solana-storage.proto
https://github.com/rpcpool/yellowstone-grpc/blob/master/LICENSING.md
https://docs.rs/crate/yellowstone-grpc-proto/latest
https://docs.rs/crate/yellowstone-grpc-client/latest
```
Le réaudit final a été exécuté en `pre.012`; aucun changement upstream matériel na remis en cause la surface retenue.
### 4.2 Licence
Le repository upstream est globalement `AGPL-3.0-only`, mais `LICENSING.md` affecte explicitement Apache-2.0 à :
```text
examples/
yellowstone-grpc-client/
yellowstone-grpc-client-nodejs/
yellowstone-grpc-proto/
```
Décision :
```text
dépendance publiée yellowstone-grpc-proto = acceptée
.proto vendored dans KSP = non
source provenant des zones AGPL = non copiée
future copie upstream = nouveau gate provenance/licence obligatoire
```
### 4.3 Stratégie client
| Stratégie | Décision | Raison principale |
|-------------------------------------------------------|--------------------|-----------------------------------------------------------------------|
| `yellowstone-grpc-client + yellowstone-grpc-proto` | non retenue | importerait trop de lifecycle/reconnect upstream et de surface client |
| `yellowstone-grpc-proto + client KSP autour de tonic` | **retenue** | wire officiel, moteur/lifecycle/redaction KSP-owned |
| proto/génération KSP vendored | fallback seulement | dette licence/synchronisation/build plus forte |
Matérialisation courante :
```text
yellowstone-grpc-proto ^12.6 runtime sans feature tonic
yellowstone-grpc-proto dev/test avec feature tonic pour GeyserServer fixture
tonic ^0.14 channel + TLS runtime ; codegen/server dev/test
tonic-prost ^0.14 ProstCodec bas niveau
http ^1.5 PathAndQuery interne
yellowstone-grpc-client absent
prost/prost-types aucune dépendance KSP directe
proto vendored absent
```
Les graphes Cargo inspectés pendant `pre.002/pre.003` n'ont pas révélé de seconde génération incompatible à corriger. Le graph final est réinspecté en `pre.012`.
## 5. Matrice protocolaire fermée
### 5.1 Service `Geyser`
| RPC | Forme | Classification | Cible `0.2.9` | État |
|-----------------------|-------------|------------------------------------------------------|---------------|----------------------------|
| `Subscribe` | bidi stream | standard Yellowstone | oui | **DONE** `pre.009/pre.010` |
| `SubscribeDeshred` | bidi stream | extension/pré-exécution Triton publiée dans le proto | non | **OUT** |
| `SubscribeReplayInfo` | unary | standard | oui | **DONE** |
| `Ping` | unary | standard | oui | **DONE** |
| `GetLatestBlockhash` | unary | standard | oui | **DONE** |
| `GetBlockHeight` | unary | standard | oui | **DONE** |
| `GetSlot` | unary | standard | oui | **DONE** |
| `IsBlockhashValid` | unary | standard | oui | **DONE** |
| `GetVersion` | unary | standard | oui | **DONE** |
`SubscribeDeshred` reste explicitement exclu même s'il existe dans le proto publié : sa présence wire n'en fait pas une capacité provider-neutral de la fondation KSP.
### 5.2 `SubscribeRequest`
| Champ | Sémantique | État |
|-----------------------|----------------------------------------|-------------------------------|
| `accounts` | map nom -> filtre accounts | **DONE** |
| `slots` | map nom -> filtre slots | **DONE** |
| `transactions` | map nom -> filtre transactions | **DONE** |
| `transactions_status` | même famille de filtre transaction | **DONE** |
| `blocks` | map nom -> filtre blocks | **DONE** |
| `blocks_meta` | map nom -> filtre marqueur vide | **DONE** |
| `entry` | map nom -> filtre marqueur vide | **DONE** |
| `commitment` | optional Processed/Confirmed/Finalized | **DONE** |
| `accounts_data_slice` | repeated offset/length | **DONE** |
| `ping` | optional request ping/id | **DONE** |
| `from_slot` | optional u64 | **DONE**, sémantique prudente |
Accounts :
```text
account[]
owner[]
filters[]
nonempty_txn_signature?
cuckoo_accounts_filter?
memcmp { offset, oneof bytes | base58 | base64 }
datasize
token_account_state
lamports { oneof eq | ne | lt | gt }
```
Slots :
```text
filter_by_commitment?
interslot_updates?
statuses = processed | confirmed | finalized | first_shred_received | completed | created_bank | dead
```
Transactions et `transaction_status` :
```text
vote?
failed?
signature?
account_include[]
account_exclude[]
account_required[]
cuckoo_account_include?
token_accounts? = ALL | BALANCE_CHANGED
```
Blocks :
```text
account_include[]
include_transactions?
include_accounts?
include_entries?
cuckoo_account_include?
```
`blocks_meta` et `entry` conservent la distinction absence / map vide / filtre nommé vide.
Bornes KSP communes matérialisées :
```text
filter groups nommés total <= 1024
filter name 1..128 octets, trim exact, sans contrôle
filter names uniques globalement entre les sept maps
accounts_data_slice count <= 128
accounts_data_slice length <= 64 MiB
offset + length sans overflow u64
```
Les bounds spécifiques Accounts/Transactions/Blocks sont ceux désormais testés dans leurs tranches respectives ; ils ne sont pas dupliqués comme knobs Config.
### 5.3 `SubscribeUpdate`
| Variante | Champs structurants conservés | État |
|----------------------|-----------------------------------------------------------------------------|----------|
| `account` | account info + slot + `is_startup` | **DONE** |
| `slot` | slot + parent? + status + dead_error? | **DONE** |
| `transaction` | signature/is_vote/transaction/meta/index + slot | **DONE** |
| `transaction_status` | slot/signature/is_vote/index/error | **DONE** |
| `block` | slot/hash/rewards/time/height/parent/counts + transactions/accounts/entries | **DONE** |
| `ping` | marker server ping | **DONE** |
| `pong` | id | **DONE** |
| `block_meta` | block metadata/counts sans tableaux complets | **DONE** |
| `entry` | slot/index/num_hashes/hash/transaction counts/index | **DONE** |
Le top-level conserve également `filters[]` et `created_at`. Les types Prost/Yellowstone générés restent privés.
### 5.4 Unary standards
| RPC | Request | Response KSP utile | État |
|-----------------------|---------------------------|------------------------------------------|----------|
| `SubscribeReplayInfo` | vide | `first_available?` | **DONE** |
| `Ping` | `count` | `count` | **DONE** |
| `GetLatestBlockhash` | `commitment?` | slot, blockhash, last_valid_block_height | **DONE** |
| `GetBlockHeight` | `commitment?` | block_height | **DONE** |
| `GetSlot` | `commitment?` | slot | **DONE** |
| `IsBlockhashValid` | blockhash + `commitment?` | slot + valid | **DONE** |
| `GetVersion` | vide | version bornée | **DONE** |
Ces capacités ne remplacent pas les wrappers Solana JSON-RPC HTTP.
## 6. Architecture runtime actuelle
### 6.1 Séparation des backends
```text
HTTP HttpTransportSettings / pool HTTP
WebSocket engine WsSession actor partagé
Solana standard WS SolanaStandardWsSession
Helius LaserStream WS HeliusLaserStreamWsSession
Yellowstone gRPC engine YellowstoneGrpcChannel + moteur bidi KSP
Solana Yellowstone standard contrats typed KSP
provider descripteur d'exécution/capability, pas nouveau protocole
```
Interdictions :
```text
pas de WsProtocolKind pour gRPC
pas de WsEndpointSettings pour gRPC
pas de client Tonic brut réexporté
pas de second moteur physique par provider
pas de façade provider vide qui ne ferait que renommer le standard
```
### 6.2 Contrats publics principaux matérialisés
```text
YellowstoneGrpcEndpointUrl
YellowstoneGrpcProviderName
YellowstoneGrpcClusterName
YellowstoneGrpcMetadataEntry
YellowstoneGrpcReconnectSettings
YellowstoneGrpcSessionSettings
YellowstoneGrpcEndpointSettings
YellowstoneGrpcTransportSettings
YellowstoneGrpcChannel
YellowstoneSubscribeRequest + filtres typed
YellowstoneSubscribeUpdate + variantes typed
SolanaYellowstoneGrpcSubscribeSession
YellowstoneGrpcSubscribeSnapshot
7 unary typed
```
Le wire Tonic/Prost reste privé et n'est pas une escape hatch publique.
### 6.3 Credentials et diagnostics
Transport reçoit des valeurs déjà résolues par son consumer. Il ne connaît :
```text
aucun KSP_SECRET_*
aucun KSP_PUBLIC_*
aucun std::env
aucun header commercial hardcodé dans le standard
```
Les URLs, metadata sensibles, messages/details de `tonic::Status` et payloads arbitraires ne sont pas recopiés dans `Debug`, `Display`, snapshots ou contexts KSP.
## 7. Lifecycle, backpressure et continuité
### 7.1 Stream bidi
Acquis depuis `pre.009` :
```text
une request mpsc bornée consommée par Tonic
une update queue bornée côté KSP
mutation du SubscribeRequest sur le même stream
Ping serveur -> réponse automatique appropriée
Pong décodé
server half-close observable
client explicit close borné
Drop best-effort sans panic
oversized inbound/outbound borné
slow receiver overflow terminal et observable
shutdown déterministe
```
Aucune queue non bornée et aucun drop silencieux n'est présenté comme lossless.
### 7.2 Reconnect/replay
Acquis depuis `pre.010` :
```text
reconnect automatique oui, borné et KSP-owned
resubscribe dernier SubscribeRequest complet accepté
from_slot de reprise max(from_slot explicite, dernier slot observé) quand applicable
SubscribeReplayInfo informatif
first_available clamp/prouve un gap seulement s'il dépasse le slot demandé
exactly-once non garanti
lossless non garanti
ordre global sans gap non garanti
duplicate possible, compté, non supprimé silencieusement
gap compté seulement lorsqu'une preuve est disponible
shutdown during backoff interrompt la reconnexion
budget reconnect épuisé état terminal safe
```
Snapshot public safe :
```text
reconnect_count
replay_attempt_count
continuity_gap_count
duplicate_update_count
last_requested_from_slot
last_observed_slot
terminal state/error code safe
```
La présence de `from_slot` ou `SubscribeReplayInfo` n'autorise aucune promesse de replay historique complet.
## 8. Config Transport V3 — tranche `pre.011`
### 8.1 Compatibilité documentaire
Décision fermée :
```text
V1 = HTTP-only, backward-readable
V2 = HTTP + WebSocket, backward-readable
V3 = HTTP + WebSocket + Yellowstone gRPC optionnel par profil
```
Le schema V3 conserve des branches strictes V1/V2 au lieu de relâcher leurs `additionalProperties`.
Shape V3 :
```text
format_version = 3
retry
ws_defaults
grpc_defaults
default_profile
profiles[] {
profile_id
endpoints[]
ws_endpoints[]
grpc_endpoints[]? # optionnel par profil
}
```
L'absence de `grpc_endpoints` dans un profil V3 signifie `None`, pas un `YellowstoneGrpcTransportSettings` vide inventé.
### 8.2 `grpc_defaults`
Les defaults Config correspondent uniquement à de vrais settings runtime Transport :
```text
connect_timeout_ms
unary_timeout_ms
close_timeout_ms
reconnect.max_retries
reconnect.initial_backoff_ms
reconnect.max_backoff_ms
request_channel_capacity
update_channel_capacity
max_inbound_message_size_bytes
max_outbound_message_size_bytes
```
Les bounds de filtres Subscribe restent un contrat Transport fixe et ne deviennent pas des options Config sans besoin démontré.
### 8.3 Endpoint gRPC
Chaque `grpc_endpoints[]` porte :
```text
name
enabled
provider
cluster
protocol = solana_yellowstone
url
metadata[]? # classe publique
secret_metadata[]? # classe secrète
session? # overrides bornés
```
Axes distincts :
```text
protocol = contrat wire standard, actuellement solana_yellowstone
provider = environnement d'exécution descriptif, par exemple publicnode
```
`provider = publicnode` ne crée donc pas un `PublicNodeGrpcProtocol` ni une façade provider sans divergence réelle.
### 8.4 Provenance des metadata
Config est propriétaire de la résolution :
```text
metadata -> interdit toute provenance KSP_SECRET_*/KSPB_SECRET_*
secret_metadata -> exige au moins une provenance secret et interdit une variable non-secret
littéraux autour d'un placeholder secret -> autorisés ; safe_value masque seulement le segment secret
```
Transport reçoit ensuite `YellowstoneGrpcMetadataEntry` public/secret et ne connaît jamais le nom de variable d'environnement.
### 8.5 API Config sans rupture V2
L'API existante reste :
```text
http_settings()
ws_settings()
into_transport_settings() -> (HTTP, Option<WS>)
```
`pre.011` ajoute :
```text
grpc_settings()
into_all_transport_settings() -> (HTTP, Option<WS>, Option<Yellowstone gRPC>)
```
Le tuple historique n'est pas modifié silencieusement.
## 9. PublicNode dans `0.2.9`
### 9.1 Mainnet
Endpoint retenu et validé en live :
```text
https://solana-yellowstone-grpc.publicnode.com:443
```
Le profil committé est :
```text
profile_id = publicnode_mainnet
provider = publicnode
cluster = mainnet-beta
protocol = solana_yellowstone
metadata = x-token secret via KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN
```
Les tentatives sans metadata ont atteint le service mais ont reçu `PERMISSION_DENIED`. Le profil final fournit donc le personal token PublicNode comme metadata gRPC secrète `x-token`; Transport ne connaît ni le nom de variable KSP ni sa provenance.
### 9.2 Testnet
L'endpoint exact confirmé par l'opérateur et validé en live est :
```text
https://solana-testnet-yellowstone-grpc.publicnode.com:443
```
Le profil committé est :
```text
profile_id = publicnode_testnet
provider = publicnode
cluster = testnet
protocol = solana_yellowstone
metadata = x-token secret via KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN
```
Le smoke final prouve qu'un même personal token opérateur peut fonctionner sur Mainnet et Testnet. KSP conserve néanmoins deux variables de configuration distinctes afin de ne pas imposer une hypothèse de portée provider à long terme ; elles peuvent recevoir la même valeur. Cette séparation est une liberté de configuration KSP, pas une assertion que PublicNode délivre des tokens network-scoped.
## 10. Threat model et bornes
Menaces couvertes :
```text
credential dans URI/metadata
Status/message/details provider arbitraires
Debug dérivé de filtres/payloads
TLS/connect error qui réémet l'URI
message inbound/outbound hostile
stream flood / slow consumer
filter explosion / collision de noms
unknown enum/oneof
server/client half-close
reconnect loop
node divergent après reconnect
duplicate/gap après replay
mutation tardive du stream
```
Réponses :
```text
wrappers redacted
allowlist de contexts KSP
validation/bounds avant I/O
queues bornées
états terminaux observables
reconnect budget borné
aucune promesse de continuité non prouvée
Config sensitivity gate avant construction de metadata secret
```
## 11. Smoke ownership
Ordre de preuve :
```text
Transport programmatic -> PublicNode Mainnet Yellowstone + secret x-token fourni au harness
Transport programmatic -> PublicNode Testnet Yellowstone + secret x-token fourni au harness
Config V3 -> Transport -> PublicNode via secret_metadata, jamais un smoke réseau placé dans Config par facilité
```
Un smoke Transport pur peut vivre dans `ksp-onchain-transport-lib/tests` puisqu'il construit ses settings programmatiquement.
Un smoke cross-crates Config -> Transport ne doit pas devenir une responsabilité durable de `ksp-config-lib`. Sil nexiste pas encore de surface dintégration appropriée, la preuve live reste un smoke Transport programmatique avec secret fourni uniquement au harness opérateur.
Aucun secret provider n'est versionné.
## 12. État des tranches et historique compact
Les preuves détaillées restent dans les fichiers `deltas/0.2.9/*.md`. Le plan ne duplique plus leurs journaux complets.
| Tranche | Objet | État consolidé |
|-------------------|----------------------------------------------------------|----------------------|
| `pre.001` + fixes | audit upstream, licence, providers, architecture, sizing | **CLOSED** |
| `pre.002` + fixes | dépendances, settings/errors, channel minimal | **CLOSED** |
| `pre.003` + fix | TLS, metadata, fixture locale, 7 unary | **CLOSED** |
| `pre.004` + fix | Subscribe foundation/common | **CLOSED** |
| `pre.005` + fix | Accounts + Slots | **CLOSED** |
| `pre.006` | namespace privé HTTP explicite | **CLOSED** |
| `pre.007` | Transactions + transaction_status | **CLOSED** |
| `pre.008` + fix | Blocks + block_meta + entry | **CLOSED** |
| `pre.009` + fix | bidi, Ping/Pong, backpressure, half-close, shutdown | **CLOSED** |
| `pre.010` + fix | reconnect, from_slot, ReplayInfo, gaps/duplicates | **CLOSED** |
| `pre.011` | Config V3 + protocol/provider + PublicNode Mainnet | **CLOSED** |
| `pre.012` | réaudit + smoke ajouté + docs préparatoires | **LOCAL GATE PASS** |
| `pre.012-fix.001` | règles de séparation de fermeture + recalibrage | **CLOSED** |
| `pre.013` + fixes | gate technique/live PublicNode + graphes finaux | **CLOSED** |
| `pre.014` | réconciliation documentaire finale | **ACTIVE CANDIDATE** |
| `pre.015` | prompt + CHANGELOG + ROADMAP | **PLANNED** |
Gate opérateur déterministe de `pre.012` reçu le 2026-08-24 :
```text
fmt/audit Rust PASS sans warning
Markdown tables PASS, 87 tableaux / 252 fichiers
cargo check PASS
cargo clippy PASS sans warning
Config unit 113/113
Config public API 15/15
Config ownership 5/5
Transport unit 383/383
Transport public API 49/49
Transport completeness 43/43
Transport doctests 4/4
workspace dependencies 3/3
cargo test --workspace PASS
PublicNode live smoke NOT RUN, test ignored
cargo tree final NOT PROVIDED / NOT RUN in supplied log
```
`pre.012` reste historiquement une tranche mixte créée avant l'adoption de la nouvelle règle. Son fix ne réécrit pas cette histoire : le prompt `0.2.10` déjà présent est désormais considéré comme un brouillon à finaliser seulement en `pre.015`, et les README/USAGE préparés sont réconciliés par la présente `pre.014`.
Gate technique final `pre.013-fix.004` reçu le 2026-08-24 :
```text
fmt/audit Rust PASS sans warning
Markdown tables PASS, 87 tableaux / 258 fichiers
cargo check PASS
cargo clippy --all-targets PASS sans warning
Transport unit 383/383
Transport public API 49/49
Transport completeness 43/43
Transport doctests 4/4
cargo test --workspace PASS
cargo graphs finaux inspectés, stack Yellowstone unique tonic/prost 0.14
PublicNode Mainnet Subscribe + slot PASS
PublicNode Testnet Subscribe + slot PASS
live smoke total 2/2 PASS
```
Les graphes inspectés avant `fix.004` restent valides : ce fix n'a modifié ni dépendance ni feature Cargo. La stack Yellowstone résolue est `yellowstone-grpc-proto 12.6.0`, `tonic/tonic-prost 0.14.6`, `prost/prost-types 0.14.4`, sans `yellowstone-grpc-client` runtime.
## 13. Forecast restant recalibré
### `pre.013` — gate technique/live final — **CLOSED**
Le couloir technique a finalement nécessité quatre fixes : découverte de l'auth PublicNode, ajout Testnet, adaptation de la fermeture live, puis conformité Clippy du harness. Le résultat consolidé est :
```text
workspace.package.version final du couloir = 0.2.9-pre.13.fix.4
PublicNode Mainnet Subscribe + slot PASS
PublicNode Testnet Subscribe + slot PASS
secret wire metadata x-token
secret versionné non
harness deux lignes stdin, valeurs identiques autorisées
close provider succès ou timeout KSP borné après slot reçu
graphes Cargo inspectés
clippy --all-targets PASS sans warning
workspace PASS
```
Les essais intermédiaires `PERMISSION_DENIED` sans credential sont conservés dans les deltas `pre.013-fix.001` à `fix.003`; ils ne sont pas réinterprétés comme une régression du moteur standard.
### `pre.014` — réconciliation documentaire finale — **ACTIVE CANDIDATE**
Payload :
```text
workspace.package.version = 0.2.9-pre.14
plan 016 réconcilié avec le gate technique/live réel
validation 012 fermée avec uniquement des preuves exécutées
README/USAGE Transport relus et corrigés
références durables liées à 0.2.9 réconciliées si nécessaire
audit Markdown final des documents touchés
aucune modification fonctionnelle/runtime
aucune finalisation CHANGELOG/ROADMAP/prompt suivant
```
Si une correction de code ou de smoke apparaît ici, `pre.014` ne l'absorbe pas : une nouvelle tranche technique est ouverte, puis la réconciliation documentaire est rejouée.
### `pre.015` — préparation de publication minimale
Payload fonctionnel strict :
```text
workspace.package.version = 0.2.9-pre.15
prompt 0.2.10 OrbitFlare finalisé
CHANGELOG.md synchronisé pour 0.2.9
ROADMAP.md état global 0.2.9 / suite synchronisé
delta pre.015
```
Aucun README, USAGE, plan, validation, règle, code, test, schema ou config n'est modifié dans cette tranche. Si l'un de ces fichiers nécessite encore une correction, la tranche de publication est décalée après une nouvelle prerelease dédiée.
### `rel.001` — publication stable
`rel.001` ne sert qu'à la mécanique de publication stable : version Cargo finale `0.2.9`, delta de release, commit final et tag `v0.2.9` après gate. Aucun rattrapage technique ou documentaire n'y est mélangé.
## 14. Critères de split
Scinder avant dette silencieuse si :
1. une évolution upstream matérielle invalide le wire retenu ;
2. PublicNode exige une divergence provider-specific significative ;
3. le replay nécessite un sous-système de fork/equivocation plus large que la foundation ;
4. une tranche dépasse nettement le budget nominal sans frontière claire ;
5. le gate technique/live montre une dette dépendance/licence ou une non-régression qui ne peut pas être corrigée proprement dans son couloir ;
6. une correction appartenant à un couloir déjà franchi est découverte pendant un couloir ultérieur.
Le noyau à préserver reste :
```text
moteur Yellowstone + façade Solana standard + Config provider-neutral + première intégration PublicNode minimale
```
## 15. Gates opérateur restants
Le gate `pre.013` est fermé. Gate `pre.014` :
```bash
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.2.9
cargo test --workspace
```
Gate `pre.015` : audit documentaire ciblé sur `prompts/`, `CHANGELOG.md` et `ROADMAP.md`, plus les validations minimales imposées par le changement de version Cargo. Aucun smoke n'est rejoué dans cette tranche en l'absence de changement technique.
## 16. Conditions de clôture `0.2.9`
```text
service/proto courant réconcilié
SubscribeDeshred explicitement OUT
7 unary verts
Subscribe standard et 9 updates verts
backend gRPC distinct de HTTP/WS
raw Tonic/Prost privé
secrets/metadata redacted
bounds/backpressure/shutdown verts
reconnect/replay documentés sans lossless implicite
Config V3 backward V1/V2
provider/protocol distincts
PublicNode Mainnet live Subscribe + slot PASS
PublicNode Testnet live Subscribe + slot PASS
auth PublicNode x-token secret documentée sans secret versionné
HTTP 52+14 non régressé
Standard WS 18/18 non régressé
Helius WS non régressé
cargo graphs finaux inspectés
README/USAGE finaux réconciliés en pre.014
validation 012 fermée en pre.014
prompt/CHANGELOG/ROADMAP seuls finalisés en pre.015
workspace final vert
```
## 17. Séquence après `0.2.9`
La séquence active a été recalibrée par les fixes de `pre.001`; cette décision est conservée pendant le nettoyage documentaire :
```text
0.2.9 moteur Yellowstone + Solana standard + PublicNode
0.2.10 OrbitFlare Yellowstone gRPC
0.2.11 Helius LaserStream gRPC
0.2.12 off-chain price transport
0.2.13 Price Desk + intégration prix Wallet Desk
0.2.14 interface/wire foundation
0.2.15 program-api foundation
```
Les intégrations suivantes restent dans le backlog non numéroté tant qu'aucune décision d'implémentation ne les fait entrer dans la séquence active :
```text
TODO eRPC
TODO Triton
TODO Alchemy
TODO QuickNode
TODO Chainstack
IDEAS Tatum
IDEAS Shyft
IDEAS Solinfra
IDEAS NodeFlare
```
OrbitFlare reste le provider dédié `0.2.10` et Helius LaserStream gRPC `0.2.11` selon la séquence recalibrée par les fixes de `pre.001`. Chaque release doit réauditer auth, capabilities, restrictions, extensions wire, replay/from_slot et lifecycle au lieu de supposer une équivalence complète avec N2.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/FILE_CONTRACTS.md --> <!-- file: docs/rules/FILE_CONTRACTS.md -->
<!-- version: 17 --> <!-- version: 18 -->
# Contrats des fichiers # Contrats des fichiers
@@ -20,13 +20,13 @@ Les règles `FILE-*` définissent la responsabilité et le mode de modification
| `.cargo/config.toml` | Définir les réglages Cargo propres au workspace qui ne relèvent pas du manifeste, notamment l'emplacement des artefacts de build. | Modifier lorsqu'un réglage Cargo commun change ; ne pas y placer de secret ni de configuration spécifique à une machine particulière. | | `.cargo/config.toml` | Définir les réglages Cargo propres au workspace qui ne relèvent pas du manifeste, notamment l'emplacement des artefacts de build. | Modifier lorsqu'un réglage Cargo commun change ; ne pas y placer de secret ni de configuration spécifique à une machine particulière. |
| `rustfmt.toml` | Définir le formatage Rust commun. | Modifier comme changement normatif, avec justification dans le delta. | | `rustfmt.toml` | Définir le formatage Rust commun. | Modifier comme changement normatif, avec justification dans le delta. |
| `clippy.toml` | Définir les paramètres Clippy communs. | Modifier comme changement normatif, avec justification dans le delta. | | `clippy.toml` | Définir les paramètres Clippy communs. | Modifier comme changement normatif, avec justification dans le delta. |
| `ROADMAP.md` | Décrire les objectifs globaux et les grandes étapes prévues par phase/version, avec leur état synthétique. | Modifier lorsqu'un objectif, une grande étape, un report, une annulation ou un état global change ; ne pas y recopier le détail des prereleases prévu dans les plans de version. | | `ROADMAP.md` | Décrire les objectifs globaux et les grandes étapes prévues par phase/version, avec leur état synthétique. | Modifier lorsqu'un objectif, une grande étape, un report, une annulation ou un état global change ; pour une clôture de release, réserver sa synchronisation finale à la dernière prerelease de publication et ne pas y recopier le détail des prereleases prévu dans les plans de version. |
| `CHANGELOG.md` | Résumer les releases stables dans un ordre chronologique décroissant, sous forme d'un ou plusieurs paragraphes par release. | Synchroniser lors de la phase documentaire finale ; ne pas dupliquer les deltas ni créer de changelog par crate/module. | | `CHANGELOG.md` | Résumer les releases stables dans un ordre chronologique décroissant, sous forme d'un ou plusieurs paragraphes par release. | Synchroniser uniquement dans la dernière prerelease de préparation de publication ; ne pas le finaliser dans la prerelease de réconciliation README/USAGE et ne pas dupliquer les deltas ni créer de changelog par crate/module. |
## Répertoire `docs/` ## Répertoire `docs/`
| Fichier/famille | Responsabilité | Règle de modification | | Fichier/famille | Responsabilité | Règle de modification |
|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `docs/000-README.md` | Indexer et expliquer la documentation tout en restant en tête des listings et arbres de fichiers. | Modifier lorsque l'organisation durable de `docs/` change ; `000-README.md` reste prioritaire lorsqu'un ordre numérique existe. | | `docs/000-README.md` | Indexer et expliquer la documentation tout en restant en tête des listings et arbres de fichiers. | Modifier lorsque l'organisation durable de `docs/` change ; `000-README.md` reste prioritaire lorsqu'un ordre numérique existe. |
| `docs/formats/000-README.md` | Indexer les spécifications de formats durables KSP destinées à l'interopérabilité externe. | Modifier lorsqu'un format durable entre/sort de cette famille ou que son statut change ; conserver `000-README.md` comme point d'entrée. | | `docs/formats/000-README.md` | Indexer les spécifications de formats durables KSP destinées à l'interopérabilité externe. | Modifier lorsqu'un format durable entre/sort de cette famille ou que son statut change ; conserver `000-README.md` comme point d'entrée. |
| `docs/formats/*.md` | Spécifier un wire KSP durable indépendamment de son implémentation, avec encodages, limites, parsing, auth et vecteurs. | Modifier avec traçabilité lorsqu'un contrat de format évolue ; après publication stable d'une version de format, toute incompatibilité de wire ouvre une nouvelle version de format plutôt qu'une tolérance silencieuse. | | `docs/formats/*.md` | Spécifier un wire KSP durable indépendamment de son implémentation, avec encodages, limites, parsing, auth et vecteurs. | Modifier avec traçabilité lorsqu'un contrat de format évolue ; après publication stable d'une version de format, toute incompatibilité de wire ouvre une nouvelle version de format plutôt qu'une tolérance silencieuse. |
@@ -35,10 +35,10 @@ Les règles `FILE-*` définissent la responsabilité et le mode de modification
| `docs/architecture/000-README.md` | Indexer les documents décrivant l'architecture KSP décidée ou en cours de cadrage explicite. | Modifier lorsque la structure documentaire d'architecture change. | | `docs/architecture/000-README.md` | Indexer les documents décrivant l'architecture KSP décidée ou en cours de cadrage explicite. | Modifier lorsque la structure documentaire d'architecture change. |
| `docs/architecture/*.md` | Décrire les objectifs, frontières, responsabilités et architecture courante ou explicitement proposée. | Ne pas utiliser comme journal de livraison ; distinguer clairement les décisions validées des hypothèses encore ouvertes. | | `docs/architecture/*.md` | Décrire les objectifs, frontières, responsabilités et architecture courante ou explicitement proposée. | Ne pas utiliser comme journal de livraison ; distinguer clairement les décisions validées des hypothèses encore ouvertes. |
| `docs/plans/000-README.md` | Indexer les plans de versions/phases. | Modifier lorsque l'organisation des plans change. | | `docs/plans/000-README.md` | Indexer les plans de versions/phases. | Modifier lorsque l'organisation des plans change. |
| `docs/plans/*.md` | Organiser une version ou phase complexe et, pour `pre.001`, détailler la prévision souple de ses prereleases. | Faire évoluer le plan lorsque la planification change ; prévoir des tranches intermédiaires bornées et redécouper toute tranche estimée trop lourde. | | `docs/plans/*.md` | Organiser une version ou phase complexe et, pour `pre.001`, détailler la prévision souple de ses prereleases. | Faire évoluer le plan lorsque la planification change ; prévoir des tranches intermédiaires bornées et les couloirs de fermeture. Sa réconciliation finale appartient à l'avant-dernière prerelease documentaire, pas à la dernière prerelease de publication. |
| `docs/IDEAS.md` | Conserver les idées, pistes, questions et alternatives à explorer qui ne sont pas encore des engagements du roadmap. | Ajouter une idée dès qu'elle mérite d'être conservée ; mettre à jour son statut lorsqu'elle est explorée, retenue, rejetée ou transférée. | | `docs/IDEAS.md` | Conserver les idées, pistes, questions et alternatives à explorer qui ne sont pas encore des engagements du roadmap. | Ajouter une idée dès qu'elle mérite d'être conservée ; mettre à jour son statut lorsqu'elle est explorée, retenue, rejetée ou transférée. |
| futurs documents de référence | Définir vocabulaire, identifiants et références canoniques. | Mettre à jour quand la référence canonique évolue. | | futurs documents de référence | Définir vocabulaire, identifiants et références canoniques. | Mettre à jour quand la référence canonique évolue. |
| futures validations | Conserver des résultats réellement exécutés. | Ne jamais enregistrer une validation supposée comme réussie. | | futures validations | Conserver des résultats réellement exécutés. | Ne jamais enregistrer une validation supposée comme réussie. La validation finale est réconciliée et fermée dans l'avant-dernière prerelease documentaire, avant la dernière prerelease de publication. |
## Répertoire `config/` ## Répertoire `config/`
@@ -62,9 +62,9 @@ Pour un document standard profilé, `default_profile` et `profiles` sont des cl
## Répertoire `prompts/` ## Répertoire `prompts/`
| Fichier/famille | Responsabilité | Règle de modification | | Fichier/famille | Responsabilité | Règle de modification |
|-----------------------------|----------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |-----------------------------|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `prompts/000-README.md` | Point d'entrée des prompts et de leur cycle de vie. | Modifier lorsque l'organisation pratique des prompts change ; les règles normatives restent sous `docs/rules/PROMPT_STRUCTURE.md`. | | `prompts/000-README.md` | Point d'entrée des prompts et de leur cycle de vie. | Modifier lorsque l'organisation pratique des prompts change ; les règles normatives restent sous `docs/rules/PROMPT_STRUCTURE.md`. |
| `prompts/*START_PROMPT*.md` | Conserver un prompt de reprise versionné et réutilisable pour ouvrir une phase/version de travail. | Le créer tôt sous forme de brouillon lorsque la trajectoire devient assez claire, le mettre à jour au fil des décisions, puis le finaliser pendant la phase documentaire de clôture avant son utilisation. | | `prompts/*START_PROMPT*.md` | Conserver un prompt de reprise versionné et réutilisable pour ouvrir une phase/version de travail. | Le créer tôt sous forme de brouillon lorsque la trajectoire devient assez claire et le mettre à jour au fil des décisions ; sa finalisation appartient exclusivement à la dernière prerelease de préparation de publication, avec CHANGELOG et ROADMAP. |
## Répertoire `deltas/` ## Répertoire `deltas/`
@@ -72,6 +72,12 @@ Pour un document standard profilé, `default_profile` et `profiles` sont des cl
|----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `deltas/<X.Y.Z>/<delta-name>.md` | Tracer une livraison précise, sa base, son contenu, ses suppressions, validations et questions ouvertes, regroupée sous la version cible `X.Y.Z`. | Créé avec la livraison ; une livraison déjà publiée n'est pas réécrite silencieusement. Les prereleases utilisent `pre.NNN`, leurs correctifs `pre.NNN-fix.NNN`, et les publications de release utilisent `rel.NNN`. | | `deltas/<X.Y.Z>/<delta-name>.md` | Tracer une livraison précise, sa base, son contenu, ses suppressions, validations et questions ouvertes, regroupée sous la version cible `X.Y.Z`. | Créé avec la livraison ; une livraison déjà publiée n'est pas réécrite silencieusement. Les prereleases utilisent `pre.NNN`, leurs correctifs `pre.NNN-fix.NNN`, et les publications de release utilisent `rel.NNN`. |
## Séparation des fichiers pendant la fermeture
La prerelease de réconciliation documentaire possède la dernière passe sur les README/USAGE, plans, validations et références durables de la release. Après son gate, ces familles sont considérées figées pour la préparation de publication.
La dernière prerelease ne rouvre pas ces documents : son payload fonctionnel est limité au prompt suivant, `CHANGELOG.md` et `ROADMAP.md`, en plus des fichiers mécaniques de version/delta exigés par le workflow. Si un document durable doit encore être corrigé, une nouvelle prerelease documentaire est ouverte et la tranche de publication finale est décalée.
## Rust ## Rust
| Fichier/famille | Responsabilité | Règle de modification | | Fichier/famille | Responsabilité | Règle de modification |

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/PROMPT_STRUCTURE.md --> <!-- file: docs/rules/PROMPT_STRUCTURE.md -->
<!-- version: 5 --> <!-- version: 6 -->
# Structure des prompts KSP # Structure des prompts KSP
@@ -19,8 +19,42 @@ Sauf raison explicitement documentée :
- `pre.001` = lectures obligatoires + audit interne/externe + brainstorming + sizing + planification ; - `pre.001` = lectures obligatoires + audit interne/externe + brainstorming + sizing + planification ;
- les prereleases intermédiaires = tranches bornées de développement/validation ; - les prereleases intermédiaires = tranches bornées de développement/validation ;
- la dernière prerelease = validation finale + documentation + nettoyage/archivage + prompt de la release suivante ; - la fermeture réserve des prereleases distinctes pour le gate technique/live, la réconciliation documentaire puis la préparation de publication ;
- une `fix` corrige l'étape réellement livrée sans réécrire l'historique. - la dernière prerelease avant `rel.NNN` est une tranche de publication minimale, distincte de la réconciliation documentaire et des smokes ;
- une `fix` corrige uniquement la responsabilité de la tranche à laquelle il est rattaché et ne sert pas à absorber un autre couloir de fermeture.
## Séparation obligatoire des dernières prereleases
La queue de fermeture d'une release est structurée de manière à empêcher qu'un correctif de dernière minute mélange tests réseau, documentation durable et préparation de publication.
Lorsqu'un smoke final ou un autre gate live est pertinent, les trois dernières responsabilités sont ordonnées ainsi :
```text
pre.N-2 gate technique/live : smoke(s), graphes ou vérifications finales directement liées au runtime
pre.N-1 réconciliation documentaire : plan, validation, README, USAGE et autres références durables concernées
pre.N préparation de publication : prompt suivant + CHANGELOG + ROADMAP uniquement
rel.001 mécanique de publication stable
```
Les numéros `N-2`, `N-1` et `N` sont relatifs : l'insertion d'une tranche ou d'une correction décale la numérotation réelle sans affaiblir cette séparation.
Si aucun smoke/gate live final n'existe, le couloir `pre.N-2` est omis ; les deux dernières prereleases restent néanmoins séparées entre réconciliation documentaire et préparation de publication.
La dernière prerelease ne modifie fonctionnellement que :
```text
prompt de démarrage de la release suivante
CHANGELOG.md
ROADMAP.md
```
Les fichiers mécaniques imposés par le workflow restent autorisés : `Cargo.toml` pour la version d'une prerelease non-fix et `deltas/<X.Y.Z>/pre.NNN.md` pour sa traçabilité. Aucun README, USAGE, plan, validation, code, test, schema, config ou règle normative ne doit être introduit ou corrigé dans cette dernière tranche.
La prerelease documentaire immédiatement précédente possède la réconciliation finale des documents durables de la release : README/USAGE, plan, validation, architecture/référence concernée et cohérence documentaire globale. Elle ne finalise ni `CHANGELOG.md`, ni `ROADMAP.md`, ni le prompt de la release suivante.
Le gate technique/live, lorsqu'il existe, précède cette réconciliation documentaire. Les smokes et leurs corrections restent donc isolés avant que les documents finaux soient figés.
Un `fix` reste local à son couloir. Si un défaut d'une responsabilité antérieure est découvert après avoir avancé, il ne doit pas être glissé dans le `fix` de la tranche courante : une nouvelle tranche dédiée à la responsabilité concernée est ouverte, puis les couloirs de fermeture postérieurs sont rejoués si nécessaire. La livraison `rel.NNN` ne sert jamais à absorber un correctif fonctionnel ou documentaire qui aurait dû être traité en prerelease.
## Dimensionnement ## Dimensionnement
@@ -28,6 +62,8 @@ Lors de `pre.001`, une prerelease intermédiaire estimée à plus d'environ **15
La trajectoire initiale est **souple** : elle indique un nombre prévisionnel de prereleases, leur objectif et leur ordre, mais autorise l'insertion de tranches/fixes lorsqu'un audit ou une validation révèle un besoin réel. La fermeture ne doit jamais être forcée pour respecter un numéro prévu. La trajectoire initiale est **souple** : elle indique un nombre prévisionnel de prereleases, leur objectif et leur ordre, mais autorise l'insertion de tranches/fixes lorsqu'un audit ou une validation révèle un besoin réel. La fermeture ne doit jamais être forcée pour respecter un numéro prévu.
La prévision `pre.001` réserve explicitement les couloirs de fermeture applicables : gate technique/live éventuel, réconciliation documentaire, puis préparation de publication minimale. Leur numérotation peut dériver, mais leur ordre et leur séparation de responsabilités restent normatifs.
## Structure obligatoire d'un prompt de démarrage ## Structure obligatoire d'un prompt de démarrage
Un prompt de nouvelle session contient explicitement, dans un ordre facile à retrouver : Un prompt de nouvelle session contient explicitement, dans un ordre facile à retrouver :

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_DOCUMENTATION.md --> <!-- file: docs/rules/RULES_DOCUMENTATION.md -->
<!-- version: 4 --> <!-- version: 5 -->
# Règles de documentation # Règles de documentation
@@ -32,6 +32,15 @@ Les règles `DOC-*` s'appliquent aux documents Markdown internes et à leur orga
- **DOC-IDEAS-003** — Lorsqu'une idée devient un engagement, elle est transférée vers le roadmap ou un plan ; lorsqu'elle devient une décision durable, elle est reportée dans le document normatif ou architectural approprié. `IDEAS.md` conserve une trace concise de son issue. - **DOC-IDEAS-003** — Lorsqu'une idée devient un engagement, elle est transférée vers le roadmap ou un plan ; lorsqu'elle devient une décision durable, elle est reportée dans le document normatif ou architectural approprié. `IDEAS.md` conserve une trace concise de son issue.
- **DOC-IDEAS-004** — `IDEAS.md` ne doit pas devenir un second roadmap ni une liste de tâches de développement promises. - **DOC-IDEAS-004** — `IDEAS.md` ne doit pas devenir un second roadmap ni une liste de tâches de développement promises.
## Format des tableaux Markdown
- **DOC-TABLE-001** — Un tableau Markdown KSP utilise `|` uniquement comme séparateur structurel de colonnes. Un caractère `|` littéral, y compris sous forme échappée `\|`, est interdit dans le contenu dune cellule ; reformuler le contenu, utiliser `/`, `et`, une liste ou un bloc de code hors tableau.
- **DOC-TABLE-002** — Toutes les lignes dun même tableau ont leurs séparateurs verticaux aux mêmes positions. La largeur de chaque colonne est déterminée par le contenu le plus large de cette colonne, en comptant len-tête et les lignes de données.
- **DOC-TABLE-003** — Chaque cellule de contenu commence par exactement un espace après le séparateur `|` gauche. Dans la cellule qui porte le contenu le plus large dune colonne, il existe aussi exactement un espace entre le contenu et le séparateur `|` droit. Les cellules plus courtes conservent lunique espace gauche et reçoivent uniquement le padding droit nécessaire pour aligner les séparateurs verticaux.
- **DOC-TABLE-004** — La ligne séparatrice en tirets occupe exactement la même largeur que chaque colonne ; les marqueurs `:` de lalignement Markdown restent autorisés lorsquils sont intentionnels. Le résultat attendu est équivalent au reformatage de tableau produit par RustRover, mais la règle structurelle KSP prime sur léditeur utilisé.
- **DOC-TABLE-005** — Lorsquune modification touche une ligne dun tableau, le tableau entier est réaligné avant livraison. `python3 scripts/audit_markdown_tables.py <fichiers-markdown-modifiés>` est le canari mécanique recommandé pour les fichiers concernés ; les blocs de code fenced ne sont pas interprétés comme des tableaux.
- **DOC-TABLE-006** — Limmuabilité des deltas déjà publiés prime sur un reformatage rétrospectif : un ancien fichier `deltas/` nest jamais réécrit uniquement pour satisfaire une règle de présentation introduite ultérieurement. Tout nouveau delta et tout autre tableau modifiable créé ou touché doivent en revanche respecter immédiatement `DOC-TABLE-001` à `DOC-TABLE-005`.
## Contenu et exactitude ## Contenu et exactitude
- **DOC-CONTENT-001** — Une documentation décrit l'état réellement décidé ou validé et distingue explicitement les hypothèses, propositions, TODO et questions ouvertes. - **DOC-CONTENT-001** — Une documentation décrit l'état réellement décidé ou validé et distingue explicitement les hypothèses, propositions, TODO et questions ouvertes.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/VERSION_WORKFLOW.md --> <!-- file: docs/rules/VERSION_WORKFLOW.md -->
<!-- version: 6 --> <!-- version: 7 -->
# Versionnement, sessions et livraisons # Versionnement, sessions et livraisons
@@ -69,6 +69,14 @@ Les règles `VER-*` définissent la progression des versions KSP, les identifian
- **VER-LIFECYCLE-001** — La première prerelease d'une nouvelle phase fonctionnelle est prioritairement consacrée au brainstorming, à l'inventaire, aux risques, dépendances, hors-périmètre, critères de validation et plan de travail. - **VER-LIFECYCLE-001** — La première prerelease d'une nouvelle phase fonctionnelle est prioritairement consacrée au brainstorming, à l'inventaire, aux risques, dépendances, hors-périmètre, critères de validation et plan de travail.
- **VER-LIFECYCLE-002** — Une phase importante ne commence pas directement par des modifications fonctionnelles dispersées sans cadrage. - **VER-LIFECYCLE-002** — Une phase importante ne commence pas directement par des modifications fonctionnelles dispersées sans cadrage.
- **VER-LIFECYCLE-003** — La dernière prerelease d'une phase est prioritairement consacrée aux validations finales, écarts résiduels, documentation finale, synthèse changelog et prompt de reprise. - **VER-LIFECYCLE-003** — La dernière prerelease avant `rel.NNN` est une tranche de préparation de publication minimale. Hors `Cargo.toml` et delta obligatoires, elle ne modifie que le prompt de démarrage de la release suivante, `CHANGELOG.md` et `ROADMAP.md`.
- **VER-LIFECYCLE-004** — Le document de planification établi ou révisé pendant `pre.001` d'une version détaille une prévision souple des prereleases de cette version : objectifs de chaque tranche, ordre envisagé, dépendances, validations et éventuels hors-périmètre. Cette prévision peut être réorganisée lorsque la réflexion ou le développement le justifie ; le delta trace ces changements. - **VER-LIFECYCLE-004** — Le document de planification établi ou révisé pendant `pre.001` d'une version détaille une prévision souple des prereleases de cette version : objectifs de chaque tranche, ordre envisagé, dépendances, validations et éventuels hors-périmètre. Cette prévision peut être réorganisée lorsque la réflexion ou le développement le justifie ; le delta trace ces changements.
- **VER-LIFECYCLE-005** — Le `ROADMAP.md` n'est pas obligé de reprendre une entrée par prerelease. Il décrit la trajectoire globale ; le plan de version porte le découpage prévisionnel plus fin des prereleases. - **VER-LIFECYCLE-005** — Le `ROADMAP.md` n'est pas obligé de reprendre une entrée par prerelease. Il décrit la trajectoire globale ; le plan de version porte le découpage prévisionnel plus fin des prereleases.
- **VER-LIFECYCLE-006** — La prerelease immédiatement antérieure à la dernière prerelease de publication est dédiée à la réconciliation documentaire finale : plan, validation, README, USAGE et autres documents durables concernés. Elle ne finalise pas `CHANGELOG.md`, `ROADMAP.md` ni le prompt de la release suivante.
- **VER-LIFECYCLE-007** — Lorsqu'un smoke final, un test live ou un gate réseau/provider est requis, une prerelease technique dédiée précède la prerelease de réconciliation documentaire. Cette tranche peut également porter les graphes et contrôles techniques finaux directement liés au gate, mais elle ne mélange pas la réconciliation README/USAGE ni la préparation de publication.
- **VER-LIFECYCLE-008** — Lorsqu'aucun smoke/gate live final n'est pertinent, la tranche technique dédiée peut être omise ; les deux dernières responsabilités restent obligatoirement séparées entre réconciliation documentaire puis préparation de publication.
- **VER-LIFECYCLE-009** — Un correctif `pre.NNN-fix.MMM` reste strictement dans le périmètre de responsabilité de `pre.NNN`. Un fix de smoke ne corrige pas README/USAGE/CHANGELOG/prompt ; un fix documentaire ne contient pas de nouveau smoke/runtime ; un fix de publication ne contient pas de correction documentaire durable hors `CHANGELOG.md`, `ROADMAP.md` et prompt suivant.
- **VER-LIFECYCLE-010** — Si une anomalie appartenant à un couloir antérieur est découverte après son franchissement, elle ouvre une nouvelle prerelease dédiée à cette responsabilité au lieu d'être mélangée au fix de la tranche courante. Les couloirs postérieurs sont ensuite rejoués si nécessaire afin que la dernière prerelease reste une préparation de publication minimale.
- **VER-LIFECYCLE-011** — Le plan établi en `pre.001` réserve explicitement, dans sa prévision souple, le gate technique/live éventuel, la réconciliation documentaire et la préparation de publication. La numérotation peut évoluer, mais l'ordre de ces responsabilités ne doit pas être fusionné pour raccourcir artificiellement la release.
- **VER-LIFECYCLE-012** — Une livraison `rel.NNN` effectue la mécanique de publication stable et ne sert pas de tranche de rattrapage. Tout nouveau défaut fonctionnel, test live manquant, correction README/USAGE/validation, ou préparation CHANGELOG/ROADMAP/prompt non achevée renvoie vers une prerelease appropriée avant `rel.NNN`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/000-README.md --> <!-- file: docs/validation/000-README.md -->
<!-- version: 23 --> <!-- version: 24 -->
# Validations KSP # Validations KSP
@@ -20,3 +20,4 @@ Documents :
- [`009-V0_2_6_WALLET_DESK_COMPLIANCE.md`](009-V0_2_6_WALLET_DESK_COMPLIANCE.md) — matrice finale validée de la release stable `0.2.6`, couvrant Wallet Desk, les wires V1/V2, la migration explicite, le runtime Tauri packagé, les frontières sécurité/ownership et le gate opérateur `pre.018-fix.001` avec build final Linux vert. - [`009-V0_2_6_WALLET_DESK_COMPLIANCE.md`](009-V0_2_6_WALLET_DESK_COMPLIANCE.md) — matrice finale validée de la release stable `0.2.6`, couvrant Wallet Desk, les wires V1/V2, la migration explicite, le runtime Tauri packagé, les frontières sécurité/ownership et le gate opérateur `pre.018-fix.001` avec build final Linux vert.
- [`010-V0_2_7_ONCHAIN_WEBSOCKET.md`](010-V0_2_7_ONCHAIN_WEBSOCKET.md) — matrice finale validée de la release stable `0.2.7`, ouverte par `pre.001`, fermée techniquement par `pre.014` puis publiée par `rel.001` : inventaire 9 subscribe + 9 unsubscribe, lifecycle borné, statuts unstable, compliance 18/18, non-régression HTTP 52+14, composition Config V2, smoke WebSocket Devnet et audit de dépendances. - [`010-V0_2_7_ONCHAIN_WEBSOCKET.md`](010-V0_2_7_ONCHAIN_WEBSOCKET.md) — matrice finale validée de la release stable `0.2.7`, ouverte par `pre.001`, fermée techniquement par `pre.014` puis publiée par `rel.001` : inventaire 9 subscribe + 9 unsubscribe, lifecycle borné, statuts unstable, compliance 18/18, non-régression HTTP 52+14, composition Config V2, smoke WebSocket Devnet et audit de dépendances.
- [`011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md`](011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md) — matrice finale validée de la release stable `0.2.8`, ouverte par `pre.001`, consolidée jusquà `pre.011` puis publiée par `rel.001` : Helius `account/logs/program/root/signature/slot/slotsUpdates + transaction`, `block/vote` absents, heartbeat provider, sécurité/redaction, lifecycle adversarial, non-régressions HTTP/WS, stratégie smoke live et graphes Cargo finaux. - [`011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md`](011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md) — matrice finale validée de la release stable `0.2.8`, ouverte par `pre.001`, consolidée jusquà `pre.011` puis publiée par `rel.001` : Helius `account/logs/program/root/signature/slot/slotsUpdates + transaction`, `block/vote` absents, heartbeat provider, sécurité/redaction, lifecycle adversarial, non-régressions HTTP/WS, stratégie smoke live et graphes Cargo finaux.
- [`012-V0_2_9_YELLOWSTONE_GRPC.md`](012-V0_2_9_YELLOWSTONE_GRPC.md) — matrice active ouverte par `0.2.9-pre.001` : service Geyser courant, `Subscribe` standard, 7 unary, filtres/updates, dépendances/licence, replay/backpressure, Config V3 et interopérabilité PublicNode/OrbitFlare.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md --> <!-- file: docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md -->
<!-- version: 17 --> <!-- version: 18 -->
# Validation `0.2.7` — WebSocket Solana standard # Validation `0.2.7` — WebSocket Solana standard
@@ -34,7 +34,7 @@ Pour une paire unstable, l'unsubscribe associé est classé `Unstable pair` dans
## 2. Matrice exhaustive des 18 opérations ## 2. Matrice exhaustive des 18 opérations
| # | Méthode | Type | Statut `pre.001` | Paramètres / résultat essentiels | Notification / paire | Stratégie de test | Source officielle | Compliance | | # | Méthode | Type | Statut `pre.001` | Paramètres / résultat essentiels | Notification / paire | Stratégie de test | Source officielle | Compliance |
|---:|---------------------------|-------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------|---------------------------------------------------------------|-----------------------------------------------------------------|-------------------| |---:|---------------------------|-------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------|---------------------------------------------------------------|-----------------------------------------------------------------|----------------|
| 1 | `accountSubscribe` | subscribe | Stable/documented | pubkey ; config `commitment`, `encoding`, `dataSlice` ; result numeric id ; `minContextSlot` upstream actuellement ignoré, donc non promis | `accountNotification` | fixture encodings/config + subscribe/notify | `https://solana.com/docs/rpc/websocket/accountsubscribe` | Done `pre.009` | | 1 | `accountSubscribe` | subscribe | Stable/documented | pubkey ; config `commitment`, `encoding`, `dataSlice` ; result numeric id ; `minContextSlot` upstream actuellement ignoré, donc non promis | `accountNotification` | fixture encodings/config + subscribe/notify | `https://solana.com/docs/rpc/websocket/accountsubscribe` | Done `pre.009` |
| 2 | `accountUnsubscribe` | unsubscribe | Stable/documented | remote id ; `true` or RPC error unknown id | account pair | handle local -> remote id fixture | `https://solana.com/docs/rpc/websocket/accountunsubscribe` | Done `pre.009` | | 2 | `accountUnsubscribe` | unsubscribe | Stable/documented | remote id ; `true` or RPC error unknown id | account pair | handle local -> remote id fixture | `https://solana.com/docs/rpc/websocket/accountunsubscribe` | Done `pre.009` |
| 3 | `blockSubscribe` | subscribe | **Unstable** | `all`/mentions filter ; confirmed/finalized ; encoding ; tx details ; max tx version ; showRewards | `blockNotification` | all options + null block/error + validator capability fixture | `https://solana.com/docs/rpc/websocket/blocksubscribe` | Done `pre.011` | | 3 | `blockSubscribe` | subscribe | **Unstable** | `all`/mentions filter ; confirmed/finalized ; encoding ; tx details ; max tx version ; showRewards | `blockNotification` | all options + null block/error + validator capability fixture | `https://solana.com/docs/rpc/websocket/blocksubscribe` | Done `pre.011` |
@@ -207,7 +207,7 @@ Les votes observés sont gossip/pre-consensus ; aucune garantie d'entrée dans l
## 7. Threat/security compliance initiale ## 7. Threat/security compliance initiale
| Invariant | Preuve attendue | Statut | | Invariant | Preuve attendue | Statut |
|---------------------------------------------------|----------------------------------------------|--------------------------------------------------------| |---------------------------------------------------|----------------------------------------------|----------------------------------------------------|
| URL/credentials absents de `Debug` | unit tests URL wrapper | **Done `pre.002`** | | URL/credentials absents de `Debug` | unit tests URL wrapper | **Done `pre.002`** |
| URL/credentials absents des erreurs | validation URL + connection errors safe | **Done through `pre.004`** | | URL/credentials absents des erreurs | validation URL + connection errors safe | **Done through `pre.004`** |
| URL/credentials absents des logs | actor logs only safe endpoint metadata | **Done through `pre.011`, source audit `pre.013`** | | URL/credentials absents des logs | actor logs only safe endpoint metadata | **Done through `pre.011`, source audit `pre.013`** |

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md --> <!-- file: docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md -->
<!-- version: 25 --> <!-- version: 26 -->
# Validation `0.2.8` — Helius LaserStream WebSocket # Validation `0.2.8` — Helius LaserStream WebSocket

View File

@@ -0,0 +1,468 @@
<!-- file: docs/validation/012-V0_2_9_YELLOWSTONE_GRPC.md -->
<!-- version: 27 -->
# Validation `0.2.9` — Yellowstone gRPC standard + PublicNode
**Statut courant : la validation technique `pre.013-fix.004` est fermée et verte : `clippy --all-targets`, workspace, graphes Cargo, puis smoke PublicNode authentifié Mainnet + Testnet `2/2 PASS`. `pre.014` réconcilie et ferme cette matrice documentaire ; seule la tranche `pre.015` de préparation de publication restera avant `rel.001`.**
## 1. Autorités et baseline courante
Autorités :
```text
RULES.md + docs/rules/*
plan 016 courant
code réel Transport/Config
prompts/014-V0_2_9_START_PROMPT.md pour les contraintes non supersédées
deltas/0.2.9/* pour l'historique immuable et les décisions postérieures au prompt
upstream Yellowstone primaire
```
Baseline opérateur immédiatement avant `pre.012` (`0.2.9-pre.011`) :
| Gate | Résultat |
|------------------------------------------|--------------------------|
| `cargo fmt --all` | PASS |
| audit Rust workspace | PASS, 0 candidate export |
| `cargo check --workspace` | PASS |
| `cargo clippy --workspace --all-targets` | PASS sans warning |
| Config unit | 113/113 PASS |
| Config public API | 15/15 PASS |
| Config ownership | 5/5 PASS |
| Transport unit | 383/383 PASS |
| Transport public API | 49/49 PASS |
| Transport release completeness | 43/43 PASS |
| Transport doctests | 4/4 PASS |
| workspace dependency canary | 3/3 PASS |
| `cargo test --workspace` | PASS |
Cette baseline est le seuil de non-régression de `pre.012`.
## 2. Gate dépendances et licence
| Exigence | Décision / preuve | État |
|--------------------------------|------------------------------------------|----------|
| proto officiel sans vendoring | `yellowstone-grpc-proto ^12.6` | **PASS** |
| pas de client upstream runtime | `yellowstone-grpc-client` absent | **PASS** |
| runtime Tonic KSP-owned | `tonic ^0.14` + `tonic-prost ^0.14` | **PASS** |
| proto subtree compatible | Apache-2.0 selon `LICENSING.md` upstream | **PASS** |
| source AGPL copiée | aucune | **PASS** |
| raw client Tonic public | aucun | **PASS** |
| graph final | graphes `pre.013` inspectés | **PASS** |
Le graphe de dépendances n'est pas modifié par `pre.012`; aucune nouvelle dépendance Cargo n'est introduite dans cette tranche.
## 3. Matrice service `Geyser`
| RPC | Classification | Cible | Preuve | Verdict |
|-----------------------|--------------------------------|-------|---------------------------------|----------|
| `Subscribe` | standard | oui | stream bidi local + lifecycle | **PASS** |
| `SubscribeDeshred` | extension/pré-exécution Triton | non | exclusion documentée | **OUT** |
| `SubscribeReplayInfo` | standard unary | oui | fixture unary + reconnect tests | **PASS** |
| `Ping` | standard unary | oui | fixture exact echo | **PASS** |
| `GetLatestBlockhash` | standard unary | oui | fixture typed | **PASS** |
| `GetBlockHeight` | standard unary | oui | fixture typed | **PASS** |
| `GetSlot` | standard unary | oui | fixture typed | **PASS** |
| `IsBlockhashValid` | standard unary | oui | fixture typed + invalid input | **PASS** |
| `GetVersion` | standard unary | oui | fixture typed | **PASS** |
## 4. `SubscribeRequest` — coverage normative
### 4.1 Top-level
| Champ | Verdict | Preuve minimale |
|-----------------------|----------|----------------------------------|
| `accounts` | **PASS** | maps typed + wire exact |
| `slots` | **PASS** | maps typed + wire exact |
| `transactions` | **PASS** | maps typed + wire exact |
| `transactions_status` | **PASS** | same filter family, separate map |
| `blocks` | **PASS** | maps typed + wire exact |
| `blocks_meta` | **PASS** | named empty marker preserved |
| `entry` | **PASS** | named empty marker preserved |
| `commitment` | **PASS** | optional wire semantics |
| `accounts_data_slice` | **PASS** | order + bounds |
| `ping` | **PASS** | optional id |
| `from_slot` | **PASS** | optional + replay mutation |
### 4.2 Accounts
```text
account[]
owner[]
filters[]
nonempty_txn_signature?
cuckoo_accounts_filter?
memcmp bytes/base58/base64
datasize
token_account_state
lamports eq/ne/lt/gt
```
Verdict : **PASS** — wire complet retenu, bounds déterministes, payload Debug redacted, malformed fixed-width rejeté.
### 4.3 Slots
```text
filter_by_commitment?
interslot_updates?
processed
confirmed
finalized
first_shred_received
completed
created_bank
dead + dead_error?
```
Verdict : **PASS**.
### 4.4 Transactions et transaction_status
```text
vote?
failed?
signature?
account_include[]
account_exclude[]
account_required[]
cuckoo_account_include?
token_accounts? = ALL | BALANCE_CHANGED
```
Verdict : **PASS** — request wire, transaction storage/meta, status error et malformed signature couverts.
### 4.5 Blocks, block_meta, entry
```text
account_include[]
include_transactions?
include_accounts?
include_entries?
cuckoo_account_include?
blocks_meta marker
entry marker
```
Verdict : **PASS** — block complet, metadata, entries et champs optional/legacy couverts.
## 5. `SubscribeUpdate` — coverage normative
| Variante | Verdict |
|--------------------------------------|----------|
| account | **PASS** |
| slot | **PASS** |
| transaction | **PASS** |
| transaction_status | **PASS** |
| block | **PASS** |
| ping | **PASS** |
| pong | **PASS** |
| block_meta | **PASS** |
| entry | **PASS** |
| top-level `filters[]` / `created_at` | **PASS** |
Les raw protobufs ne sortent pas de la façade publique. Les décodeurs rejettent les formes structurellement invalides sans copier des payloads arbitraires dans les erreurs.
## 6. Settings, bounds et redaction
| Contrôle | État |
|-----------------------------------------------|--------------------|
| URL `http/https` seulement et longueur bornée | **PASS** |
| URL Debug redacted | **PASS** |
| metadata key/value/count bornés | **PASS** |
| metadata secret/public distincte | **PASS** Transport |
| timeouts non nuls et bornés | **PASS** |
| reconnect bounds cohérents | **PASS** |
| inbound/outbound message sizes | **PASS** |
| request/update queue capacities | **PASS** |
| filter group/name bounds | **PASS** |
| account/owner/memcmp/data slice bounds | **PASS** |
| transaction/block selectors bounds | **PASS** |
| remote `Status` message/details non recopiés | **PASS** |
| Transport -> env/config | absent ou **PASS** |
## 7. Lifecycle, backpressure et replay
| Exigence | Verdict | Preuve |
|------------------------------|-----------------|------------------------------------------------|
| stream bidi unique | **PASS** | round-trip fixture |
| mutation request | **PASS** | same request channel |
| server Ping / client reply | **PASS** | actor test |
| Pong decode | **PASS** | update decode |
| server half-close | **PASS** | state terminal observable |
| explicit client close | **PASS** | half-close avant deadline |
| hostile server shutdown | **PASS** | close timeout borné |
| slow receiver | **PASS** | overflow terminal observable |
| oversized inbound | **PASS** | Tonic decoder bound |
| oversized outbound | **PASS** | reject avant queue dispatch |
| reconnect backoff | **PASS** | budget borné |
| shutdown pendant backoff | **PASS** | interruption sans nouvelle connexion |
| resubscribe déterministe | **PASS** | last accepted full request |
| reprise `from_slot` | **PASS** | dernier slot observé + demande explicite |
| ReplayInfo `first_available` | **PASS** | clamp/gap seulement si prouvé |
| duplicate observability | **PASS** | compteur borné, pas de suppression silencieuse |
| exactly-once/lossless | **NON GARANTI** | contrat explicite |
Snapshot public requis et présent :
```text
reconnect_count
replay_attempt_count
continuity_gap_count
duplicate_update_count
last_requested_from_slot
last_observed_slot
state/error code safe
```
## 8. Gate Config V3 — `pre.011`
### 8.1 Schema et backward compatibility
Le gate opérateur `pre.011` ferme les exigences suivantes :
| Exigence | Preuve | Verdict |
|------------------------------------------|-----------------------------------------|----------|
| schema `$id` V3 | `urn:ksp:schema:std.transport:v3` | **PASS** |
| branches V1/V2 conservées | fixtures V1, V2 et document committé | **PASS** |
| V1 HTTP-only | `v1_transport_fixture...` | **PASS** |
| V2 HTTP+WS | fixture V2 + compatibilité constructeur | **PASS** |
| V3 HTTP+WS+gRPC | 113 tests Config | **PASS** |
| `grpc_endpoints` optionnel par profil V3 | profil `devnet_public` sans gRPC | **PASS** |
| propriétés inconnues refusées | branches schema strictes | **PASS** |
### 8.2 Mapping runtime
V3 doit mapper :
```text
grpc_defaults -> YellowstoneGrpcSessionSettings
grpc_endpoints[].url -> YellowstoneGrpcEndpointUrl
provider -> YellowstoneGrpcProviderName
cluster -> YellowstoneGrpcClusterName
protocol = solana_yellowstone -> gate Config explicite
metadata -> YellowstoneGrpcMetadataEntry::public
secret_metadata -> YellowstoneGrpcMetadataEntry::secret
session overrides -> merge avec grpc_defaults
```
API :
| Surface | Exigence | Statut source |
|---------------------------------|-------------------------------------|---------------|
| `http_settings()` | inchangée | **OK** |
| `ws_settings()` | V1 None, V2/V3 selon profil | **OK** |
| `grpc_settings()` | V1/V2 None, V3 optionnel | **PASS** |
| `into_transport_settings()` | tuple historique HTTP + WS inchangé | **PASS** |
| `into_all_transport_settings()` | nouveau tuple HTTP + WS + gRPC | **PASS** |
### 8.3 Provenance et secrets
Règles validées :
```text
metadata publique + provenance KSP_SECRET_* -> reject
secret_metadata sans provenance secret -> reject
secret_metadata + variable KSP_PUBLIC_/KSP_* -> reject
secret_metadata + KSP_SECRET_/KSPB_SECRET_ -> accept
segments littéraux autour du secret -> accept
safe_value -> secret segment ********
Transport Debug -> URL/metadata secret absents
```
Les tests V3 démontrent le cas valide et les deux croisements invalides sans exposer les canaris.
### 8.4 PublicNode Mainnet
Profil committé et validé :
```text
profile_id = publicnode_mainnet
provider = publicnode
cluster = mainnet-beta
protocol = solana_yellowstone
url = https://solana-yellowstone-grpc.publicnode.com:443
secret_metadata = x-token <- ${KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN}
```
Le mapping produit un endpoint TLS Yellowstone standard et une metadata secrète redacted. Le smoke final ouvre `Subscribe`, reçoit un update `Slot` non nul et ferme de manière bornée : **PASS**.
### 8.5 PublicNode Testnet
Profil committé et validé :
```text
profile_id = publicnode_testnet
provider = publicnode
cluster = testnet
protocol = solana_yellowstone
url = https://solana-testnet-yellowstone-grpc.publicnode.com:443
secret_metadata = x-token <- ${KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN}
```
Le hostname exact a été fourni par l'opérateur puis validé par le smoke live. `Subscribe` reçoit un update `Slot` non nul : **PASS**.
Le même personal token opérateur a été validé sur les deux réseaux. Les deux variables KSP restent distinctes afin de permettre des valeurs différentes si la policy provider change ; cette modélisation ne constitue pas une preuve que les tokens PublicNode sont network-scoped.
## 9. Provider-neutrality
| Point | Verdict |
|-----------------------------------------------|------------------|
| type public `PublicNodeGrpc*` sans divergence | absent, **PASS** |
| protocol standard encodé comme provider | non, **PASS** |
| provider descriptif séparé | oui, **PASS** |
| auth PublicNode hardcodée dans Transport | non, **PASS** |
| Helius/OrbitFlare runtime gRPC dans `0.2.9` | non, **PASS** |
| provider extensions dans N2 | aucune, **PASS** |
## 10. Non-régressions obligatoires
Le gate final doit préserver :
```text
HTTP current typed 52/52
HTTP historical 14/14 Deprecated/Removed
KSP-TRANSPORT-007 vert
Standard WebSocket 9 familles / 18 opérations
Helius LaserStream WebSocket 7 familles standard + transaction + slotsUpdates, heartbeat provider-owned
Transport -> Config interdit
Transport -> std::env KSP_* interdit
tracing direct Transport interdit
```
La baseline `pre.011` reste conservée. Les changements `pre.013` ont porté uniquement le gate live, la Config PublicNode/Testnet et le harness de smoke ; le gate final `pre.013-fix.004` confirme les non-régressions workspace.
## 11. Historique des gates fermé
Les détails de commandes, warnings corrigés et fichiers exacts restent dans leurs deltas immuables.
| Tranche | Gate consolidé |
|-----------------------------------|---------------------------------------|
| `pre.001` + `fix.001` + `fix.002` | audit/sizing/providers/licence fermé |
| `pre.002` + `fix.001` + `fix.002` | moteur/settings/channel fermé |
| `pre.003` + `fix.001` | TLS/metadata/unary fermé |
| `pre.004` + `fix.001` | Subscribe common fermé |
| `pre.005` + `fix.001` | Accounts/Slots fermé |
| `pre.006` | namespace HTTP fermé |
| `pre.007` | Transactions fermé |
| `pre.008` + `fix.001` | Blocks fermé |
| `pre.009` + `fix.001` | bidi/backpressure fermé |
| `pre.010` + `fix.001` | reconnect/replay fermé sans warning |
| `pre.011` | Config V3/PublicNode mapping fermé |
| `pre.012` + `fix.001` | réaudit + règles de fermeture |
| `pre.013` + `fix.001``fix.004` | live PublicNode + graphes fermé |
| `pre.014` | réconciliation documentaire candidate |
Cette table remplace les anciens appendices numérotés successivement `19.x`, `20`, `21`, etc. qui rendaient le document ambigu.
## 12. Verdict opérateur consolidé jusqu'à `pre.013-fix.004`
Le gate technique final reçu le 2026-08-24 est :
| Exigence | Résultat opérateur |
|-----------------------------------------------|--------------------------------------|
| `cargo fmt --all` | **PASS** |
| audit Rust workspace | **PASS, 0 candidate export** |
| audit Markdown | **PASS, 87 tableaux / 258 fichiers** |
| `cargo check --workspace` | **PASS** |
| `cargo clippy --workspace --all-targets` | **PASS sans warning** |
| Transport unit | **383/383 PASS** |
| Transport public API | **49/49 PASS** |
| Transport release completeness | **43/43 PASS** |
| Transport doctests | **4/4 PASS** |
| Config unit dans workspace | **113/113 PASS** |
| Config ownership dans workspace | **5/5 PASS** |
| Config public API dans workspace | **15/15 PASS** |
| workspace dependencies | **3/3 PASS** |
| `cargo test --workspace` | **PASS** |
| graphes Cargo finaux | **PASS / inspectés** |
| smoke PublicNode Mainnet `Subscribe` + `Slot` | **PASS** |
| smoke PublicNode Testnet `Subscribe` + `Slot` | **PASS** |
| smoke live total | **2/2 PASS** |
Les graphes résolus confirment la stack attendue :
```text
yellowstone-grpc-proto 12.6.0
tonic 0.14.6
tonic-prost 0.14.6
prost 0.14.4
prost-types 0.14.4
yellowstone-grpc-client absent du runtime KSP
```
Les graphes ont été exécutés avant `pre.013-fix.004`; ils restent valides car `fix.004` n'a modifié ni dépendance ni feature Cargo.
## 13. Verdict PublicNode live final
Les essais intermédiaires ont établi deux faits utiles : les appels sans credential peuvent atteindre le service mais recevoir `PERMISSION_DENIED`, et la validation représentative de la foundation doit porter sur `Subscribe` plutôt que sur une unary provider éventuellement restreinte.
Le smoke final :
```text
reçoit deux personal tokens par stdin, Mainnet puis Testnet
accepte que les deux lignes contiennent la même valeur
construit une metadata secrète x-token
ouvre TLS puis Subscribe
filtre slots
attend un YellowstoneSubscribeUpdate::Slot sous timeout
exige slot > 0
ferme de manière bornée
n'expose aucun secret dans URL/Debug/arguments
```
Résultat opérateur final : **Mainnet PASS, Testnet PASS, 2/2**.
Le timeout KSP de half-close est un résultat accepté par le harness uniquement après réception prouvée d'un slot ; le runtime conserve son contrat de timeout explicite.
## 14. Gate `pre.014` — réconciliation documentaire
Cette tranche ne modifie ni runtime, ni Config, ni schema, ni smoke. Elle synchronise le plan, la présente validation, README/USAGE Transport et les références durables avec les faits exécutés en `pre.013`.
Le verdict technique de `0.2.9` est donc **PASS**. Après le gate documentaire `pre.014`, seuls le prompt suivant, `CHANGELOG.md` et `ROADMAP.md` restent à finaliser en `pre.015`.
## 15. Gate `pre.015` — préparation de publication
Payload fonctionnel autorisé :
```text
prompts/015-V0_2_10_START_PROMPT.md
CHANGELOG.md
ROADMAP.md
```
Seuls les fichiers mécaniques de version et de traçabilité (`Cargo.toml`, delta `pre.015`) peuvent s'ajouter à ce payload. Aucun README, USAGE, plan, validation, règle, code, test, schema ou config ne doit être corrigé dans `pre.015`.
Si un tel défaut est découvert, une nouvelle prerelease dédiée est ouverte puis la préparation de publication est rejouée sous un nouveau numéro.
## 16. Verdict stable attendu avant `rel.001`
`0.2.9` peut passer à `rel.001` seulement après la séquence suivante :
```text
pre.013 technique/live fermé
pre.014 réconciliation documentaire fermée
pre.015 prompt/CHANGELOG/ROADMAP fermé
```
Et si tous les invariants suivants sont vrais :
```text
standard vs extension explicitement classifié
SubscribeDeshred OUT documenté
7 unary verts
Subscribe + 9 updates verts
resource/backpressure/lifecycle verts
reconnect/replay sans promesse lossless
Config V3 backward V1/V2
provider/protocol distincts
PublicNode Mainnet live Subscribe + Slot PASS
PublicNode Testnet live Subscribe + Slot PASS
x-token secret requis par les profils PublicNode, aucun secret versionné
HTTP/WS/Helius non régressés
dependency firewall vert
cargo graphs inspectés
README/USAGE réconciliés avant la dernière pre
validation 012 fermée avant la dernière pre
prompt/CHANGELOG/ROADMAP seuls dans la dernière pre
workspace final vert
```

View File

@@ -1,5 +1,5 @@
<!-- file: prompts/000-README.md --> <!-- file: prompts/000-README.md -->
<!-- version: 25 --> <!-- version: 26 -->
# Prompts KSP # Prompts KSP
@@ -35,3 +35,4 @@ Le prompt générique `0.1.x` a été affiné pendant `0.0.3` puis remplacé par
- [`012-V0_2_7_START_PROMPT.md`](012-V0_2_7_START_PROMPT.md) — prompt réaligné par `0.2.6-pre.015` puis renforcé en contrat de reprise autonome par `0.2.6-pre.018-fix.002`; prompt historique consommé pour ouvrir `0.2.7 — WebSocket Solana standard` depuis `v0.2.6`, avec lectures/règles ordonnées, audit officiel et historique, threat-model session/subscription/reconnect/backpressure, matrice de compliance, gate `pre.001` strict et prévision souple de prereleases avant toute implémentation lourde. - [`012-V0_2_7_START_PROMPT.md`](012-V0_2_7_START_PROMPT.md) — prompt réaligné par `0.2.6-pre.015` puis renforcé en contrat de reprise autonome par `0.2.6-pre.018-fix.002`; prompt historique consommé pour ouvrir `0.2.7 — WebSocket Solana standard` depuis `v0.2.6`, avec lectures/règles ordonnées, audit officiel et historique, threat-model session/subscription/reconnect/backpressure, matrice de compliance, gate `pre.001` strict et prévision souple de prereleases avant toute implémentation lourde.
- [`013-V0_2_8_START_PROMPT.md`](013-V0_2_8_START_PROMPT.md) — prompt historique consommé pour ouvrir puis conduire la release stable `0.2.8 — Helius LaserStream WebSocket` depuis `v0.2.7`; préparé par `0.2.7-pre.014` puis renforcé en version 2 par `pre.014-fix.001`, il imposait relecture des règles, réaudit Helius, gate `pre.001` strict audit/brainstorming/sizing, actor standard partagé, frontières provider/Config/secrets et forecast souple avant implémentation lourde. - [`013-V0_2_8_START_PROMPT.md`](013-V0_2_8_START_PROMPT.md) — prompt historique consommé pour ouvrir puis conduire la release stable `0.2.8 — Helius LaserStream WebSocket` depuis `v0.2.7`; préparé par `0.2.7-pre.014` puis renforcé en version 2 par `pre.014-fix.001`, il imposait relecture des règles, réaudit Helius, gate `pre.001` strict audit/brainstorming/sizing, actor standard partagé, frontières provider/Config/secrets et forecast souple avant implémentation lourde.
- [`014-V0_2_9_START_PROMPT.md`](014-V0_2_9_START_PROMPT.md) — prompt actif après publication `0.2.8-rel.001` et tag stable `v0.2.8`, préparé par `0.2.8-pre.011`; il ouvre `0.2.9 — Yellowstone gRPC standard/provider-neutral` et impose la base stable autoritaire, la relecture ordonnée des règles/architectures/compliances Transport, un réaudit Yellowstone actuel (service/proto/releases/crates), un audit dépendances/licences/MSRV avant choix du client/proto, la séparation standard/provider/deshred, un gate `pre.001` strict audit/brainstorming/sizing et une prévision souple détaillée avant toute implémentation gRPC lourde. - [`014-V0_2_9_START_PROMPT.md`](014-V0_2_9_START_PROMPT.md) — prompt actif après publication `0.2.8-rel.001` et tag stable `v0.2.8`, préparé par `0.2.8-pre.011`; il ouvre `0.2.9 — Yellowstone gRPC standard/provider-neutral` et impose la base stable autoritaire, la relecture ordonnée des règles/architectures/compliances Transport, un réaudit Yellowstone actuel (service/proto/releases/crates), un audit dépendances/licences/MSRV avant choix du client/proto, la séparation standard/provider/deshred, un gate `pre.001` strict audit/brainstorming/sizing et une prévision souple détaillée avant toute implémentation gRPC lourde.
- [`015-V0_2_10_START_PROMPT.md`](015-V0_2_10_START_PROMPT.md) — prompt préparé par `0.2.9-pre.012` pour ouvrir `0.2.10 — OrbitFlare Yellowstone gRPC` depuis la future base stable `v0.2.9`; il impose la réutilisation du moteur Yellowstone N1 et du standard N2, un gate `pre.001` daudit actuel des endpoints/auth/capabilities/heartbeat OrbitFlare, la distinction control-plane/data-plane/IP whitelist/credentials, le sizing avant toute extension provider et la non-régression PublicNode/Yellowstone standard.

File diff suppressed because it is too large Load Diff

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