Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| afaa9c0239 | |||
| 7d456b41dc | |||
| 0edeed1c48 | |||
| be5e3464ee | |||
| 1967e845b0 | |||
| af807afff5 | |||
| 7f567bc1bb | |||
| 09aacc36ab | |||
| 15b129ad04 | |||
| 6ddf2b4995 | |||
| dcabceb52f | |||
| 51bcf870d3 | |||
| c6c5793c61 | |||
| d98ee736be | |||
| 39dbd86f1e | |||
| c938b2409e | |||
| 5b30bb9948 | |||
| 813a45385a | |||
| 21ed5f88da | |||
| 3198cc5666 | |||
| 4568d94a62 | |||
| d32d8de3a5 | |||
| a9fb6a7ac8 | |||
| 433e69272a | |||
| affa18a6e9 | |||
| 05aec88178 | |||
| b32de48c0b | |||
| 15e3402e1c | |||
| dca0a4b809 | |||
| 20320dba7b | |||
| e4bbc78a41 | |||
| feb9befb35 | |||
| a038194679 | |||
| 3172cda241 | |||
| 61fba107ef | |||
| 835de48cb7 | |||
| 21de590aea | |||
| 3081bd1085 | |||
| bdcab1e1a7 | |||
| d70c3a1672 | |||
| 7cdf5e80c9 | |||
| d8bfd7cd2e | |||
| 9c0d4fc197 | |||
| 7c12ec886b | |||
| 7eb6dec809 | |||
| 68f4384c5b | |||
| 9cc140fb84 | |||
| 3b64d1e0ec | |||
| 56b9ce6abc | |||
| 1c8d69778b | |||
| 8e739b9e55 | |||
| 53dbb5bccd | |||
| 92224e5ac6 | |||
| cbb4e7b0de | |||
| 38fd62c256 | |||
| f2a3ec62aa | |||
| b3363073c4 | |||
| c94a54f3e3 | |||
| 0871b85df9 | |||
| 315e7e67e5 | |||
| df95f2f558 | |||
| f0865d5137 |
20
.env.example
20
.env.example
@@ -1,5 +1,5 @@
|
|||||||
# file: .env.example
|
# file: .env.example
|
||||||
# version: 5
|
# version: 9
|
||||||
|
|
||||||
# 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,9 +34,21 @@ 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
|
||||||
|
|
||||||
# Optional complete private-provider WebSocket endpoint URL used only by the Transport example when explicitly selected.
|
# PublicNode Mainnet personal token used as secret x-token metadata by the committed Mainnet Yellowstone gRPC profile.
|
||||||
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
|
# 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_SOLANA_WS_URL=wss://provider.example/?api-key=replace-me
|
# 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
|
||||||
|
|
||||||
|
# OrbitFlare Solana Free License Key used as secret x-token metadata by the committed Devnet Yellowstone gRPC profile.
|
||||||
|
# Keep the real ORBIT-* value only in the process environment or local .env; do not substitute the Customer API X-ORBIT-KEY.
|
||||||
|
# KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN=replace-me
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
# KSP_SECRET_HELIUS_API_KEY=replace-me
|
||||||
|
|
||||||
# Fade-in duration in milliseconds used by the common KSP desk splash lifecycle.
|
# Fade-in duration in milliseconds used by the common KSP desk splash lifecycle.
|
||||||
KSP_DESK_SPLASH_FADE_IN_MS=300
|
KSP_DESK_SPLASH_FADE_IN_MS=300
|
||||||
|
|||||||
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,10 +1,32 @@
|
|||||||
<!-- file: CHANGELOG.md -->
|
<!-- file: CHANGELOG.md -->
|
||||||
<!-- version: 11 -->
|
<!-- version: 14 -->
|
||||||
|
|
||||||
# Changelog KSP
|
# Changelog KSP
|
||||||
|
|
||||||
|
## 0.2.10 — OrbitFlare Yellowstone gRPC — 2026-08-25
|
||||||
|
|
||||||
|
`0.2.10` ajoute OrbitFlare comme provider Yellowstone gRPC sur le moteur N1 et le standard N2 acquis dans `0.2.9`, sans modifier le moteur gRPC ni créer de façade provider inutile. L'audit et le smoke live ont établi le contrat réel du service Solana Free Devnet : endpoint `http://devnet.rpc.orbitflare.com:10000`, authentification par metadata gRPC secrète `x-token` portant la License Key OrbitFlare, et émission du Ping Yellowstone standard. Config Transport V3 représente ce provider avec `provider = orbitflare`, `cluster = devnet`, `protocol = solana_yellowstone` et `secret_metadata`, tandis que `.env.example` inventorie `KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN` sans valeur réelle.
|
||||||
|
|
||||||
|
La première tentative live sans metadata a volontairement échoué avec `Unauthenticated`, ce qui a conduit à `pre.002-fix.001` après audit des sources OrbitFlare actuelles. Le smoke corrigé reçoit la License Key uniquement par stdin opérateur, ouvre `Subscribe`, observe un Slot non nul et un `SubscribeUpdate::Ping`, puis ferme la session proprement. Le même smoke est repassé au gate technique final `pre.003`. La clôture conserve donc N1/N2 inchangés, n'ajoute ni heartbeat OrbitFlare, ni SDK provider, ni Config V4, et confirme que la réponse automatique au Ping standard existante suffit.
|
||||||
|
|
||||||
|
Le gate final conserve l'ensemble du workspace vert : audits Rust/Markdown, `cargo check`, Clippy, tests Config et Transport, `cargo test --workspace`, canaris de dépendances et graphes Cargo. Helius LaserStream gRPC, audité comme largement wire-compatible Yellowstone mais non raisonnablement live-testable sans abonnement payant, est reporté dans les TODO provider sans numéro de release. La séquence active passe donc à `0.2.11 — off-chain price transport`; `prompts/016-V0_2_11_START_PROMPT.md` l'ouvre exclusivement depuis le tag stable `v0.2.10` avec audit actuel des sources de prix, de leurs sémantiques et de leur testabilité avant toute implémentation lourde.
|
||||||
|
|
||||||
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` é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 l’extension 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 l’actor partagé et émet un WebSocket Ping control frame toutes les 60 secondes uniquement pour `WsProtocolKind::HeliusLaserStream`.
|
||||||
|
|
||||||
|
La release ajoute le mapping Config V2 `helius_laserstream`, les profils Helius mainnet/devnet et le secret `KSP_SECRET_HELIUS_API_KEY` avec provenance/redaction segmentaire, sans dépendance inverse Transport -> Config ni lecture directe de l’environnement par Transport. Les canaris couvrent erreurs RPC provider, payload oversized, mismatch de notification, reconnect/remap/unsubscribe races, backpressure isolé et diagnostics sans payload brut. La compliance finale conserve simultanément **52 méthodes HTTP courantes + 14 historiques**, **9 familles / 18 opérations WebSocket Solana standard**, et la surface Helius `7 standard + transaction`. Le smoke Helius live cross-crates est volontairement reporté vers une future surface d’intégration/orchestration afin de préserver l’ownership Config du secret. Les graphes Cargo finaux n’introduisent aucun SDK Helius/gRPC ni nouvelle duplication bloquante. `prompts/014-V0_2_9_START_PROMPT.md` ouvre ensuite `0.2.9 — Yellowstone gRPC standard/provider-neutral` uniquement depuis le tag stable `v0.2.8`, avec audit service/proto/crates/licences/MSRV/features et sizing strict en `pre.001` avant toute implémentation lourde.
|
||||||
|
|
||||||
## 0.2.7 — WebSocket Solana standard — 2026-08-23
|
## 0.2.7 — WebSocket Solana standard — 2026-08-23
|
||||||
|
|
||||||
`0.2.7` stabilise dans `ksp-onchain-transport-lib` le moteur WebSocket Solana standard en complément de la surface HTTP déjà complète. La release couvre exactement les **9 familles subscribe + 9 unsubscribe** de l’inventaire officiel ciblé : `account`, `block`, `logs`, `program`, `root`, `signature`, `slot`, `slotsUpdates` et `vote`. Les wrappers sont typés, les IDs KSP de session/subscription restent locaux et stables, les IDs serveur restent internes/remappables, et plusieurs sessions physiques peuvent coexister explicitement sur la même URL sans introduire de pool/scheduler automatique. Les familles `block`, `slotsUpdates` et `vote` restent identifiées comme unstable selon l’audit normatif courant et utilisent le warning KSP centralisé.
|
`0.2.7` stabilise dans `ksp-onchain-transport-lib` le moteur WebSocket Solana standard en complément de la surface HTTP déjà complète. La release couvre exactement les **9 familles subscribe + 9 unsubscribe** de l’inventaire officiel ciblé : `account`, `block`, `logs`, `program`, `root`, `signature`, `slot`, `slotsUpdates` et `vote`. Les wrappers sont typés, les IDs KSP de session/subscription restent locaux et stables, les IDs serveur restent internes/remappables, et plusieurs sessions physiques peuvent coexister explicitement sur la même URL sans introduire de pool/scheduler automatique. Les familles `block`, `slotsUpdates` et `vote` restent identifiées comme unstable selon l’audit normatif courant et utilisent le warning KSP centralisé.
|
||||||
|
|||||||
28
Cargo.toml
28
Cargo.toml
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 216
|
# version: 268
|
||||||
|
|
||||||
[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.7"
|
version = "0.2.10"
|
||||||
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 }
|
||||||
|
getrandom = { version = "^0.4", default-features = false }
|
||||||
|
http = { version = "^1.5", default-features = false }
|
||||||
|
jsonschema = { version = "^0.51", default-features = false }
|
||||||
|
reqwest = { version = "^0.13", default-features = false }
|
||||||
serde = { version = "^1.0" }
|
serde = { version = "^1.0" }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
jsonschema = { version = "^0.50", default-features = false }
|
|
||||||
reqwest = { version = "^0.13", default-features = false }
|
|
||||||
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]
|
||||||
|
|||||||
25
ROADMAP.md
25
ROADMAP.md
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 82 -->
|
<!-- version: 87 -->
|
||||||
|
|
||||||
# Roadmap KSP
|
# Roadmap KSP
|
||||||
|
|
||||||
@@ -52,12 +52,23 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
|
|||||||
- [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001`–`fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe l’audit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
|
- [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001`–`fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe l’audit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
|
||||||
- [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.
|
||||||
- [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
|
- [X] `0.2.8` — Helius LaserStream WebSocket stable : façade provider dédiée sur l’actor 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 d’update, 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.
|
- [X] `0.2.10` — OrbitFlare Yellowstone gRPC stable : profil Config V3 Devnet, License Key injectée comme metadata secrète `x-token`, smoke live `Subscribe -> Slot + Ping` validé deux fois, sans modification du moteur N1/N2 ni heartbeat provider.
|
||||||
- [ ] `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` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix provider-neutral, au minimum SOL/USD et SOL/EUR, après audit actuel des sémantiques provider et de la testabilité live.
|
||||||
- [ ] `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 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.13` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet.
|
- [ ] `0.2.13` — Introduire la première surface de `ksp-interface-lib`, comprenant une API wire publique utilisable par les implémentations officielles et externes.
|
||||||
|
- [ ] `0.2.14` — Introduire `ksp-program-api` comme premier contrat Program extensible, sans imposer encore `ksp-program-lib` complet.
|
||||||
|
|
||||||
|
### TODO/IDEAS — providers Yellowstone non planifiés
|
||||||
|
|
||||||
|
- [ ] **TODO** — Helius LaserStream gRPC : réauditer lorsque l'accès live gRPC est raisonnablement disponible ; conserver N1/N2 Yellowstone inchangés, vérifier auth/endpoints/Subscribe/Ping/replay/from_slot/erreurs provider et traiter les preprocessed transactions comme extension Helius séparée.
|
||||||
|
- [ ] **TODO** — eRPC : réauditer accès, auth/IP policy, capabilities et produits complémentaires avant toute décision d’implé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 d’implémentation.
|
||||||
|
|
||||||
### Règles Transport pour toute la série
|
### Règles Transport pour toute la série
|
||||||
|
|
||||||
|
|||||||
@@ -90,18 +90,59 @@
|
|||||||
"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}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mainnet_private_ws",
|
"name": "mainnet_helius_ws",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"provider": "private-provider",
|
"provider": "helius",
|
||||||
"cluster": "mainnet-beta",
|
"cluster": "mainnet-beta",
|
||||||
"kind": "solana_standard",
|
"kind": "helius_laserstream",
|
||||||
"url": "${KSP_SECRET_SOLANA_WS_URL:-wss://example.invalid}",
|
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}",
|
||||||
"session": {
|
"session": {
|
||||||
"notification_queue_capacity": 512,
|
"notification_queue_capacity": 512,
|
||||||
"max_active_subscriptions": 2048
|
"max_active_subscriptions": 2048
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "devnet_helius",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "devnet_public",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "devnet",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL:-https://api.devnet.solana.com}",
|
||||||
|
"connect_timeout_ms": 5000,
|
||||||
|
"request_timeout_ms": 15000,
|
||||||
|
"max_idle_connections_per_host": 8,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "default",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"priority": 100,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 5,
|
||||||
|
"burst_capacity": 10,
|
||||||
|
"max_concurrent_requests": 8,
|
||||||
|
"pause_after_rate_limit_ms": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ws_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "devnet_helius_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "helius",
|
||||||
|
"cluster": "devnet",
|
||||||
|
"kind": "helius_laserstream",
|
||||||
|
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": {
|
||||||
@@ -335,7 +338,8 @@
|
|||||||
},
|
},
|
||||||
"kind": {
|
"kind": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"solana_standard"
|
"solana_standard",
|
||||||
|
"helius_laserstream"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
@@ -454,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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": [
|
||||||
{
|
{
|
||||||
@@ -65,6 +79,64 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "orbitflare_devnet",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "solana_devnet_public",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "devnet",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_DEVNET_HTTP_URL:-https://api.devnet.solana.com}",
|
||||||
|
"connect_timeout_ms": 5000,
|
||||||
|
"request_timeout_ms": 15000,
|
||||||
|
"max_idle_connections_per_host": 8,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "default",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"priority": 100,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 5,
|
||||||
|
"burst_capacity": 10,
|
||||||
|
"max_concurrent_requests": 8,
|
||||||
|
"pause_after_rate_limit_ms": 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ws_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "solana_devnet_public_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "devnet",
|
||||||
|
"kind": "solana_standard",
|
||||||
|
"url": "${KSP_PUBLIC_SOLANA_DEVNET_WS_URL:-wss://api.devnet.solana.com}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grpc_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "orbitflare_solana_devnet_yellowstone",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "orbitflare",
|
||||||
|
"cluster": "devnet",
|
||||||
|
"protocol": "solana_yellowstone",
|
||||||
|
"url": "http://devnet.rpc.orbitflare.com:10000",
|
||||||
|
"metadata": [],
|
||||||
|
"secret_metadata": [
|
||||||
|
{
|
||||||
|
"key": "x-token",
|
||||||
|
"value": "${KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"profile_id": "mainnet_public",
|
"profile_id": "mainnet_public",
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
@@ -105,6 +177,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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
||||||
<!-- version: 8 -->
|
<!-- 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,16 +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 exige actuellement `kind = "solana_standard"`. 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.
|
||||||
|
|
||||||
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.
|
Un endpoint gRPC V3 sépare explicitement :
|
||||||
|
|
||||||
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.
|
```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_*
|
||||||
|
```
|
||||||
|
|
||||||
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.
|
`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`.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// file: crates/ksp-config-lib/src/transport.rs
|
// file: crates/ksp-config-lib/src/transport.rs
|
||||||
// version: 3
|
// 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,
|
||||||
@@ -443,6 +832,7 @@ fn map_ws_protocol_kind(
|
|||||||
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsProtocolKind> {
|
) -> ksp_core_lib::Result<ksp_onchain_transport_lib::WsProtocolKind> {
|
||||||
return match value {
|
return match value {
|
||||||
"solana_standard" => std::result::Result::Ok(ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard),
|
"solana_standard" => std::result::Result::Ok(ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard),
|
||||||
|
"helius_laserstream" => std::result::Result::Ok(ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream),
|
||||||
_ => std::result::Result::Err(
|
_ => std::result::Result::Err(
|
||||||
effective_error(profile, "effective WebSocket protocol kind is unsupported")
|
effective_error(profile, "effective WebSocket protocol kind is unsupported")
|
||||||
.with_context("endpoint_name", endpoint_name)
|
.with_context("endpoint_name", endpoint_name)
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -71,6 +71,55 @@
|
|||||||
"notification_queue_capacity": 48,
|
"notification_queue_capacity": 48,
|
||||||
"max_pending_requests": 24
|
"max_pending_requests": 24
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fixture_helius_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "helius",
|
||||||
|
"cluster": "mainnet-beta",
|
||||||
|
"kind": "helius_laserstream",
|
||||||
|
"url": "wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-fixture-helius-key}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"profile_id": "helius_devnet",
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"name": "fixture_devnet_http",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "devnet",
|
||||||
|
"url": "https://api.devnet.solana.com",
|
||||||
|
"connect_timeout_ms": 750,
|
||||||
|
"request_timeout_ms": 2500,
|
||||||
|
"max_idle_connections_per_host": 3,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "default",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"priority": 7,
|
||||||
|
"limits": {
|
||||||
|
"requests_per_second": 9,
|
||||||
|
"burst_capacity": 12,
|
||||||
|
"max_concurrent_requests": 4,
|
||||||
|
"pause_after_rate_limit_ms": 650
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ws_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "fixture_helius_devnet_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "helius",
|
||||||
|
"cluster": "devnet",
|
||||||
|
"kind": "helius_laserstream",
|
||||||
|
"url": "wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-fixture-helius-key}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
242
crates/ksp-config-lib/unit_tests/fixtures_v3/std.transport.json
Normal file
242
crates/ksp-config-lib/unit_tests/fixtures_v3/std.transport.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/unit_tests/transport.rs
|
// file: crates/ksp-config-lib/unit_tests/transport.rs
|
||||||
// version: 4
|
// version: 12
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fixture_transport_profile_maps_complete_runtime_contract() {
|
fn fixture_transport_profile_maps_complete_runtime_contract() {
|
||||||
@@ -41,10 +41,11 @@ 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 {
|
||||||
assert_eq!(ws.endpoints().len(), 1);
|
assert_eq!(ws.endpoints().len(), 2);
|
||||||
let endpoint = &ws.endpoints()[0];
|
let endpoint = &ws.endpoints()[0];
|
||||||
assert_eq!(endpoint.name(), "fixture_private_ws");
|
assert_eq!(endpoint.name(), "fixture_private_ws");
|
||||||
assert_eq!(endpoint.provider().as_str(), "fixture-provider");
|
assert_eq!(endpoint.provider().as_str(), "fixture-provider");
|
||||||
@@ -64,6 +65,13 @@ fn fixture_transport_profile_maps_complete_runtime_contract() {
|
|||||||
assert_eq!(endpoint.session().max_message_size_bytes(), 33_554_432);
|
assert_eq!(endpoint.session().max_message_size_bytes(), 33_554_432);
|
||||||
assert_eq!(endpoint.session().max_frame_size_bytes(), 8_388_608);
|
assert_eq!(endpoint.session().max_frame_size_bytes(), 8_388_608);
|
||||||
assert_eq!(endpoint.session().max_write_buffer_size_bytes(), 524_288);
|
assert_eq!(endpoint.session().max_write_buffer_size_bytes(), 524_288);
|
||||||
|
let helius = &ws.endpoints()[1];
|
||||||
|
assert_eq!(helius.name(), "fixture_helius_ws");
|
||||||
|
assert_eq!(helius.provider().as_str(), "helius");
|
||||||
|
assert_eq!(helius.cluster().as_str(), "mainnet-beta");
|
||||||
|
assert_eq!(helius.protocol(), ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream);
|
||||||
|
assert_eq!(helius.url().as_str(), "wss://mainnet.helius-rpc.com/?api-key=fixture-helius-key");
|
||||||
|
let _connect_future = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::connect(helius.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,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);
|
||||||
@@ -114,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");
|
||||||
}
|
}
|
||||||
@@ -123,7 +134,193 @@ 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_orbitflare_devnet_profile_maps_provider_neutral_yellowstone_grpc_with_license_x_token() {
|
||||||
|
let engine = committed_engine();
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let canary = "ORBITFLARE-DEVNET-LICENSE-X-TOKEN-CANARY";
|
||||||
|
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||||
|
process.insert("KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN".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("orbitflare_devnet"), &environment);
|
||||||
|
assert!(resolved.is_ok(), "committed OrbitFlare Devnet Transport profile should map from its License Key x-token: {resolved:?}");
|
||||||
|
if let std::result::Result::Ok(resolved) = resolved {
|
||||||
|
assert_eq!(resolved.profile_id(), "orbitflare_devnet");
|
||||||
|
assert_eq!(resolved.selection_source(), crate::ConfigProfileSelectionSource::Explicit);
|
||||||
|
assert_eq!(resolved.settings().endpoints()[0].cluster().as_str(), "devnet");
|
||||||
|
assert_eq!(resolved.settings().endpoints()[0].url().as_str(), "https://api.devnet.solana.com");
|
||||||
|
let ws = resolved.ws_settings();
|
||||||
|
assert!(ws.is_some(), "OrbitFlare Devnet profile should retain the standard Solana Devnet WebSocket companion endpoint");
|
||||||
|
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].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
|
||||||
|
}
|
||||||
|
let grpc = resolved.grpc_settings();
|
||||||
|
assert!(grpc.is_some(), "OrbitFlare Devnet profile should expose one provider-neutral Yellowstone gRPC endpoint");
|
||||||
|
if let std::option::Option::Some(grpc) = grpc {
|
||||||
|
assert_eq!(grpc.endpoints().len(), 1);
|
||||||
|
let endpoint = &grpc.endpoints()[0];
|
||||||
|
assert_eq!(endpoint.name(), "orbitflare_solana_devnet_yellowstone");
|
||||||
|
assert_eq!(endpoint.provider().as_str(), "orbitflare");
|
||||||
|
assert_eq!(endpoint.cluster().as_str(), "devnet");
|
||||||
|
assert_eq!(endpoint.url().as_str(), "http://devnet.rpc.orbitflare.com:10000");
|
||||||
|
assert!(!endpoint.url().uses_tls(), "OrbitFlare shared Devnet URL must remain explicitly HTTP/2 plaintext");
|
||||||
|
assert_eq!(endpoint.metadata().len(), 1);
|
||||||
|
assert_eq!(endpoint.metadata()[0].key(), "x-token");
|
||||||
|
assert!(endpoint.metadata()[0].is_secret());
|
||||||
|
assert!(grpc.validate().is_ok(), "Config-produced OrbitFlare Yellowstone gRPC settings should satisfy Transport validation");
|
||||||
|
let debug = format!("{grpc:?}");
|
||||||
|
assert!(!debug.contains("orbitflare.com"), "Transport settings Debug must not expose OrbitFlare gRPC endpoint URLs");
|
||||||
|
assert!(!debug.contains(canary), "OrbitFlare License Key x-token must stay redacted from Transport settings Debug");
|
||||||
|
assert!(!debug.contains("X-ORBIT-KEY"), "OrbitFlare Customer API credentials must never enter Yellowstone Transport settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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,
|
||||||
@@ -131,11 +328,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);
|
||||||
@@ -163,6 +360,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));
|
||||||
@@ -259,6 +457,78 @@ fn secret_websocket_url_is_runtime_available_but_safe_projection_is_redacted() {
|
|||||||
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
|
assert!(debug.contains(crate::REDACTED_CONFIG_VALUE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helius_laserstream_mainnet_and_devnet_api_key_map_to_protocol_and_safe_redaction() {
|
||||||
|
let engine = fixture_engine();
|
||||||
|
let engine = match engine {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let canary = "helius-api-key-canary";
|
||||||
|
let mut process = std::collections::BTreeMap::<String, String>::new();
|
||||||
|
process.insert("KSP_SECRET_HELIUS_API_KEY".to_owned(), canary.to_owned());
|
||||||
|
let environment = crate::ConfigEnvironment::from_maps(process, std::collections::BTreeMap::new());
|
||||||
|
let mainnet = engine.load_resolved_transport_config(std::option::Option::None, &environment);
|
||||||
|
assert!(mainnet.is_ok(), "Helius mainnet WebSocket endpoint should map without exposing its API key: {mainnet:?}");
|
||||||
|
let mainnet = match mainnet {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let mainnet_ws = mainnet.ws_settings();
|
||||||
|
assert!(mainnet_ws.is_some(), "V2 fixture should expose mainnet WebSocket settings");
|
||||||
|
if let std::option::Option::Some(ws) = mainnet_ws {
|
||||||
|
assert_eq!(ws.endpoints().len(), 2);
|
||||||
|
let endpoint = &ws.endpoints()[1];
|
||||||
|
assert_eq!(endpoint.provider().as_str(), "helius");
|
||||||
|
assert_eq!(endpoint.cluster().as_str(), "mainnet-beta");
|
||||||
|
assert_eq!(endpoint.protocol(), ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream);
|
||||||
|
assert_eq!(endpoint.url().as_str(), "wss://mainnet.helius-rpc.com/?api-key=helius-api-key-canary");
|
||||||
|
}
|
||||||
|
let mainnet_safe_url = mainnet.effective().safe_value().pointer("/ws_endpoints/1/url").and_then(serde_json::Value::as_str);
|
||||||
|
assert_eq!(mainnet_safe_url, std::option::Option::Some("wss://mainnet.helius-rpc.com/?api-key=********"));
|
||||||
|
let mainnet_provenance = mainnet.effective().provenance_at("/ws_endpoints/1/url");
|
||||||
|
assert!(mainnet_provenance.is_some(), "Helius mainnet endpoint URL should retain secret environment provenance");
|
||||||
|
if let std::option::Option::Some(provenance) = mainnet_provenance {
|
||||||
|
assert_eq!(provenance.len(), 2);
|
||||||
|
assert_eq!(provenance[0], crate::ConfigValueProvenance::DocumentLiteral);
|
||||||
|
assert_eq!(provenance[1].environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Process));
|
||||||
|
assert_eq!(provenance[1].variable_name(), std::option::Option::Some("KSP_SECRET_HELIUS_API_KEY"));
|
||||||
|
}
|
||||||
|
let devnet = engine.load_resolved_transport_config(std::option::Option::Some("helius_devnet"), &environment);
|
||||||
|
assert!(devnet.is_ok(), "Helius devnet WebSocket endpoint should map without exposing its API key: {devnet:?}");
|
||||||
|
let devnet = match devnet {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => return,
|
||||||
|
};
|
||||||
|
let devnet_ws = devnet.ws_settings();
|
||||||
|
assert!(devnet_ws.is_some(), "V2 fixture should expose devnet WebSocket settings");
|
||||||
|
if let std::option::Option::Some(ws) = devnet_ws {
|
||||||
|
assert_eq!(ws.endpoints().len(), 1);
|
||||||
|
let endpoint = &ws.endpoints()[0];
|
||||||
|
assert_eq!(endpoint.provider().as_str(), "helius");
|
||||||
|
assert_eq!(endpoint.cluster().as_str(), "devnet");
|
||||||
|
assert_eq!(endpoint.protocol(), ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream);
|
||||||
|
assert_eq!(endpoint.url().as_str(), "wss://devnet.helius-rpc.com/?api-key=helius-api-key-canary");
|
||||||
|
let _connect_future = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::connect(endpoint.clone());
|
||||||
|
}
|
||||||
|
let devnet_safe_url = devnet.effective().safe_value().pointer("/ws_endpoints/0/url").and_then(serde_json::Value::as_str);
|
||||||
|
assert_eq!(devnet_safe_url, std::option::Option::Some("wss://devnet.helius-rpc.com/?api-key=********"));
|
||||||
|
let devnet_provenance = devnet.effective().provenance_at("/ws_endpoints/0/url");
|
||||||
|
assert!(devnet_provenance.is_some(), "Helius devnet endpoint URL should retain secret environment provenance");
|
||||||
|
if let std::option::Option::Some(provenance) = devnet_provenance {
|
||||||
|
assert_eq!(provenance.len(), 2);
|
||||||
|
assert_eq!(provenance[0], crate::ConfigValueProvenance::DocumentLiteral);
|
||||||
|
assert_eq!(provenance[1].environment_source(), std::option::Option::Some(crate::ConfigEnvironmentSource::Process));
|
||||||
|
assert_eq!(provenance[1].variable_name(), std::option::Option::Some("KSP_SECRET_HELIUS_API_KEY"));
|
||||||
|
}
|
||||||
|
let mainnet_debug = format!("{mainnet:?}");
|
||||||
|
let devnet_debug = format!("{devnet:?}");
|
||||||
|
assert!(!mainnet_debug.contains(canary));
|
||||||
|
assert!(!devnet_debug.contains(canary));
|
||||||
|
assert!(mainnet_debug.contains(crate::REDACTED_CONFIG_VALUE));
|
||||||
|
assert!(devnet_debug.contains(crate::REDACTED_CONFIG_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transport_secret_url_provenance_uses_process_and_process_beats_dotenv() {
|
fn transport_secret_url_provenance_uses_process_and_process_beats_dotenv() {
|
||||||
let engine = fixture_engine();
|
let engine = fixture_engine();
|
||||||
@@ -342,6 +612,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"));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-core-lib/tests/workspace_dependencies.rs
|
// file: crates/ksp-core-lib/tests/workspace_dependencies.rs
|
||||||
// version: 4
|
// 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 = [\"net\", \"rt\"] }"));
|
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> {
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-onchain-transport-lib/Cargo.toml
|
# file: crates/ksp-onchain-transport-lib/Cargo.toml
|
||||||
# version: 5
|
# 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 = ["net", "rt"] }
|
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
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
||||||
<!-- version: 19 -->
|
<!-- version: 24 -->
|
||||||
|
|
||||||
# `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 lorsqu’une 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 lorsqu’une 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 :
|
||||||
@@ -199,6 +202,51 @@ Ces familles restent explicitement **unstable**. Le moteur commun `subscribe_typ
|
|||||||
|
|
||||||
`SolanaVoteNotification` conserve `votePubkey`, `slots`, `hash`, `timestamp` et `signature`. Le timestamp reste optionnel : omission et `null` deviennent `None`, tandis qu'une valeur `i64` est préservée. Transport ne transforme pas ces votes gossip pre-consensus en vérité ledger.
|
`SolanaVoteNotification` conserve `votePubkey`, `slots`, `hash`, `timestamp` et `signature`. Le timestamp reste optionnel : omission et `null` deviennent `None`, tandis qu'une valeur `i64` est préservée. Transport ne transforme pas ces votes gossip pre-consensus en vérité ledger.
|
||||||
|
|
||||||
|
## Helius LaserStream WebSocket
|
||||||
|
|
||||||
|
La façade `HeliusLaserStreamWsSession` utilise le même actor physique `WsSession` mais expose uniquement la surface provider actuellement retenue par l’audit Helius :
|
||||||
|
|
||||||
|
```text
|
||||||
|
standard réutilisé : account / logs / program / root / signature / slot / slotsUpdates
|
||||||
|
extension Helius : transactionSubscribe / transactionUnsubscribe
|
||||||
|
absent Helius : block / vote
|
||||||
|
```
|
||||||
|
|
||||||
|
`slotsUpdates` reste **unstable** et conserve le warning centralisé du moteur standard. `block` et `vote` restent absents de la façade Helius même s’ils existent sur la façade Solana standard. `transactionSubscribe` reste provider-specific et n’est jamais ajouté à `SolanaStandardWsSession`.
|
||||||
|
|
||||||
|
Les endpoints Helius mainnet/devnet utilisent un `api-key` dans l’URL. KSP recommande de les construire via `ksp-config-lib` et `KSP_SECRET_HELIUS_API_KEY`; la valeur réelle atteint Transport mais les projections sûres, `Debug`, snapshots, erreurs et diagnostics n’exposent pas le credential. Transport ne lit jamais l’environnement et ne dépend jamais de Config.
|
||||||
|
|
||||||
|
Pour Helius, l’actor envoie automatiquement un control frame WebSocket `Ping` toutes les 60 secondes sur une session active. Cette policy est provider-owned, non configurable et ne s’applique pas aux sessions `SolanaStandard`. Une perte physique suit le reconnect/resubscribe borné déjà décrit; aucun replay/lossless n’est promis par la couche WebSocket.
|
||||||
|
|
||||||
|
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 n’est 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 l’observabilité ; 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 n’est committé ni placé dans l’URL. Le même token a été validé par l’opérateur sur les deux réseaux, même si Config conserve deux variables distinctes pour laisser cette policy flexible.
|
||||||
|
|
||||||
|
`0.2.10` ajoute le profil `orbitflare_devnet` sur le même moteur provider-neutral. L’endpoint validé est `http://devnet.rpc.orbitflare.com:10000`; la License Key `ORBIT-*` est fournie par Config via `KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN` et devient la metadata secrète `x-token`. Le smoke live dédié a validé `Subscribe` slots à commitment confirmed, un `Slot` non nul et un `SubscribeUpdate::Ping` standard. Cette preuve confirme qu’aucune façade OrbitFlare, aucun heartbeat provider et aucune modification N1/N2 ne sont nécessaires.
|
||||||
|
|
||||||
## 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 :
|
||||||
@@ -235,7 +283,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
|
||||||
@@ -252,13 +300,23 @@ 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 PublicNode : settings programmatiques + x-token secret
|
||||||
|
-> Mainnet + Testnet
|
||||||
|
-> TLS -> Subscribe slots -> Slot non nul -> close borné
|
||||||
|
|
||||||
|
Transport Yellowstone gRPC OrbitFlare : settings programmatiques + License Key x-token
|
||||||
|
-> Devnet
|
||||||
|
-> Subscribe slots -> Slot non nul + server Ping -> 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 cinq tests sont `ignored` par défaut ; le test PublicNode Yellowstone contient deux cas live Mainnet/Testnet et le test OrbitFlare contient un cas Devnet. 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 n’est committé en `0.2.8-pre.010`. Un tel test devrait à la fois obtenir `KSP_SECRET_HELIUS_API_KEY` via Config et exercer Transport ; l’ajouter dans Transport violerait l’ownership environnement/secret, tandis que l’ajouter dans Config étendrait l’exception cross-crates que le projet veut au contraire résorber. La première surface KSP d’inté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.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -270,4 +328,8 @@ Les trois tests sont `ignored` par défaut. Les deux smokes Transport appartienn
|
|||||||
- [`../../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 ;
|
||||||
|
- [`../../docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md`](../../docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md) — composition OrbitFlare Devnet sur le standard Yellowstone ;
|
||||||
|
- [`../../docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md`](../../docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md) — validation finale OrbitFlare `x-token`, `Slot + Ping` et non-régression N1/N2 ;
|
||||||
|
- [`../../config/std.transport.json`](../../config/std.transport.json) — configuration standard Transport V3 HTTP + WebSocket + gRPC, avec lecture backward V1/V2.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
|
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
|
||||||
<!-- version: 19 -->
|
<!-- version: 23 -->
|
||||||
|
|
||||||
# Utilisation de `ksp-onchain-transport-lib`
|
# Utilisation de `ksp-onchain-transport-lib`
|
||||||
|
|
||||||
@@ -183,6 +183,46 @@ let mut blocks = match session
|
|||||||
|
|
||||||
Les trois familles utilisent le même `WsSubscription::unsubscribe().await`; aucun remote subscription ID n'entre dans l'API publique.
|
Les trois familles utilisent le même `WsSubscription::unsubscribe().await`; aucun remote subscription ID n'entre dans l'API publique.
|
||||||
|
|
||||||
|
### Façade Helius LaserStream WebSocket
|
||||||
|
|
||||||
|
Pour un endpoint Config `kind = "helius_laserstream"`, le consumer doit sélectionner l’endpoint WebSocket résolu puis ouvrir la façade Helius, sans reconstruire ni journaliser l’URL contenant l’API key :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let resolved = match engine.load_resolved_transport_config(Some("helius_devnet"), &environment) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let ws_settings = match resolved.ws_settings() {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return Err(ksp_core_lib::Error::new(ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS, "Helius profile requires WebSocket settings")),
|
||||||
|
};
|
||||||
|
let endpoint = match ws_settings
|
||||||
|
.endpoints()
|
||||||
|
.iter()
|
||||||
|
.find(|candidate| candidate.enabled() && candidate.protocol() == ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream)
|
||||||
|
{
|
||||||
|
Some(value) => value.clone(),
|
||||||
|
None => return Err(ksp_core_lib::Error::new(ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS, "Helius WebSocket endpoint is unavailable")),
|
||||||
|
};
|
||||||
|
let session = match ksp_onchain_transport_lib::HeliusLaserStreamWsSession::connect(endpoint).await {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let mut slots = match session.slot_subscribe().await {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let notification = slots.recv().await;
|
||||||
|
let removed = slots.unsubscribe().await;
|
||||||
|
let closed = session.close().await;
|
||||||
|
```
|
||||||
|
|
||||||
|
La façade Helius réutilise `account`, `logs`, `program`, `root`, `signature`, `slot` et `slotsUpdates`. `slotsUpdates` reste unstable. `block` et `vote` ne sont pas exposés. `transaction_subscribe()` prend `HeliusTransactionSubscribeRequest` et retourne `WsSubscription<HeliusTransactionNotification>` ; son unsubscribe reste porté par le handle et produit `transactionUnsubscribe` sans exposer l’ID distant.
|
||||||
|
|
||||||
|
Le heartbeat Helius est automatique : `WsSession` envoie un control frame `Ping` toutes les 60 secondes tant que la session Helius est active. Le consumer ne configure pas un second timer et ne lance pas un task heartbeat parallèle. Cette règle ne vaut pas pour `SolanaStandardWsSession`.
|
||||||
|
|
||||||
|
`KSP_SECRET_HELIUS_API_KEY` appartient à Config. Ne pas lire l’environnement dans Transport, ne pas recopier l’URL résolue dans un log et ne pas ajouter un dev-dependency inverse `Transport -> Config`. LaserStream gRPC reste un backend différent et ne doit pas réutiliser `WsProtocolKind::HeliusLaserStream`.
|
||||||
|
|
||||||
### Reconnect automatique borné
|
### Reconnect automatique borné
|
||||||
|
|
||||||
Les settings de session contrôlent le reconnect physique. Une perte de socket publie `Reconnecting { attempt }`, invalide les remote IDs et incrémente `continuity_gap_count`. Avec la policy par défaut `ActiveSubscriptions`, les handles logiques gardent leur `WsSubscriptionId` et passent temporairement en `Resubscribing`; l'actor recrée leurs subscriptions dans l'ordre local avant de republier `Active`.
|
Les settings de session contrôlent le reconnect physique. Une perte de socket publie `Reconnecting { attempt }`, invalide les remote IDs et incrémente `continuity_gap_count`. Avec la policy par défaut `ActiveSubscriptions`, les handles logiques gardent leur `WsSubscriptionId` et passent temporairement en `Resubscribing`; l'actor recrée leurs subscriptions dans l'ordre local avant de republier `Active`.
|
||||||
@@ -217,7 +257,141 @@ 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;
|
||||||
|
```
|
||||||
|
|
||||||
|
L’URL reste sensible : `Debug`, erreurs KSP et snapshots n’en exposent pas la valeur. Les metadata publiques/secrètes se construisent avec `YellowstoneGrpcMetadataEntry`; Transport ne lit jamais l’environnement. 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 l’accesseur 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 n’autorise pas Transport à introduire une API provider-specific sans divergence réelle.
|
||||||
|
|
||||||
|
### Profil OrbitFlare Devnet
|
||||||
|
|
||||||
|
Le profil committé `orbitflare_devnet` réutilise exactement le même accès Config -> Transport :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let resolved = match engine.load_resolved_transport_config(Some("orbitflare_devnet"), &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 OrbitFlare 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 OrbitFlare profile has no enabled Yellowstone gRPC endpoint",
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(endpoint).await;
|
||||||
|
```
|
||||||
|
|
||||||
|
Config résout `KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN` vers la metadata secrète `x-token`. Sa valeur effective est la License Key `ORBIT-*` du produit Solana ; `X-ORBIT-KEY` et le Bearer du Customer API ne doivent pas être utilisés pour Yellowstone. Transport ne lit jamais cette variable lui-même.
|
||||||
|
|
||||||
|
L’endpoint validé par `0.2.10` est `http://devnet.rpc.orbitflare.com:10000`. Il reste volontairement en `http` : KSP ne remplace pas le transport provider par `https` sans endpoint TLS explicitement fourni.
|
||||||
|
|
||||||
|
### Subscribe bidirectionnel
|
||||||
|
|
||||||
|
Une session standard part d’une 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`.
|
||||||
|
|
||||||
@@ -277,7 +451,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 :
|
||||||
|
|
||||||
@@ -291,7 +465,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 :
|
||||||
|
|
||||||
@@ -306,13 +480,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()`.
|
||||||
|
|
||||||
@@ -320,7 +494,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 :
|
||||||
|
|
||||||
@@ -332,7 +506,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 :
|
||||||
|
|
||||||
@@ -350,6 +524,51 @@ 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 **Transport Yellowstone gRPC OrbitFlare** est lui aussi indépendant de Config. Il lit une seule License Key sur stdin, la classe comme metadata secrète `x-token`, ouvre le standard `Subscribe`, demande `slots` à commitment confirmed, attend un Slot non nul et un `SubscribeUpdate::Ping`, puis ferme la session de manière bornée :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
read -rsp 'OrbitFlare License Key: ' ORBITFLARE_LICENSE_KEY
|
||||||
|
echo
|
||||||
|
printf '%s\n' "$ORBITFLARE_LICENSE_KEY" \
|
||||||
|
| cargo test -p ksp-onchain-transport-lib \
|
||||||
|
--test yellowstone_orbitflare_smoke \
|
||||||
|
-- --ignored --nocapture
|
||||||
|
unset ORBITFLARE_LICENSE_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
Endpoint validé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Devnet http://devnet.rpc.orbitflare.com:10000
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate `0.2.10-pre.003` a passé ce scénario en live avec `Slot + Ping`. Le Ping reçu est le message Yellowstone standard auquel N1 sait déjà répondre sans remplacer la dernière requête complète mémorisée ; aucun heartbeat OrbitFlare supplémentaire n’est donc requis.
|
||||||
|
|
||||||
Le smoke de **composition Config -> Transport** reste également disponible :
|
Le smoke de **composition Config -> Transport** reste également disponible :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -358,7 +577,25 @@ cargo test -p ksp-config-lib --test transport_devnet_smoke -- --ignored --nocapt
|
|||||||
|
|
||||||
Il valide le profil committé `devnet_public` et les quatre canaris foundation. Il reste transitoirement hébergé dans Config : les futurs smokes cross-crates ne doivent pas faire de Config leur destination générale et devront migrer vers une surface d'intégration/orchestration dédiée lorsqu'elle existera.
|
Il valide le profil committé `devnet_public` et les quatre canaris foundation. Il reste transitoirement hébergé dans Config : les futurs smokes cross-crates ne doivent pas faire de Config leur destination générale et devront migrer vers une surface d'intégration/orchestration dédiée lorsqu'elle existera.
|
||||||
|
|
||||||
Les endpoints publics Solana sont rate-limités et non destinés à la production. Un échec réseau externe n'est pas assimilé automatiquement à une régression locale ; les fixtures HTTP et WebSocket locales restent les gates reproductibles.
|
### Smoke Helius live
|
||||||
|
|
||||||
|
Aucun nouveau test Helius live n’est 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 l’une de ces deux crates contournerait donc une frontière déjà documentée.
|
||||||
|
|
||||||
|
Lorsque la surface KSP d’intégration/orchestration dédiée existera, le smoke live minimal recommandé sera :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Config helius_devnet
|
||||||
|
-> endpoint helius_laserstream résolu avec KSP_SECRET_HELIUS_API_KEY
|
||||||
|
-> HeliusLaserStreamWsSession::connect
|
||||||
|
-> slotSubscribe
|
||||||
|
-> une slotNotification sous timeout
|
||||||
|
-> slotUnsubscribe
|
||||||
|
-> close
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce scénario utilise une méthode standard stable sur l’endpoint Helius et teste donc auth + façade provider + actor + unsubscribe sans dépendre d’une entitlement particulière de `transactionSubscribe`. Un smoke `transactionSubscribe` pourra être ajouté séparément comme opt-in provider-specific si l’environnement 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 d’auth, entitlement absente ou incident réseau n’est 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 :
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
191
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
Normal file
191
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
Normal 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;
|
||||||
666
crates/ksp-onchain-transport-lib/src/grpc_settings.rs
Normal file
666
crates/ksp-onchain-transport-lib/src/grpc_settings.rs
Normal 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;
|
||||||
1146
crates/ksp-onchain-transport-lib/src/grpc_stream.rs
Normal file
1146
crates/ksp-onchain-transport-lib/src/grpc_stream.rs
Normal file
File diff suppressed because it is too large
Load Diff
3740
crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs
Normal file
3740
crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs
Normal file
File diff suppressed because it is too large
Load Diff
398
crates/ksp-onchain-transport-lib/src/grpc_unary.rs
Normal file
398
crates/ksp-onchain-transport-lib/src/grpc_unary.rs
Normal 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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
// version: 29
|
// version: 44
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -24,15 +24,41 @@
|
|||||||
//! routing. `0.2.7-pre.007` adds finite reconnect, deterministic resubscribe and continuity-gap tracking. `0.2.7-pre.008` makes per-subscription notification
|
//! routing. `0.2.7-pre.007` adds finite reconnect, deterministic resubscribe and continuity-gap tracking. `0.2.7-pre.008` makes per-subscription notification
|
||||||
//! backpressure terminal and observable, preserves safe terminal error codes, performs best-effort remote cleanup and proves bounded capacity reuse.
|
//! backpressure terminal and observable, preserves safe terminal error codes, performs best-effort remote cleanup and proves bounded capacity reuse.
|
||||||
//! `0.2.7-pre.009` opens the first stable typed WebSocket wrappers for account, program-account and transaction-log subscriptions without exposing a raw
|
//! `0.2.7-pre.009` opens the first stable typed WebSocket wrappers for account, program-account and transaction-log subscriptions without exposing a raw
|
||||||
//! provider-extension subscription API.
|
//! provider-extension subscription API. `0.2.8-pre.002` adds a Helius LaserStream WebSocket protocol discriminator and two typed protocol facades while
|
||||||
|
//! keeping the `WsSession` actor/socket implementation unique and the historical generic constructor standard-only.
|
||||||
|
//! `0.2.8-pre.003` initially exposed the six standard families unambiguously supported by the audited Helius pages; `0.2.8-pre.009` reconciles the current
|
||||||
|
//! Helius documentation and adds the now-documented unstable `slotsUpdatesSubscribe` pair while keeping explicitly unsupported block/vote pairs absent.
|
||||||
|
//! `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
|
||||||
|
//! 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.007–008`.
|
||||||
|
//! `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;
|
||||||
@@ -42,26 +68,27 @@ 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;
|
||||||
|
mod ws_helius_transactions;
|
||||||
mod ws_lifecycle;
|
mod ws_lifecycle;
|
||||||
|
mod ws_protocol_session;
|
||||||
mod ws_session;
|
mod ws_session;
|
||||||
mod ws_settings;
|
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.
|
||||||
@@ -94,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.
|
||||||
@@ -108,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`.
|
||||||
@@ -296,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.
|
||||||
@@ -336,6 +513,22 @@ pub use self::ws_cluster::SolanaSlotUpdate;
|
|||||||
pub use self::ws_cluster::SolanaSlotUpdateStats;
|
pub use self::ws_cluster::SolanaSlotUpdateStats;
|
||||||
/// Typed unstable gossip-vote notification delivered by standard Solana `voteSubscribe`.
|
/// Typed unstable gossip-vote notification delivered by standard Solana `voteSubscribe`.
|
||||||
pub use self::ws_cluster::SolanaVoteNotification;
|
pub use self::ws_cluster::SolanaVoteNotification;
|
||||||
|
/// Full/accounts-mode notification delivered by Helius `transactionSubscribe`.
|
||||||
|
pub use self::ws_helius_transactions::HeliusFullTransactionNotification;
|
||||||
|
/// Helius `tokenAccounts` expansion mode accepted by `transactionSubscribe`.
|
||||||
|
pub use self::ws_helius_transactions::HeliusTokenAccountsFilter;
|
||||||
|
/// Typed Helius `transactionNotification` payload union.
|
||||||
|
pub use self::ws_helius_transactions::HeliusTransactionNotification;
|
||||||
|
/// Signatures-mode notification delivered by Helius `transactionSubscribe`.
|
||||||
|
pub use self::ws_helius_transactions::HeliusTransactionSignatureNotification;
|
||||||
|
/// Transaction encoding accepted by Helius `transactionSubscribe`.
|
||||||
|
pub use self::ws_helius_transactions::HeliusTransactionSubscribeEncoding;
|
||||||
|
/// Helius-specific filter object accepted as the first `transactionSubscribe` parameter.
|
||||||
|
pub use self::ws_helius_transactions::HeliusTransactionSubscribeFilter;
|
||||||
|
/// Optional Helius `transactionSubscribe` result-shaping configuration.
|
||||||
|
pub use self::ws_helius_transactions::HeliusTransactionSubscribeOptions;
|
||||||
|
/// Complete typed request contract for Helius `transactionSubscribe` before actor registration.
|
||||||
|
pub use self::ws_helius_transactions::HeliusTransactionSubscribeRequest;
|
||||||
/// Stable local identity assigned to one physical WebSocket session.
|
/// Stable local identity assigned to one physical WebSocket session.
|
||||||
pub use self::ws_lifecycle::WsSessionId;
|
pub use self::ws_lifecycle::WsSessionId;
|
||||||
/// Safe runtime snapshot for one physical WebSocket session.
|
/// Safe runtime snapshot for one physical WebSocket session.
|
||||||
@@ -344,13 +537,17 @@ pub use self::ws_lifecycle::WsSessionSnapshot;
|
|||||||
pub use self::ws_lifecycle::WsSessionState;
|
pub use self::ws_lifecycle::WsSessionState;
|
||||||
/// Stable local identity assigned to one logical WebSocket subscription.
|
/// Stable local identity assigned to one logical WebSocket subscription.
|
||||||
pub use self::ws_lifecycle::WsSubscriptionId;
|
pub use self::ws_lifecycle::WsSubscriptionId;
|
||||||
/// Standard Solana subscription family represented by one logical WebSocket subscription.
|
/// WebSocket subscription family represented by one logical subscription.
|
||||||
pub use self::ws_lifecycle::WsSubscriptionKind;
|
pub use self::ws_lifecycle::WsSubscriptionKind;
|
||||||
/// Safe lifecycle projection for one logical WebSocket subscription.
|
/// Safe lifecycle projection for one logical WebSocket subscription.
|
||||||
pub use self::ws_lifecycle::WsSubscriptionSnapshot;
|
pub use self::ws_lifecycle::WsSubscriptionSnapshot;
|
||||||
/// Observable lifecycle state of one logical WebSocket subscription.
|
/// Observable lifecycle state of one logical WebSocket subscription.
|
||||||
pub use self::ws_lifecycle::WsSubscriptionState;
|
pub use self::ws_lifecycle::WsSubscriptionState;
|
||||||
/// Shareable handle for one explicitly created physical WebSocket session.
|
/// Typed facade for one Helius LaserStream WebSocket physical session.
|
||||||
|
pub use self::ws_protocol_session::HeliusLaserStreamWsSession;
|
||||||
|
/// Typed facade for one standard Solana WebSocket physical session.
|
||||||
|
pub use self::ws_protocol_session::SolanaStandardWsSession;
|
||||||
|
/// Shareable compatibility handle for one explicitly created standard Solana physical WebSocket session.
|
||||||
pub use self::ws_session::WsSession;
|
pub use self::ws_session::WsSession;
|
||||||
/// Open cluster or network descriptor used by WebSocket endpoint settings.
|
/// Open cluster or network descriptor used by WebSocket endpoint settings.
|
||||||
pub use self::ws_settings::WsClusterName;
|
pub use self::ws_settings::WsClusterName;
|
||||||
@@ -370,7 +567,7 @@ pub use self::ws_settings::WsResubscribePolicy;
|
|||||||
pub use self::ws_settings::WsSessionSettings;
|
pub use self::ws_settings::WsSessionSettings;
|
||||||
/// Complete runtime settings consumed by the KSP WebSocket transport foundation.
|
/// Complete runtime settings consumed by the KSP WebSocket transport foundation.
|
||||||
pub use self::ws_settings::WsTransportSettings;
|
pub use self::ws_settings::WsTransportSettings;
|
||||||
/// Typed handle for one logical Solana WebSocket subscription.
|
/// Typed handle for one logical WebSocket subscription.
|
||||||
pub use self::ws_subscription::WsSubscription;
|
pub use self::ws_subscription::WsSubscription;
|
||||||
/// Typed value carried by a contextual Solana `logsNotification`.
|
/// Typed value carried by a contextual Solana `logsNotification`.
|
||||||
pub use self::ws_transactions::SolanaLogsNotification;
|
pub use self::ws_transactions::SolanaLogsNotification;
|
||||||
@@ -383,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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/ws_accounts.rs
|
// file: crates/ksp-onchain-transport-lib/src/ws_accounts.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
const MAX_PROGRAM_SUBSCRIBE_FILTERS: usize = 4;
|
const MAX_PROGRAM_SUBSCRIBE_FILTERS: usize = 4;
|
||||||
const MAX_PROGRAM_SUBSCRIBE_RAW_MEMCMP_BYTES: usize = 128;
|
const MAX_PROGRAM_SUBSCRIBE_RAW_MEMCMP_BYTES: usize = 128;
|
||||||
@@ -277,6 +277,46 @@ fn validate_program_subscribe_filters(filters: &[crate::SolanaProgramAccountFilt
|
|||||||
return std::result::Result::Ok(());
|
return std::result::Result::Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl crate::SolanaStandardWsSession {
|
||||||
|
/// Subscribes to changes for one Solana account through standard `accountSubscribe`.
|
||||||
|
pub async fn account_subscribe(
|
||||||
|
&self,
|
||||||
|
account: &ksp_core_lib::Pubkey,
|
||||||
|
config: std::option::Option<&crate::SolanaAccountSubscribeConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaRpcResponse<crate::SolanaAccount>>> {
|
||||||
|
return self.physical_session().account_subscribe(account, config).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to account changes owned by one Solana program through standard `programSubscribe`.
|
||||||
|
pub async fn program_subscribe(
|
||||||
|
&self,
|
||||||
|
program_id: &ksp_core_lib::Pubkey,
|
||||||
|
config: std::option::Option<&crate::SolanaProgramSubscribeConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaProgramNotification>> {
|
||||||
|
return self.physical_session().program_subscribe(program_id, config).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::HeliusLaserStreamWsSession {
|
||||||
|
/// Subscribes to account changes through the standard `accountSubscribe` wire supported by Helius LaserStream WebSocket.
|
||||||
|
pub async fn account_subscribe(
|
||||||
|
&self,
|
||||||
|
account: &ksp_core_lib::Pubkey,
|
||||||
|
config: std::option::Option<&crate::SolanaAccountSubscribeConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaRpcResponse<crate::SolanaAccount>>> {
|
||||||
|
return self.physical_session().account_subscribe(account, config).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to program-owned account changes through the standard `programSubscribe` wire supported by Helius LaserStream WebSocket.
|
||||||
|
pub async fn program_subscribe(
|
||||||
|
&self,
|
||||||
|
program_id: &ksp_core_lib::Pubkey,
|
||||||
|
config: std::option::Option<&crate::SolanaProgramSubscribeConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaProgramNotification>> {
|
||||||
|
return self.physical_session().program_subscribe(program_id, config).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "../unit_tests/ws_accounts.rs"]
|
#[path = "../unit_tests/ws_accounts.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/ws_blocks.rs
|
// file: crates/ksp-onchain-transport-lib/src/ws_blocks.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
/// Filter accepted by unstable Solana `blockSubscribe`.
|
/// Filter accepted by unstable Solana `blockSubscribe`.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
@@ -210,6 +210,17 @@ struct WireRpcResponse {
|
|||||||
value: WireBlockNotification,
|
value: WireBlockNotification,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl crate::SolanaStandardWsSession {
|
||||||
|
/// Subscribes to unstable standard Solana block notifications through `blockSubscribe`.
|
||||||
|
pub async fn block_subscribe(
|
||||||
|
&self,
|
||||||
|
filter: &crate::SolanaBlockSubscribeFilter,
|
||||||
|
config: std::option::Option<&crate::SolanaBlockSubscribeConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaRpcResponse<crate::SolanaBlockNotification>>> {
|
||||||
|
return self.physical_session().block_subscribe(filter, config).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "../unit_tests/ws_blocks.rs"]
|
#[path = "../unit_tests/ws_blocks.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/ws_cluster.rs
|
// file: crates/ksp-onchain-transport-lib/src/ws_cluster.rs
|
||||||
// version: 3
|
// version: 5
|
||||||
|
|
||||||
/// Slot relationship reported by the standard Solana `slotNotification` WebSocket method.
|
/// Slot relationship reported by the standard Solana `slotNotification` WebSocket method.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
@@ -401,6 +401,46 @@ fn decode_vote_notification(method: &str, value: serde_json::Value) -> ksp_core_
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl crate::SolanaStandardWsSession {
|
||||||
|
/// Subscribes to standard Solana slot-processing notifications through `slotSubscribe`.
|
||||||
|
pub async fn slot_subscribe(&self) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaSlotNotification>> {
|
||||||
|
return self.physical_session().slot_subscribe().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to standard Solana root-slot notifications through `rootSubscribe`.
|
||||||
|
pub async fn root_subscribe(&self) -> ksp_core_lib::Result<crate::WsSubscription<u64>> {
|
||||||
|
return self.physical_session().root_subscribe().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to unstable standard Solana slot-lifecycle notifications through `slotsUpdatesSubscribe`.
|
||||||
|
pub async fn slots_updates_subscribe(&self) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaSlotUpdate>> {
|
||||||
|
return self.physical_session().slots_updates_subscribe().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to unstable pre-consensus gossip vote notifications through `voteSubscribe`.
|
||||||
|
pub async fn vote_subscribe(&self) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaVoteNotification>> {
|
||||||
|
return self.physical_session().vote_subscribe().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::HeliusLaserStreamWsSession {
|
||||||
|
/// Subscribes to slot-processing notifications through the standard `slotSubscribe` wire supported by Helius LaserStream WebSocket.
|
||||||
|
pub async fn slot_subscribe(&self) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaSlotNotification>> {
|
||||||
|
return self.physical_session().slot_subscribe().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to root-slot notifications through the standard `rootSubscribe` wire supported by Helius LaserStream WebSocket.
|
||||||
|
pub async fn root_subscribe(&self) -> ksp_core_lib::Result<crate::WsSubscription<u64>> {
|
||||||
|
return self.physical_session().root_subscribe().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to unstable slot-lifecycle notifications through the standard `slotsUpdatesSubscribe` wire currently documented by Helius LaserStream
|
||||||
|
/// WebSocket.
|
||||||
|
pub async fn slots_updates_subscribe(&self) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaSlotUpdate>> {
|
||||||
|
return self.physical_session().slots_updates_subscribe().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "../unit_tests/ws_cluster.rs"]
|
#[path = "../unit_tests/ws_cluster.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
608
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
Normal file
608
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
Normal file
@@ -0,0 +1,608 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
|
||||||
|
// version: 5
|
||||||
|
|
||||||
|
const MAX_HELIUS_TRANSACTION_FILTER_ACCOUNTS: usize = 50_000;
|
||||||
|
|
||||||
|
/// Helius `tokenAccounts` expansion mode accepted by `transactionSubscribe`.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub enum HeliusTokenAccountsFilter {
|
||||||
|
/// Disable token-account owner expansion explicitly; equivalent to omitting `tokenAccounts`.
|
||||||
|
None,
|
||||||
|
/// Match transactions where a token balance owned by an included account changes or its token account closes.
|
||||||
|
BalanceChanged,
|
||||||
|
/// Match transactions referencing any token account owned by an included account, even if the balance does not change.
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeliusTokenAccountsFilter {
|
||||||
|
/// Returns the exact Helius WebSocket wire string.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_str(self) -> &'static str {
|
||||||
|
return match self {
|
||||||
|
Self::None => "none",
|
||||||
|
Self::BalanceChanged => "balanceChanged",
|
||||||
|
Self::All => "all",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transaction encoding accepted by Helius `transactionSubscribe`.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub enum HeliusTransactionSubscribeEncoding {
|
||||||
|
/// Base58 encoded transaction bytes.
|
||||||
|
Base58,
|
||||||
|
/// Base64 encoded transaction bytes.
|
||||||
|
Base64,
|
||||||
|
/// Parsed JSON transaction representation.
|
||||||
|
JsonParsed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeliusTransactionSubscribeEncoding {
|
||||||
|
/// Returns the exact Helius WebSocket wire string.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_str(self) -> &'static str {
|
||||||
|
return match self {
|
||||||
|
Self::Base58 => "base58",
|
||||||
|
Self::Base64 => "base64",
|
||||||
|
Self::JsonParsed => "jsonParsed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helius-specific filter object accepted as the first `transactionSubscribe` parameter.
|
||||||
|
///
|
||||||
|
/// Debug output intentionally exposes only filter presence, modes and account counts. Transaction signatures and account values are omitted so routine
|
||||||
|
/// diagnostics cannot accidentally disclose the caller's complete provider filter payload.
|
||||||
|
#[derive(Clone, Default, Eq, PartialEq)]
|
||||||
|
pub struct HeliusTransactionSubscribeFilter {
|
||||||
|
vote: std::option::Option<bool>,
|
||||||
|
failed: std::option::Option<bool>,
|
||||||
|
signature: std::option::Option<std::string::String>,
|
||||||
|
account_include: std::option::Option<std::vec::Vec<ksp_core_lib::Pubkey>>,
|
||||||
|
account_exclude: std::option::Option<std::vec::Vec<ksp_core_lib::Pubkey>>,
|
||||||
|
account_required: std::option::Option<std::vec::Vec<ksp_core_lib::Pubkey>>,
|
||||||
|
token_accounts: std::option::Option<crate::HeliusTokenAccountsFilter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeliusTransactionSubscribeFilter {
|
||||||
|
/// Creates a complete Helius transaction filter while preserving omitted versus explicitly empty account arrays.
|
||||||
|
#[must_use]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn new(
|
||||||
|
vote: std::option::Option<bool>,
|
||||||
|
failed: std::option::Option<bool>,
|
||||||
|
signature: std::option::Option<std::string::String>,
|
||||||
|
account_include: std::option::Option<std::vec::Vec<ksp_core_lib::Pubkey>>,
|
||||||
|
account_exclude: std::option::Option<std::vec::Vec<ksp_core_lib::Pubkey>>,
|
||||||
|
account_required: std::option::Option<std::vec::Vec<ksp_core_lib::Pubkey>>,
|
||||||
|
token_accounts: std::option::Option<crate::HeliusTokenAccountsFilter>,
|
||||||
|
) -> Self {
|
||||||
|
return Self { vote, failed, signature, account_include, account_exclude, account_required, token_accounts };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional vote-transaction filter flag.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn vote(&self) -> std::option::Option<bool> {
|
||||||
|
return self.vote;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional failed-transaction filter flag.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn failed(&self) -> std::option::Option<bool> {
|
||||||
|
return self.failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional exact transaction signature filter.
|
||||||
|
#[must_use]
|
||||||
|
pub fn signature(&self) -> std::option::Option<&str> {
|
||||||
|
return match self.signature.as_ref() {
|
||||||
|
std::option::Option::Some(signature) => std::option::Option::Some(signature.as_str()),
|
||||||
|
std::option::Option::None => std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional OR-style account inclusion list.
|
||||||
|
#[must_use]
|
||||||
|
pub fn account_include(&self) -> std::option::Option<&[ksp_core_lib::Pubkey]> {
|
||||||
|
return match self.account_include.as_ref() {
|
||||||
|
std::option::Option::Some(accounts) => std::option::Option::Some(accounts.as_slice()),
|
||||||
|
std::option::Option::None => std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional account exclusion list.
|
||||||
|
#[must_use]
|
||||||
|
pub fn account_exclude(&self) -> std::option::Option<&[ksp_core_lib::Pubkey]> {
|
||||||
|
return match self.account_exclude.as_ref() {
|
||||||
|
std::option::Option::Some(accounts) => std::option::Option::Some(accounts.as_slice()),
|
||||||
|
std::option::Option::None => std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional AND-style required-account list.
|
||||||
|
#[must_use]
|
||||||
|
pub fn account_required(&self) -> std::option::Option<&[ksp_core_lib::Pubkey]> {
|
||||||
|
return match self.account_required.as_ref() {
|
||||||
|
std::option::Option::Some(accounts) => std::option::Option::Some(accounts.as_slice()),
|
||||||
|
std::option::Option::None => std::option::Option::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional Helius token-account owner-expansion mode.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn token_accounts(&self) -> std::option::Option<crate::HeliusTokenAccountsFilter> {
|
||||||
|
return self.token_accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
let include = validate_account_list("accountInclude", self.account_include.as_deref());
|
||||||
|
if let std::result::Result::Err(error) = include {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let exclude = validate_account_list("accountExclude", self.account_exclude.as_deref());
|
||||||
|
if let std::result::Result::Err(error) = exclude {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let required = validate_account_list("accountRequired", self.account_required.as_deref());
|
||||||
|
if let std::result::Result::Err(error) = required {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_json_value(&self) -> serde_json::Value {
|
||||||
|
let mut object = serde_json::Map::new();
|
||||||
|
if let std::option::Option::Some(vote) = self.vote {
|
||||||
|
object.insert("vote".to_owned(), serde_json::Value::Bool(vote));
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(failed) = self.failed {
|
||||||
|
object.insert("failed".to_owned(), serde_json::Value::Bool(failed));
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(signature) = self.signature.as_ref() {
|
||||||
|
object.insert("signature".to_owned(), serde_json::Value::String(signature.clone()));
|
||||||
|
}
|
||||||
|
insert_account_list(&mut object, "accountInclude", self.account_include.as_deref());
|
||||||
|
insert_account_list(&mut object, "accountExclude", self.account_exclude.as_deref());
|
||||||
|
insert_account_list(&mut object, "accountRequired", self.account_required.as_deref());
|
||||||
|
if let std::option::Option::Some(token_accounts) = self.token_accounts {
|
||||||
|
object.insert("tokenAccounts".to_owned(), serde_json::Value::String(token_accounts.as_str().to_owned()));
|
||||||
|
}
|
||||||
|
return serde_json::Value::Object(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for HeliusTransactionSubscribeFilter {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("HeliusTransactionSubscribeFilter")
|
||||||
|
.field("vote", &self.vote)
|
||||||
|
.field("failed", &self.failed)
|
||||||
|
.field("signature_present", &self.signature.is_some())
|
||||||
|
.field("account_include_count", &self.account_include.as_ref().map(std::vec::Vec::len))
|
||||||
|
.field("account_exclude_count", &self.account_exclude.as_ref().map(std::vec::Vec::len))
|
||||||
|
.field("account_required_count", &self.account_required.as_ref().map(std::vec::Vec::len))
|
||||||
|
.field("token_accounts", &self.token_accounts)
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional Helius `transactionSubscribe` result-shaping configuration.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct HeliusTransactionSubscribeOptions {
|
||||||
|
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||||
|
encoding: std::option::Option<crate::HeliusTransactionSubscribeEncoding>,
|
||||||
|
transaction_details: std::option::Option<crate::SolanaTransactionDetails>,
|
||||||
|
show_rewards: std::option::Option<bool>,
|
||||||
|
max_supported_transaction_version: std::option::Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeliusTransactionSubscribeOptions {
|
||||||
|
/// Creates a complete optional Helius transaction-subscription configuration.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(
|
||||||
|
commitment: std::option::Option<crate::SolanaCommitment>,
|
||||||
|
encoding: std::option::Option<crate::HeliusTransactionSubscribeEncoding>,
|
||||||
|
transaction_details: std::option::Option<crate::SolanaTransactionDetails>,
|
||||||
|
show_rewards: std::option::Option<bool>,
|
||||||
|
max_supported_transaction_version: std::option::Option<u8>,
|
||||||
|
) -> Self {
|
||||||
|
return Self { commitment, encoding, transaction_details, show_rewards, max_supported_transaction_version };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional commitment level.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn commitment(&self) -> std::option::Option<crate::SolanaCommitment> {
|
||||||
|
return self.commitment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional Helius transaction encoding.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn encoding(&self) -> std::option::Option<crate::HeliusTransactionSubscribeEncoding> {
|
||||||
|
return self.encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional transaction detail level.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn transaction_details(&self) -> std::option::Option<crate::SolanaTransactionDetails> {
|
||||||
|
return self.transaction_details;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether rewards were explicitly requested.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn show_rewards(&self) -> std::option::Option<bool> {
|
||||||
|
return self.show_rewards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the highest transaction version the caller declares it can consume.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn max_supported_transaction_version(&self) -> std::option::Option<u8> {
|
||||||
|
return self.max_supported_transaction_version;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
let requires_version =
|
||||||
|
matches!(self.transaction_details, std::option::Option::Some(crate::SolanaTransactionDetails::Full | crate::SolanaTransactionDetails::Accounts));
|
||||||
|
if requires_version && self.max_supported_transaction_version.is_none() {
|
||||||
|
let detail = match self.transaction_details {
|
||||||
|
std::option::Option::Some(detail) => detail.as_str(),
|
||||||
|
std::option::Option::None => "omitted",
|
||||||
|
};
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(
|
||||||
|
crate::ERROR_CODE_INVALID_RPC_PARAMETERS,
|
||||||
|
"Helius transactionSubscribe requires maxSupportedTransactionVersion for full or accounts transaction details",
|
||||||
|
)
|
||||||
|
.with_context("rpc_method", "transactionSubscribe")
|
||||||
|
.with_context("field", "maxSupportedTransactionVersion")
|
||||||
|
.with_context("transaction_details", detail),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_json_value(self) -> serde_json::Value {
|
||||||
|
let mut object = serde_json::Map::new();
|
||||||
|
if let std::option::Option::Some(commitment) = self.commitment {
|
||||||
|
object.insert("commitment".to_owned(), serde_json::Value::String(commitment.as_str().to_owned()));
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(encoding) = self.encoding {
|
||||||
|
object.insert("encoding".to_owned(), serde_json::Value::String(encoding.as_str().to_owned()));
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(transaction_details) = self.transaction_details {
|
||||||
|
object.insert("transactionDetails".to_owned(), serde_json::Value::String(transaction_details.as_str().to_owned()));
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(show_rewards) = self.show_rewards {
|
||||||
|
object.insert("showRewards".to_owned(), serde_json::Value::Bool(show_rewards));
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(version) = self.max_supported_transaction_version {
|
||||||
|
object.insert("maxSupportedTransactionVersion".to_owned(), serde_json::Value::Number(version.into()));
|
||||||
|
}
|
||||||
|
return serde_json::Value::Object(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete typed request contract for Helius `transactionSubscribe`.
|
||||||
|
///
|
||||||
|
/// The request owns the exact provider filter and optional result-shaping object. Validation and serialization occur before actor registration so deterministic
|
||||||
|
/// provider constraints fail without WebSocket I/O.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct HeliusTransactionSubscribeRequest {
|
||||||
|
filter: crate::HeliusTransactionSubscribeFilter,
|
||||||
|
options: std::option::Option<crate::HeliusTransactionSubscribeOptions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeliusTransactionSubscribeRequest {
|
||||||
|
/// Creates one typed Helius transaction-subscription request.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(filter: crate::HeliusTransactionSubscribeFilter, options: std::option::Option<crate::HeliusTransactionSubscribeOptions>) -> Self {
|
||||||
|
return Self { filter, options };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the provider transaction filter.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn filter(&self) -> &crate::HeliusTransactionSubscribeFilter {
|
||||||
|
return &self.filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional provider result-shaping configuration.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn options(&self) -> std::option::Option<&crate::HeliusTransactionSubscribeOptions> {
|
||||||
|
return self.options.as_ref();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates deterministic Helius request constraints before any WebSocket I/O.
|
||||||
|
pub fn validate(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
let filter = self.filter.validate();
|
||||||
|
if let std::result::Result::Err(error) = filter {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
if let std::option::Option::Some(options) = self.options {
|
||||||
|
let options = options.validate();
|
||||||
|
if let std::result::Result::Err(error) = options {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for HeliusTransactionSubscribeRequest {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter.debug_struct("HeliusTransactionSubscribeRequest").field("filter", &self.filter).field("options", &self.options).finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn helius_transaction_subscribe_params(request: &crate::HeliusTransactionSubscribeRequest) -> ksp_core_lib::Result<std::vec::Vec<serde_json::Value>> {
|
||||||
|
let validation = request.validate();
|
||||||
|
if let std::result::Result::Err(error) = validation {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let mut params = std::vec![request.filter.to_json_value()];
|
||||||
|
if let std::option::Option::Some(options) = request.options {
|
||||||
|
params.push(options.to_json_value());
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full/accounts-mode notification delivered by Helius `transactionSubscribe`.
|
||||||
|
///
|
||||||
|
/// The nested transaction payload is deliberately retained as JSON because its exact Solana wire representation depends on the requested encoding and detail
|
||||||
|
/// mode. KSP types the stable provider envelope while preserving the full nested payload without Program-specific decoding.
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct HeliusFullTransactionNotification {
|
||||||
|
transaction: serde_json::Value,
|
||||||
|
signature: std::string::String,
|
||||||
|
slot: u64,
|
||||||
|
transaction_index: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeliusFullTransactionNotification {
|
||||||
|
/// Returns the provider transaction/status payload without interpreting Program-specific contents.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn transaction(&self) -> &serde_json::Value {
|
||||||
|
return &self.transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the base58 transaction signature reported by Helius.
|
||||||
|
#[must_use]
|
||||||
|
pub fn signature(&self) -> &str {
|
||||||
|
return self.signature.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the slot in which the transaction was processed.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn slot(&self) -> u64 {
|
||||||
|
return self.slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the zero-based transaction position within the block.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn transaction_index(&self) -> u64 {
|
||||||
|
return self.transaction_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for HeliusFullTransactionNotification {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("HeliusFullTransactionNotification")
|
||||||
|
.field("transaction", &"<omitted>")
|
||||||
|
.field("signature", &"<omitted>")
|
||||||
|
.field("slot", &self.slot)
|
||||||
|
.field("transaction_index", &self.transaction_index)
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signatures-mode notification delivered by Helius `transactionSubscribe`.
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct HeliusTransactionSignatureNotification {
|
||||||
|
signature: std::string::String,
|
||||||
|
slot: u64,
|
||||||
|
transaction_index: u64,
|
||||||
|
err: crate::SolanaWireField<serde_json::Value>,
|
||||||
|
memo: crate::SolanaWireField<std::string::String>,
|
||||||
|
block_time: crate::SolanaWireField<i64>,
|
||||||
|
confirmation_status: crate::SolanaWireField<std::string::String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeliusTransactionSignatureNotification {
|
||||||
|
/// Returns the base58 transaction signature reported by Helius.
|
||||||
|
#[must_use]
|
||||||
|
pub fn signature(&self) -> &str {
|
||||||
|
return self.signature.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the slot in which the transaction was processed.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn slot(&self) -> u64 {
|
||||||
|
return self.slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the zero-based transaction position within the block.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn transaction_index(&self) -> u64 {
|
||||||
|
return self.transaction_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional transaction error while preserving omitted/null/value wire states.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn err(&self) -> &crate::SolanaWireField<serde_json::Value> {
|
||||||
|
return &self.err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional memo while preserving omitted/null/value wire states.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn memo(&self) -> &crate::SolanaWireField<std::string::String> {
|
||||||
|
return &self.memo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional block time while preserving omitted/null/value wire states.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn block_time(&self) -> &crate::SolanaWireField<i64> {
|
||||||
|
return &self.block_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optional confirmation-status label while preserving omitted/null/value wire states.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn confirmation_status(&self) -> &crate::SolanaWireField<std::string::String> {
|
||||||
|
return &self.confirmation_status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for HeliusTransactionSignatureNotification {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("HeliusTransactionSignatureNotification")
|
||||||
|
.field("signature", &"<omitted>")
|
||||||
|
.field("slot", &self.slot)
|
||||||
|
.field("transaction_index", &self.transaction_index)
|
||||||
|
.field("err", &wire_field_debug_state(&self.err))
|
||||||
|
.field("memo", &wire_field_debug_state(&self.memo))
|
||||||
|
.field("block_time", &wire_field_debug_state(&self.block_time))
|
||||||
|
.field("confirmation_status", &wire_field_debug_state(&self.confirmation_status))
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wire_field_debug_state<T>(field: &crate::SolanaWireField<T>) -> &'static str {
|
||||||
|
if field.is_omitted() {
|
||||||
|
return "omitted";
|
||||||
|
}
|
||||||
|
if field.is_null() {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
return "value";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed Helius `transactionNotification` payload union.
|
||||||
|
///
|
||||||
|
/// `Full` also covers the provider `accounts` detail mode because both contain the nested `transaction` member. `Signature` covers the lightweight
|
||||||
|
/// signatures mode. `Unknown` preserves `none` mode and forward-compatible provider shapes instead of failing the logical subscription.
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum HeliusTransactionNotification {
|
||||||
|
/// Full/accounts notification carrying the nested transaction payload.
|
||||||
|
Full(crate::HeliusFullTransactionNotification),
|
||||||
|
/// Lightweight signatures notification.
|
||||||
|
Signature(crate::HeliusTransactionSignatureNotification),
|
||||||
|
/// Provider shape not currently typed by KSP, preserved losslessly.
|
||||||
|
Unknown(serde_json::Value),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for HeliusTransactionNotification {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return match self {
|
||||||
|
Self::Full(notification) => formatter.debug_tuple("Full").field(notification).finish(),
|
||||||
|
Self::Signature(notification) => formatter.debug_tuple("Signature").field(notification).finish(),
|
||||||
|
Self::Unknown(_) => formatter.debug_tuple("Unknown").field(&"<omitted>").finish(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::HeliusLaserStreamWsSession {
|
||||||
|
/// Opens one Helius `transactionSubscribe` logical subscription through the shared physical actor.
|
||||||
|
///
|
||||||
|
/// The returned handle keeps a stable local identity across physical reconnects. Helius remote subscription IDs stay actor-private and are remapped after
|
||||||
|
/// resubscribe. Calling [`crate::WsSubscription::unsubscribe`] removes the remote mapping before sending `transactionUnsubscribe`, so provider messages
|
||||||
|
/// already in flight after cancellation are ignored without reactivating the logical subscription.
|
||||||
|
pub async fn transaction_subscribe(
|
||||||
|
&self,
|
||||||
|
request: &crate::HeliusTransactionSubscribeRequest,
|
||||||
|
) -> ksp_core_lib::Result<crate::WsSubscription<crate::HeliusTransactionNotification>> {
|
||||||
|
let params = helius_transaction_subscribe_params(request);
|
||||||
|
let params = match params {
|
||||||
|
std::result::Result::Ok(params) => params,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return self
|
||||||
|
.physical_session()
|
||||||
|
.subscribe_typed(crate::WsSubscriptionKind::HeliusTransaction, params, |value| return decode_helius_transaction_notification(value))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct WireHeliusFullTransactionNotification {
|
||||||
|
transaction: serde_json::Value,
|
||||||
|
signature: std::string::String,
|
||||||
|
slot: u64,
|
||||||
|
transaction_index: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct WireHeliusTransactionSignatureNotification {
|
||||||
|
signature: std::string::String,
|
||||||
|
slot: u64,
|
||||||
|
transaction_index: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
err: crate::SolanaWireField<serde_json::Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
memo: crate::SolanaWireField<std::string::String>,
|
||||||
|
#[serde(default)]
|
||||||
|
block_time: crate::SolanaWireField<i64>,
|
||||||
|
#[serde(default)]
|
||||||
|
confirmation_status: crate::SolanaWireField<std::string::String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_helius_transaction_notification(value: serde_json::Value) -> ksp_core_lib::Result<crate::HeliusTransactionNotification> {
|
||||||
|
if value.get("transaction").is_some() {
|
||||||
|
let decoded = crate::decode_wire_json::<WireHeliusFullTransactionNotification>("transactionNotification", value.clone());
|
||||||
|
if let std::result::Result::Ok(decoded) = decoded {
|
||||||
|
return std::result::Result::Ok(crate::HeliusTransactionNotification::Full(crate::HeliusFullTransactionNotification {
|
||||||
|
transaction: decoded.transaction,
|
||||||
|
signature: decoded.signature,
|
||||||
|
slot: decoded.slot,
|
||||||
|
transaction_index: decoded.transaction_index,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if value.get("signature").is_some() && value.get("slot").is_some() && value.get("transactionIndex").is_some() {
|
||||||
|
let decoded = crate::decode_wire_json::<WireHeliusTransactionSignatureNotification>("transactionNotification", value.clone());
|
||||||
|
if let std::result::Result::Ok(decoded) = decoded {
|
||||||
|
return std::result::Result::Ok(crate::HeliusTransactionNotification::Signature(crate::HeliusTransactionSignatureNotification {
|
||||||
|
signature: decoded.signature,
|
||||||
|
slot: decoded.slot,
|
||||||
|
transaction_index: decoded.transaction_index,
|
||||||
|
err: decoded.err,
|
||||||
|
memo: decoded.memo,
|
||||||
|
block_time: decoded.block_time,
|
||||||
|
confirmation_status: decoded.confirmation_status,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(crate::HeliusTransactionNotification::Unknown(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_account_list(field: &'static str, accounts: std::option::Option<&[ksp_core_lib::Pubkey]>) -> ksp_core_lib::Result<()> {
|
||||||
|
if let std::option::Option::Some(accounts) = accounts
|
||||||
|
&& accounts.len() > MAX_HELIUS_TRANSACTION_FILTER_ACCOUNTS
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RPC_PARAMETERS, "Helius transactionSubscribe account filter exceeds the provider limit")
|
||||||
|
.with_context("rpc_method", "transactionSubscribe")
|
||||||
|
.with_context("field", field)
|
||||||
|
.with_context("actual_count", accounts.len().to_string())
|
||||||
|
.with_context("max_count", MAX_HELIUS_TRANSACTION_FILTER_ACCOUNTS.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_account_list(
|
||||||
|
object: &mut serde_json::Map<std::string::String, serde_json::Value>,
|
||||||
|
field: &'static str,
|
||||||
|
accounts: std::option::Option<&[ksp_core_lib::Pubkey]>,
|
||||||
|
) {
|
||||||
|
if let std::option::Option::Some(accounts) = accounts {
|
||||||
|
let values = accounts.iter().map(|account| return serde_json::Value::String(account.to_string())).collect::<std::vec::Vec<_>>();
|
||||||
|
object.insert(field.to_owned(), serde_json::Value::Array(values));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/ws_helius_transactions.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/ws_lifecycle.rs
|
// file: crates/ksp-onchain-transport-lib/src/ws_lifecycle.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
/// Stable local identity assigned to one physical WebSocket session.
|
/// Stable local identity assigned to one physical WebSocket session.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
@@ -76,7 +76,7 @@ pub enum WsSubscriptionState {
|
|||||||
Failed,
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Standard Solana subscription family represented by one logical WebSocket subscription.
|
/// WebSocket subscription family represented by one logical subscription.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum WsSubscriptionKind {
|
pub enum WsSubscriptionKind {
|
||||||
@@ -98,10 +98,12 @@ pub enum WsSubscriptionKind {
|
|||||||
SlotsUpdates,
|
SlotsUpdates,
|
||||||
/// `voteSubscribe` family.
|
/// `voteSubscribe` family.
|
||||||
Vote,
|
Vote,
|
||||||
|
/// Helius LaserStream WebSocket `transactionSubscribe` extension family.
|
||||||
|
HeliusTransaction,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WsSubscriptionKind {
|
impl WsSubscriptionKind {
|
||||||
/// Returns the stable KSP descriptor for this standard subscription family.
|
/// Returns the stable KSP descriptor for this WebSocket subscription family.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn as_str(self) -> &'static str {
|
pub const fn as_str(self) -> &'static str {
|
||||||
return match self {
|
return match self {
|
||||||
@@ -114,10 +116,11 @@ impl WsSubscriptionKind {
|
|||||||
Self::Slot => "slot",
|
Self::Slot => "slot",
|
||||||
Self::SlotsUpdates => "slots_updates",
|
Self::SlotsUpdates => "slots_updates",
|
||||||
Self::Vote => "vote",
|
Self::Vote => "vote",
|
||||||
|
Self::HeliusTransaction => "helius_transaction",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the exact standard Solana subscribe JSON-RPC method for this family.
|
/// Returns the exact subscribe JSON-RPC method for this family.
|
||||||
pub(crate) const fn subscribe_method(self) -> &'static str {
|
pub(crate) const fn subscribe_method(self) -> &'static str {
|
||||||
return match self {
|
return match self {
|
||||||
Self::Account => "accountSubscribe",
|
Self::Account => "accountSubscribe",
|
||||||
@@ -129,10 +132,11 @@ impl WsSubscriptionKind {
|
|||||||
Self::Slot => "slotSubscribe",
|
Self::Slot => "slotSubscribe",
|
||||||
Self::SlotsUpdates => "slotsUpdatesSubscribe",
|
Self::SlotsUpdates => "slotsUpdatesSubscribe",
|
||||||
Self::Vote => "voteSubscribe",
|
Self::Vote => "voteSubscribe",
|
||||||
|
Self::HeliusTransaction => "transactionSubscribe",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the exact standard Solana unsubscribe JSON-RPC method for this family.
|
/// Returns the exact unsubscribe JSON-RPC method for this family.
|
||||||
pub(crate) const fn unsubscribe_method(self) -> &'static str {
|
pub(crate) const fn unsubscribe_method(self) -> &'static str {
|
||||||
return match self {
|
return match self {
|
||||||
Self::Account => "accountUnsubscribe",
|
Self::Account => "accountUnsubscribe",
|
||||||
@@ -144,10 +148,11 @@ impl WsSubscriptionKind {
|
|||||||
Self::Slot => "slotUnsubscribe",
|
Self::Slot => "slotUnsubscribe",
|
||||||
Self::SlotsUpdates => "slotsUpdatesUnsubscribe",
|
Self::SlotsUpdates => "slotsUpdatesUnsubscribe",
|
||||||
Self::Vote => "voteUnsubscribe",
|
Self::Vote => "voteUnsubscribe",
|
||||||
|
Self::HeliusTransaction => "transactionUnsubscribe",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the exact standard Solana notification method emitted for this family.
|
/// Returns the exact notification method emitted for this family.
|
||||||
pub(crate) const fn notification_method(self) -> &'static str {
|
pub(crate) const fn notification_method(self) -> &'static str {
|
||||||
return match self {
|
return match self {
|
||||||
Self::Account => "accountNotification",
|
Self::Account => "accountNotification",
|
||||||
@@ -159,10 +164,13 @@ impl WsSubscriptionKind {
|
|||||||
Self::Slot => "slotNotification",
|
Self::Slot => "slotNotification",
|
||||||
Self::SlotsUpdates => "slotsUpdatesNotification",
|
Self::SlotsUpdates => "slotsUpdatesNotification",
|
||||||
Self::Vote => "voteNotification",
|
Self::Vote => "voteNotification",
|
||||||
|
Self::HeliusTransaction => "transactionNotification",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether Solana documents this standard subscription family as unstable.
|
/// Returns whether Solana documents this standard subscription family as unstable.
|
||||||
|
///
|
||||||
|
/// Provider extensions are stable here unless explicitly classified otherwise.
|
||||||
pub(crate) const fn is_unstable(self) -> bool {
|
pub(crate) const fn is_unstable(self) -> bool {
|
||||||
return matches!(self, Self::Block | Self::SlotsUpdates | Self::Vote);
|
return matches!(self, Self::Block | Self::SlotsUpdates | Self::Vote);
|
||||||
}
|
}
|
||||||
@@ -211,7 +219,7 @@ impl WsSubscriptionSnapshot {
|
|||||||
return self.id;
|
return self.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the standard subscription family.
|
/// Returns the logical WebSocket subscription family.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn kind(&self) -> crate::WsSubscriptionKind {
|
pub const fn kind(&self) -> crate::WsSubscriptionKind {
|
||||||
return self.kind;
|
return self.kind;
|
||||||
|
|||||||
149
crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
Normal file
149
crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
|
||||||
|
// version: 6
|
||||||
|
|
||||||
|
/// Typed facade for one standard Solana WebSocket physical session.
|
||||||
|
///
|
||||||
|
/// The facade delegates to the same [`crate::WsSession`] actor used by the compatibility API. It owns no socket, registry, reconnect loop or queue of its
|
||||||
|
/// own and therefore does not duplicate the physical WebSocket runtime. Subscription wrappers are implemented beside their wire owners in the
|
||||||
|
/// `ws_accounts`, `ws_blocks`, `ws_cluster` and `ws_transactions` modules.
|
||||||
|
///
|
||||||
|
/// ```compile_fail
|
||||||
|
/// async fn unsupported_helius_transaction(
|
||||||
|
/// session: &ksp_onchain_transport_lib::SolanaStandardWsSession,
|
||||||
|
/// request: &ksp_onchain_transport_lib::HeliusTransactionSubscribeRequest,
|
||||||
|
/// ) {
|
||||||
|
/// let _ = session.transaction_subscribe(request).await;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SolanaStandardWsSession {
|
||||||
|
inner: crate::WsSession,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SolanaStandardWsSession {
|
||||||
|
/// Opens one standard Solana WebSocket session through the shared physical actor.
|
||||||
|
pub async fn connect(endpoint: crate::WsEndpointSettings) -> ksp_core_lib::Result<Self> {
|
||||||
|
let connected = crate::WsSession::connect_for_protocol(endpoint, crate::WsProtocolKind::SolanaStandard).await;
|
||||||
|
return match connected {
|
||||||
|
std::result::Result::Ok(inner) => std::result::Result::Ok(Self { inner }),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the stable local session identity.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn id(&self) -> crate::WsSessionId {
|
||||||
|
return self.inner.id();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the latest safe runtime snapshot published by the shared actor.
|
||||||
|
#[must_use]
|
||||||
|
pub fn snapshot(&self) -> crate::WsSessionSnapshot {
|
||||||
|
return self.inner.snapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the latest observable physical-session state.
|
||||||
|
#[must_use]
|
||||||
|
pub fn state(&self) -> crate::WsSessionState {
|
||||||
|
return self.inner.state();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly closes the shared physical session under its configured close timeout.
|
||||||
|
pub async fn close(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
return self.inner.close().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the crate-private shared physical session used by domain-specific facade wrappers.
|
||||||
|
pub(crate) fn physical_session(&self) -> &crate::WsSession {
|
||||||
|
return &self.inner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for SolanaStandardWsSession {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter.debug_struct("SolanaStandardWsSession").field("id", &self.id()).field("snapshot", &self.snapshot()).finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typed facade for one Helius LaserStream WebSocket physical session.
|
||||||
|
///
|
||||||
|
/// The facade exposes the seven standard Solana subscription families that the current Helius method pages support or document as available plus the
|
||||||
|
/// Helius-specific typed
|
||||||
|
/// `transactionSubscribe` lifecycle. Transaction notifications, reconnect/resubscribe, unsubscribe races and bounded backpressure all delegate to the same
|
||||||
|
/// shared [`crate::WsSession`] actor; the facade owns no second socket, registry or queue. No public inner handle is exposed, so callers cannot bypass the
|
||||||
|
/// provider-specific surface by recovering a generic [`crate::WsSession`].
|
||||||
|
///
|
||||||
|
/// ```compile_fail
|
||||||
|
/// async fn unsupported_block(session: &ksp_onchain_transport_lib::HeliusLaserStreamWsSession) {
|
||||||
|
/// let _ = session.block_subscribe().await;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```compile_fail
|
||||||
|
/// async fn unsupported_vote(session: &ksp_onchain_transport_lib::HeliusLaserStreamWsSession) {
|
||||||
|
/// let _ = session.vote_subscribe().await;
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```compile_fail
|
||||||
|
/// fn no_escape_hatch(session: ksp_onchain_transport_lib::HeliusLaserStreamWsSession) {
|
||||||
|
/// let _ = session.into_inner();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HeliusLaserStreamWsSession {
|
||||||
|
inner: crate::WsSession,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeliusLaserStreamWsSession {
|
||||||
|
/// Opens one Helius LaserStream WebSocket session through the shared physical actor.
|
||||||
|
pub async fn connect(endpoint: crate::WsEndpointSettings) -> ksp_core_lib::Result<Self> {
|
||||||
|
let connected = crate::WsSession::connect_for_protocol(endpoint, crate::WsProtocolKind::HeliusLaserStream).await;
|
||||||
|
return match connected {
|
||||||
|
std::result::Result::Ok(inner) => std::result::Result::Ok(Self { inner }),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the stable local session identity.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn id(&self) -> crate::WsSessionId {
|
||||||
|
return self.inner.id();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the latest safe runtime snapshot published by the shared actor.
|
||||||
|
#[must_use]
|
||||||
|
pub fn snapshot(&self) -> crate::WsSessionSnapshot {
|
||||||
|
return self.inner.snapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the latest observable physical-session state.
|
||||||
|
#[must_use]
|
||||||
|
pub fn state(&self) -> crate::WsSessionState {
|
||||||
|
return self.inner.state();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicitly closes the shared physical session under its configured close timeout.
|
||||||
|
pub async fn close(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
return self.inner.close().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the crate-private shared physical session used by domain-specific facade wrappers.
|
||||||
|
pub(crate) fn physical_session(&self) -> &crate::WsSession {
|
||||||
|
return &self.inner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for HeliusLaserStreamWsSession {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter.debug_struct("HeliusLaserStreamWsSession").field("id", &self.id()).field("snapshot", &self.snapshot()).finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/ws_helius_standard.rs"]
|
||||||
|
mod helius_standard_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/ws_protocol_session.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/ws_session.rs
|
// file: crates/ksp-onchain-transport-lib/src/ws_session.rs
|
||||||
// version: 12
|
// version: 14
|
||||||
|
|
||||||
use futures_util::SinkExt; // rust-rules: trait-import
|
use futures_util::SinkExt; // rust-rules: trait-import
|
||||||
use futures_util::StreamExt; // rust-rules: trait-import
|
use futures_util::StreamExt; // rust-rules: trait-import
|
||||||
|
|
||||||
static NEXT_WS_SESSION_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
static NEXT_WS_SESSION_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||||
|
|
||||||
|
const HELIUS_WS_HEARTBEAT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(60);
|
||||||
|
|
||||||
type WsPhysicalStream = tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
|
type WsPhysicalStream = tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
|
||||||
|
|
||||||
/// Shareable handle for one explicitly created physical WebSocket session.
|
/// Shareable handle for one explicitly created physical WebSocket session.
|
||||||
@@ -24,15 +26,32 @@ pub struct WsSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WsSession {
|
impl WsSession {
|
||||||
/// Opens one physical WebSocket connection for the supplied endpoint settings.
|
/// Opens one physical standard Solana WebSocket connection for the supplied endpoint settings.
|
||||||
///
|
///
|
||||||
/// Calling this function twice with the same endpoint creates two independent physical sessions. The function returns only after the WebSocket
|
/// This historical constructor remains standard-only after provider-specific protocol kinds are added. Calling this function twice with the same endpoint
|
||||||
/// handshake succeeds or the configured command timeout expires.
|
/// creates two independent physical sessions. Provider-specific callers must use their typed protocol facade instead of obtaining a generic handle.
|
||||||
pub async fn connect(endpoint: crate::WsEndpointSettings) -> ksp_core_lib::Result<Self> {
|
pub async fn connect(endpoint: crate::WsEndpointSettings) -> ksp_core_lib::Result<Self> {
|
||||||
|
return Self::connect_for_protocol(endpoint, crate::WsProtocolKind::SolanaStandard).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens one physical WebSocket connection after validating the typed facade protocol.
|
||||||
|
pub(crate) async fn connect_for_protocol(endpoint: crate::WsEndpointSettings, expected_protocol: crate::WsProtocolKind) -> ksp_core_lib::Result<Self> {
|
||||||
let validation = crate::WsTransportSettings::new(std::vec![endpoint.clone()]).validate();
|
let validation = crate::WsTransportSettings::new(std::vec![endpoint.clone()]).validate();
|
||||||
if let std::result::Result::Err(error) = validation {
|
if let std::result::Result::Err(error) = validation {
|
||||||
return std::result::Result::Err(error);
|
return std::result::Result::Err(error);
|
||||||
}
|
}
|
||||||
|
if endpoint.protocol() != expected_protocol {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "WebSocket session constructor does not accept the endpoint protocol")
|
||||||
|
.with_context("field", "ws_endpoints.protocol")
|
||||||
|
.with_context("expected_protocol", expected_protocol.as_str())
|
||||||
|
.with_context("actual_protocol", endpoint.protocol().as_str()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Self::connect_physical(endpoint).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_physical(endpoint: crate::WsEndpointSettings) -> ksp_core_lib::Result<Self> {
|
||||||
let id_result = next_session_id();
|
let id_result = next_session_id();
|
||||||
let id = match id_result {
|
let id = match id_result {
|
||||||
std::result::Result::Ok(id) => id,
|
std::result::Result::Ok(id) => id,
|
||||||
@@ -395,6 +414,8 @@ async fn run_ws_session_actor(
|
|||||||
let mut pending = std::collections::BTreeMap::<u64, PendingWsRequest>::new();
|
let mut pending = std::collections::BTreeMap::<u64, PendingWsRequest>::new();
|
||||||
let mut subscriptions = std::collections::BTreeMap::<u64, crate::WsSubscriptionRuntime>::new();
|
let mut subscriptions = std::collections::BTreeMap::<u64, crate::WsSubscriptionRuntime>::new();
|
||||||
let mut remote_to_local = std::collections::BTreeMap::<u64, crate::WsSubscriptionId>::new();
|
let mut remote_to_local = std::collections::BTreeMap::<u64, crate::WsSubscriptionId>::new();
|
||||||
|
let heartbeat_enabled = helius_heartbeat_enabled(endpoint.protocol());
|
||||||
|
let mut heartbeat_deadline = next_helius_heartbeat_deadline();
|
||||||
loop {
|
loop {
|
||||||
prune_cancelled_pending(id, &mut pending, &mut subscriptions, &mut remote_to_local);
|
prune_cancelled_pending(id, &mut pending, &mut subscriptions, &mut remote_to_local);
|
||||||
let timeout_deadline = next_pending_deadline(&pending);
|
let timeout_deadline = next_pending_deadline(&pending);
|
||||||
@@ -454,6 +475,13 @@ async fn run_ws_session_actor(
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
},
|
},
|
||||||
|
() = tokio::time::sleep_until(heartbeat_deadline), if heartbeat_enabled => {
|
||||||
|
let heartbeat = send_helius_heartbeat(id, &endpoint, &mut websocket, &mut shutdown_rx).await;
|
||||||
|
if matches!(&heartbeat, WsActorIoOutcome::Continue) {
|
||||||
|
heartbeat_deadline = next_helius_heartbeat_deadline();
|
||||||
|
}
|
||||||
|
heartbeat
|
||||||
|
},
|
||||||
() = tokio::time::sleep_until(timeout_deadline) => {
|
() = tokio::time::sleep_until(timeout_deadline) => {
|
||||||
expire_pending_requests(id, &mut pending, &mut subscriptions, &mut remote_to_local);
|
expire_pending_requests(id, &mut pending, &mut subscriptions, &mut remote_to_local);
|
||||||
WsActorIoOutcome::Continue
|
WsActorIoOutcome::Continue
|
||||||
@@ -533,7 +561,12 @@ async fn run_ws_session_actor(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
match recovery {
|
match recovery {
|
||||||
WsReconnectOutcome::Connected { websocket: replacement } => websocket = *replacement,
|
WsReconnectOutcome::Connected { websocket: replacement } => {
|
||||||
|
websocket = *replacement;
|
||||||
|
if heartbeat_enabled {
|
||||||
|
heartbeat_deadline = next_helius_heartbeat_deadline();
|
||||||
|
}
|
||||||
|
},
|
||||||
WsReconnectOutcome::ShutdownRequested { deadline } => {
|
WsReconnectOutcome::ShutdownRequested { deadline } => {
|
||||||
finish_disconnected_shutdown(
|
finish_disconnected_shutdown(
|
||||||
id,
|
id,
|
||||||
@@ -594,7 +627,12 @@ async fn run_ws_session_actor(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
match recovery {
|
match recovery {
|
||||||
WsReconnectOutcome::Connected { websocket: replacement } => websocket = *replacement,
|
WsReconnectOutcome::Connected { websocket: replacement } => {
|
||||||
|
websocket = *replacement;
|
||||||
|
if heartbeat_enabled {
|
||||||
|
heartbeat_deadline = next_helius_heartbeat_deadline();
|
||||||
|
}
|
||||||
|
},
|
||||||
WsReconnectOutcome::ShutdownRequested { deadline } => {
|
WsReconnectOutcome::ShutdownRequested { deadline } => {
|
||||||
finish_disconnected_shutdown(
|
finish_disconnected_shutdown(
|
||||||
id,
|
id,
|
||||||
@@ -641,6 +679,69 @@ async fn run_ws_session_actor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fn helius_heartbeat_enabled(protocol: crate::WsProtocolKind) -> bool {
|
||||||
|
return matches!(protocol, crate::WsProtocolKind::HeliusLaserStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_helius_heartbeat_deadline() -> tokio::time::Instant {
|
||||||
|
return tokio::time::Instant::now() + HELIUS_WS_HEARTBEAT_INTERVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_helius_heartbeat<S>(
|
||||||
|
id: crate::WsSessionId,
|
||||||
|
endpoint: &crate::WsEndpointSettings,
|
||||||
|
websocket: &mut tokio_tungstenite::WebSocketStream<S>,
|
||||||
|
shutdown_rx: &mut tokio::sync::watch::Receiver<std::option::Option<tokio::time::Instant>>,
|
||||||
|
) -> WsActorIoOutcome
|
||||||
|
where
|
||||||
|
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin,
|
||||||
|
{
|
||||||
|
let message = tokio_tungstenite::tungstenite::Message::Ping(std::vec::Vec::new().into());
|
||||||
|
let send_result = tokio::select! {
|
||||||
|
biased;
|
||||||
|
shutdown_changed = shutdown_rx.changed() => {
|
||||||
|
let deadline = resolve_shutdown_deadline(shutdown_rx, shutdown_changed, endpoint.session().close_timeout());
|
||||||
|
return WsActorIoOutcome::ShutdownRequested { deadline };
|
||||||
|
},
|
||||||
|
send_result = websocket.send(message) => send_result,
|
||||||
|
() = tokio::time::sleep(endpoint.session().command_timeout()) => {
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
session_id = id.get(),
|
||||||
|
endpoint_name = endpoint.name(),
|
||||||
|
"Helius WebSocket heartbeat Ping write timed out"
|
||||||
|
);
|
||||||
|
return WsActorIoOutcome::Failed {
|
||||||
|
code: crate::ERROR_CODE_WS_CONNECTION_FAILED,
|
||||||
|
pending_message: "WebSocket connection failed while writing Helius heartbeat Ping",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return match send_result {
|
||||||
|
std::result::Result::Ok(()) => {
|
||||||
|
ksp_logging_lib::trace!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
session_id = id.get(),
|
||||||
|
endpoint_name = endpoint.name(),
|
||||||
|
"sent Helius WebSocket heartbeat Ping control frame"
|
||||||
|
);
|
||||||
|
WsActorIoOutcome::Continue
|
||||||
|
},
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
session_id = id.get(),
|
||||||
|
endpoint_name = endpoint.name(),
|
||||||
|
"Helius WebSocket heartbeat Ping write failed"
|
||||||
|
);
|
||||||
|
WsActorIoOutcome::Failed {
|
||||||
|
code: crate::ERROR_CODE_WS_CONNECTION_FAILED,
|
||||||
|
pending_message: "WebSocket connection failed while writing Helius heartbeat Ping",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
fn websocket_config(endpoint: &crate::WsEndpointSettings) -> tokio_tungstenite::tungstenite::protocol::WebSocketConfig {
|
fn websocket_config(endpoint: &crate::WsEndpointSettings) -> tokio_tungstenite::tungstenite::protocol::WebSocketConfig {
|
||||||
return tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default()
|
return tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default()
|
||||||
.write_buffer_size(0)
|
.write_buffer_size(0)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/ws_settings.rs
|
// file: crates/ksp-onchain-transport-lib/src/ws_settings.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
const DEFAULT_WS_CLOSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
const DEFAULT_WS_CLOSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||||
const DEFAULT_WS_COMMAND_QUEUE_CAPACITY: usize = 128;
|
const DEFAULT_WS_COMMAND_QUEUE_CAPACITY: usize = 128;
|
||||||
@@ -119,13 +119,15 @@ impl WsClusterName {
|
|||||||
|
|
||||||
/// WebSocket protocol family understood by KSP Transport.
|
/// WebSocket protocol family understood by KSP Transport.
|
||||||
///
|
///
|
||||||
/// `0.2.7` exposes only standard Solana WebSocket. The non-exhaustive contract allows later provider-specific families without changing the common endpoint
|
/// The protocol discriminator belongs specifically to the WebSocket runtime. Provider products using another transport, including a future Helius
|
||||||
/// container or injecting provider-only options into [`WsSessionSettings`].
|
/// LaserStream gRPC backend, require a distinct transport-owned descriptor instead of reusing this enum.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum WsProtocolKind {
|
pub enum WsProtocolKind {
|
||||||
/// Standard Solana JSON-RPC WebSocket PubSub.
|
/// Standard Solana JSON-RPC WebSocket PubSub.
|
||||||
SolanaStandard,
|
SolanaStandard,
|
||||||
|
/// Helius LaserStream WebSocket protocol surface.
|
||||||
|
HeliusLaserStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WsProtocolKind {
|
impl WsProtocolKind {
|
||||||
@@ -134,6 +136,7 @@ impl WsProtocolKind {
|
|||||||
pub const fn as_str(self) -> &'static str {
|
pub const fn as_str(self) -> &'static str {
|
||||||
return match self {
|
return match self {
|
||||||
Self::SolanaStandard => "solana_standard",
|
Self::SolanaStandard => "solana_standard",
|
||||||
|
Self::HeliusLaserStream => "helius_laserstream",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/ws_subscription.rs
|
// file: crates/ksp-onchain-transport-lib/src/ws_subscription.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
/// Typed handle for one logical Solana WebSocket subscription.
|
/// Typed handle for one logical WebSocket subscription.
|
||||||
///
|
///
|
||||||
/// The handle owns the bounded typed notification receiver while the physical session actor owns the remote subscription identity and socket. The remote
|
/// The handle owns the bounded typed notification receiver while the physical session actor owns the remote subscription identity and socket. The remote
|
||||||
/// numeric subscription identifier is intentionally never exposed because it is transient and is remapped by the session actor after reconnect.
|
/// numeric subscription identifier is intentionally never exposed because it is transient and is remapped by the session actor after reconnect.
|
||||||
@@ -43,7 +43,7 @@ impl<T> WsSubscription<T> {
|
|||||||
return self.id;
|
return self.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the standard Solana subscription family.
|
/// Returns the logical WebSocket subscription family.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn kind(&self) -> crate::WsSubscriptionKind {
|
pub const fn kind(&self) -> crate::WsSubscriptionKind {
|
||||||
return self.kind;
|
return self.kind;
|
||||||
@@ -71,7 +71,7 @@ impl<T> WsSubscription<T> {
|
|||||||
return self.notification_rx.recv().await;
|
return self.notification_rx.recv().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cancels this logical subscription and sends the matching Solana unsubscribe request when a remote binding still exists.
|
/// Cancels this logical subscription and sends the matching protocol unsubscribe request when a remote binding still exists.
|
||||||
///
|
///
|
||||||
/// The returned boolean preserves the standard Solana unsubscribe result when the current remote binding is reachable. Local cancellation is terminal
|
/// The returned boolean preserves the standard Solana unsubscribe result when the current remote binding is reachable. Local cancellation is terminal
|
||||||
/// for this handle; during reconnect it wins before resubscribe selection, and a late remote acknowledgement is cleaned up best-effort without
|
/// for this handle; during reconnect it wins before resubscribe selection, and a late remote acknowledgement is cleaned up best-effort without
|
||||||
@@ -198,11 +198,11 @@ impl WsSubscriptionRegistration {
|
|||||||
pub(crate) struct WsSubscriptionRuntime {
|
pub(crate) struct WsSubscriptionRuntime {
|
||||||
/// Stable local identity.
|
/// Stable local identity.
|
||||||
pub(crate) id: crate::WsSubscriptionId,
|
pub(crate) id: crate::WsSubscriptionId,
|
||||||
/// Standard Solana subscription family.
|
/// WebSocket subscription family.
|
||||||
pub(crate) kind: crate::WsSubscriptionKind,
|
pub(crate) kind: crate::WsSubscriptionKind,
|
||||||
/// Current logical lifecycle state.
|
/// Current logical lifecycle state.
|
||||||
pub(crate) state: crate::WsSubscriptionState,
|
pub(crate) state: crate::WsSubscriptionState,
|
||||||
/// Original standard subscribe parameters retained internally for deterministic resubscribe.
|
/// Original subscribe parameters retained internally for deterministic resubscribe.
|
||||||
pub(crate) params: std::vec::Vec<serde_json::Value>,
|
pub(crate) params: std::vec::Vec<serde_json::Value>,
|
||||||
/// Current transient remote subscription identity when bound.
|
/// Current transient remote subscription identity when bound.
|
||||||
pub(crate) remote_id: std::option::Option<u64>,
|
pub(crate) remote_id: std::option::Option<u64>,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/ws_transactions.rs
|
// file: crates/ksp-onchain-transport-lib/src/ws_transactions.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
/// Optional configuration accepted by standard Solana `signatureSubscribe`.
|
/// Optional configuration accepted by standard Solana `signatureSubscribe`.
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
@@ -246,6 +246,46 @@ fn decode_logs_notification(method: &str, value: serde_json::Value) -> ksp_core_
|
|||||||
return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, notification));
|
return std::result::Result::Ok(crate::SolanaRpcResponse::new(context, notification));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl crate::SolanaStandardWsSession {
|
||||||
|
/// Subscribes to one Solana transaction signature through standard `signatureSubscribe`.
|
||||||
|
pub async fn signature_subscribe(
|
||||||
|
&self,
|
||||||
|
signature: &str,
|
||||||
|
config: std::option::Option<&crate::SolanaSignatureSubscribeConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaRpcResponse<crate::SolanaSignatureNotification>>> {
|
||||||
|
return self.physical_session().signature_subscribe(signature, config).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to Solana transaction logs through standard `logsSubscribe`.
|
||||||
|
pub async fn logs_subscribe(
|
||||||
|
&self,
|
||||||
|
filter: &crate::SolanaLogsSubscribeFilter,
|
||||||
|
config: std::option::Option<&crate::SolanaCommitmentConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaRpcResponse<crate::SolanaLogsNotification>>> {
|
||||||
|
return self.physical_session().logs_subscribe(filter, config).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::HeliusLaserStreamWsSession {
|
||||||
|
/// Subscribes to one transaction signature through the standard `signatureSubscribe` wire supported by Helius LaserStream WebSocket.
|
||||||
|
pub async fn signature_subscribe(
|
||||||
|
&self,
|
||||||
|
signature: &str,
|
||||||
|
config: std::option::Option<&crate::SolanaSignatureSubscribeConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaRpcResponse<crate::SolanaSignatureNotification>>> {
|
||||||
|
return self.physical_session().signature_subscribe(signature, config).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to transaction logs through the standard `logsSubscribe` wire supported by Helius LaserStream WebSocket.
|
||||||
|
pub async fn logs_subscribe(
|
||||||
|
&self,
|
||||||
|
filter: &crate::SolanaLogsSubscribeFilter,
|
||||||
|
config: std::option::Option<&crate::SolanaCommitmentConfig>,
|
||||||
|
) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaRpcResponse<crate::SolanaLogsNotification>>> {
|
||||||
|
return self.physical_session().logs_subscribe(filter, config).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "../unit_tests/ws_transactions.rs"]
|
#[path = "../unit_tests/ws_transactions.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -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: 33
|
// version: 48
|
||||||
|
|
||||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||||
|
|
||||||
@@ -676,3 +676,339 @@ fn public_v0_2_7_pre_012_complete_standard_websocket_surface_is_available_from_c
|
|||||||
];
|
];
|
||||||
assert_eq!(kinds.len(), 9);
|
assert_eq!(kinds.len(), 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_2_8_pre_002_protocol_facades_are_available_without_replacing_the_standard_session_contract() {
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard.as_str(), "solana_standard");
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream.as_str(), "helius_laserstream");
|
||||||
|
let _historical_connect = ksp_onchain_transport_lib::WsSession::connect;
|
||||||
|
let _standard_connect = ksp_onchain_transport_lib::SolanaStandardWsSession::connect;
|
||||||
|
let _standard_close = ksp_onchain_transport_lib::SolanaStandardWsSession::close;
|
||||||
|
let _standard_snapshot = ksp_onchain_transport_lib::SolanaStandardWsSession::snapshot;
|
||||||
|
let _standard_account = ksp_onchain_transport_lib::SolanaStandardWsSession::account_subscribe;
|
||||||
|
let _standard_block = ksp_onchain_transport_lib::SolanaStandardWsSession::block_subscribe;
|
||||||
|
let _standard_logs = ksp_onchain_transport_lib::SolanaStandardWsSession::logs_subscribe;
|
||||||
|
let _standard_program = ksp_onchain_transport_lib::SolanaStandardWsSession::program_subscribe;
|
||||||
|
let _standard_root = ksp_onchain_transport_lib::SolanaStandardWsSession::root_subscribe;
|
||||||
|
let _standard_signature = ksp_onchain_transport_lib::SolanaStandardWsSession::signature_subscribe;
|
||||||
|
let _standard_slot = ksp_onchain_transport_lib::SolanaStandardWsSession::slot_subscribe;
|
||||||
|
let _standard_slots_updates = ksp_onchain_transport_lib::SolanaStandardWsSession::slots_updates_subscribe;
|
||||||
|
let _standard_vote = ksp_onchain_transport_lib::SolanaStandardWsSession::vote_subscribe;
|
||||||
|
let _helius_connect = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::connect;
|
||||||
|
let _helius_close = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::close;
|
||||||
|
let _helius_snapshot = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_2_8_pre_003_helius_standard_surface_reuses_shared_typed_contracts() {
|
||||||
|
let _account = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::account_subscribe;
|
||||||
|
let _program = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::program_subscribe;
|
||||||
|
let _logs = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::logs_subscribe;
|
||||||
|
let _signature = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::signature_subscribe;
|
||||||
|
let _slot = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::slot_subscribe;
|
||||||
|
let _root = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::root_subscribe;
|
||||||
|
let _shared_account_config = std::any::type_name::<ksp_onchain_transport_lib::SolanaAccountSubscribeConfig>();
|
||||||
|
let _shared_program_config = std::any::type_name::<ksp_onchain_transport_lib::SolanaProgramSubscribeConfig>();
|
||||||
|
let _shared_logs_filter = std::any::type_name::<ksp_onchain_transport_lib::SolanaLogsSubscribeFilter>();
|
||||||
|
let _shared_signature_config = std::any::type_name::<ksp_onchain_transport_lib::SolanaSignatureSubscribeConfig>();
|
||||||
|
let _shared_slot_notification = std::any::type_name::<ksp_onchain_transport_lib::SolanaSlotNotification>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_2_8_pre_005_helius_transaction_request_contract_is_available_without_live_handle() {
|
||||||
|
let account = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let filter = ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter::new(
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
std::option::Option::Some("fixture-signature".to_owned()),
|
||||||
|
std::option::Option::Some(std::vec![account]),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::BalanceChanged),
|
||||||
|
);
|
||||||
|
let options = ksp_onchain_transport_lib::HeliusTransactionSubscribeOptions::new(
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTransactionSubscribeEncoding::JsonParsed),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Full),
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
std::option::Option::Some(0),
|
||||||
|
);
|
||||||
|
let request = ksp_onchain_transport_lib::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::Some(options));
|
||||||
|
assert!(request.validate().is_ok());
|
||||||
|
assert_eq!(request.filter().token_accounts(), std::option::Option::Some(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::BalanceChanged));
|
||||||
|
assert_eq!(
|
||||||
|
request.options().and_then(ksp_onchain_transport_lib::HeliusTransactionSubscribeOptions::encoding),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTransactionSubscribeEncoding::JsonParsed)
|
||||||
|
);
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::None.as_str(), "none");
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::BalanceChanged.as_str(), "balanceChanged");
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::All.as_str(), "all");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_2_8_pre_006_helius_transaction_live_handle_and_notification_types_are_available() {
|
||||||
|
let _subscribe = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::transaction_subscribe;
|
||||||
|
let _notification = std::any::type_name::<ksp_onchain_transport_lib::HeliusTransactionNotification>();
|
||||||
|
let _full = std::any::type_name::<ksp_onchain_transport_lib::HeliusFullTransactionNotification>();
|
||||||
|
let _signature = std::any::type_name::<ksp_onchain_transport_lib::HeliusTransactionSignatureNotification>();
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction.as_str(), "helius_transaction");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_2_8_pre_009_helius_slots_updates_surface_reuses_shared_typed_contract() {
|
||||||
|
let _slots_updates = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::slots_updates_subscribe;
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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: 24
|
// version: 41
|
||||||
|
|
||||||
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
|
//! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage.
|
||||||
|
|
||||||
@@ -769,3 +769,578 @@ fn release_v0_2_7_pre_012_http_inventory_remains_52_current_plus_14_historical()
|
|||||||
assert!(current.iter().all(|descriptor| return descriptor.runtime_status() == ksp_onchain_transport_lib::RpcRuntimeStatus::Supported));
|
assert!(current.iter().all(|descriptor| return descriptor.runtime_status() == ksp_onchain_transport_lib::RpcRuntimeStatus::Supported));
|
||||||
assert!(historical.iter().all(|descriptor| return descriptor.runtime_status() == ksp_onchain_transport_lib::RpcRuntimeStatus::Removed));
|
assert!(historical.iter().all(|descriptor| return descriptor.runtime_status() == ksp_onchain_transport_lib::RpcRuntimeStatus::Removed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_8_pre_002_protocol_facades_preserve_the_standard_partition() {
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard.as_str(), "solana_standard");
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream.as_str(), "helius_laserstream");
|
||||||
|
let standard_kinds = [
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Account,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Block,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Logs,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Program,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Root,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Signature,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Slot,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Vote,
|
||||||
|
];
|
||||||
|
assert_eq!(standard_kinds.len(), 9);
|
||||||
|
assert_eq!(
|
||||||
|
std::any::type_name::<ksp_onchain_transport_lib::SolanaStandardWsSession>().rsplit("::").next(),
|
||||||
|
std::option::Option::Some("SolanaStandardWsSession")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
std::any::type_name::<ksp_onchain_transport_lib::HeliusLaserStreamWsSession>().rsplit("::").next(),
|
||||||
|
std::option::Option::Some("HeliusLaserStreamWsSession")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_8_pre_003_original_six_standard_families_remain_available_after_provider_evolution() {
|
||||||
|
let _account = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::account_subscribe;
|
||||||
|
let _program = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::program_subscribe;
|
||||||
|
let _logs = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::logs_subscribe;
|
||||||
|
let _signature = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::signature_subscribe;
|
||||||
|
let _slot = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::slot_subscribe;
|
||||||
|
let _root = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::root_subscribe;
|
||||||
|
let source = include_str!("../src/ws_protocol_session.rs");
|
||||||
|
assert!(source.contains("unsupported_block"));
|
||||||
|
assert!(source.contains("unsupported_vote"));
|
||||||
|
assert!(!source.contains("pub async fn transaction_subscribe"));
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream.as_str(), "helius_laserstream");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_8_pre_005_helius_transaction_request_surface_is_typed_before_actor_integration() {
|
||||||
|
let filter = ksp_onchain_transport_lib::HeliusTransactionSubscribeFilter::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTokenAccountsFilter::All),
|
||||||
|
);
|
||||||
|
let options = ksp_onchain_transport_lib::HeliusTransactionSubscribeOptions::new(
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Processed),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::HeliusTransactionSubscribeEncoding::Base64),
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Signatures),
|
||||||
|
std::option::Option::Some(true),
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
let request = ksp_onchain_transport_lib::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::Some(options));
|
||||||
|
assert!(request.validate().is_ok());
|
||||||
|
let facade_source = include_str!("../src/ws_protocol_session.rs");
|
||||||
|
assert!(!facade_source.contains("pub async fn transaction_subscribe"));
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::WsProtocolKind::HeliusLaserStream.as_str(), "helius_laserstream");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_8_pre_006_helius_transaction_lifecycle_is_actor_integrated_without_advancing_heartbeat() {
|
||||||
|
let _transaction = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::transaction_subscribe;
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction.as_str(), "helius_transaction");
|
||||||
|
let source = include_str!("../src/ws_helius_transactions.rs");
|
||||||
|
assert!(source.contains("transactionNotification"));
|
||||||
|
assert!(source.contains("WsSubscriptionKind::HeliusTransaction"));
|
||||||
|
assert!(!source.contains("tokio_tungstenite::connect_async"));
|
||||||
|
let protocol_source = include_str!("../src/ws_protocol_session.rs");
|
||||||
|
assert!(protocol_source.contains("unsupported_block"));
|
||||||
|
assert!(protocol_source.contains("unsupported_vote"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_8_pre_007_helius_heartbeat_is_provider_owned_by_shared_actor_only() {
|
||||||
|
let actor_source = include_str!("../src/ws_session.rs");
|
||||||
|
assert!(actor_source.contains("HELIUS_WS_HEARTBEAT_INTERVAL"));
|
||||||
|
assert!(actor_source.contains("std::time::Duration::from_secs(60)"));
|
||||||
|
assert!(actor_source.contains("WsProtocolKind::HeliusLaserStream"));
|
||||||
|
assert!(actor_source.contains("tungstenite::Message::Ping"));
|
||||||
|
assert!(actor_source.contains("send_helius_heartbeat"));
|
||||||
|
let settings_source = include_str!("../src/ws_settings.rs");
|
||||||
|
assert!(!settings_source.contains("heartbeat_interval"));
|
||||||
|
assert!(!settings_source.contains("heartbeat_enabled"));
|
||||||
|
let protocol_source = include_str!("../src/ws_protocol_session.rs");
|
||||||
|
assert!(!protocol_source.contains("heartbeat_interval"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_8_pre_008_adversarial_guards_preserve_provider_isolation_and_safe_diagnostics() {
|
||||||
|
let protocol_source = include_str!("../src/ws_protocol_session.rs");
|
||||||
|
assert!(protocol_source.contains("unsupported_helius_transaction"));
|
||||||
|
assert!(protocol_source.contains("unsupported_block"));
|
||||||
|
assert!(protocol_source.contains("unsupported_vote"));
|
||||||
|
let helius_source = include_str!("../src/ws_helius_transactions.rs");
|
||||||
|
assert!(helius_source.contains("impl std::fmt::Debug for HeliusFullTransactionNotification"));
|
||||||
|
assert!(helius_source.contains("impl std::fmt::Debug for HeliusTransactionSignatureNotification"));
|
||||||
|
assert!(helius_source.contains("impl std::fmt::Debug for HeliusTransactionNotification"));
|
||||||
|
assert!(helius_source.contains("Self::Unknown(_)"));
|
||||||
|
assert!(helius_source.contains("<omitted>"));
|
||||||
|
let actor_source = include_str!("../src/ws_session.rs");
|
||||||
|
assert!(actor_source.contains("max_message_size_bytes"));
|
||||||
|
assert!(actor_source.contains("max_frame_size_bytes"));
|
||||||
|
assert!(actor_source.contains("WsNotificationDispatchOutcome::QueueFull"));
|
||||||
|
assert!(actor_source.contains("ERROR_CODE_WS_BACKPRESSURE_OVERFLOW"));
|
||||||
|
assert!(actor_source.contains("remote_to_local.remove"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_8_pre_009_http_and_standard_websocket_inventories_remain_exact() {
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::current_http_rpc_methods().len(), 52);
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::historical_http_rpc_methods().len(), 14);
|
||||||
|
let standard = [
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Account,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Block,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Logs,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Program,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Root,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Signature,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Slot,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates,
|
||||||
|
ksp_onchain_transport_lib::WsSubscriptionKind::Vote,
|
||||||
|
];
|
||||||
|
assert_eq!(standard.len(), 9);
|
||||||
|
let lifecycle_source = include_str!("../src/ws_lifecycle.rs");
|
||||||
|
for method in [
|
||||||
|
"accountSubscribe",
|
||||||
|
"accountUnsubscribe",
|
||||||
|
"blockSubscribe",
|
||||||
|
"blockUnsubscribe",
|
||||||
|
"logsSubscribe",
|
||||||
|
"logsUnsubscribe",
|
||||||
|
"programSubscribe",
|
||||||
|
"programUnsubscribe",
|
||||||
|
"rootSubscribe",
|
||||||
|
"rootUnsubscribe",
|
||||||
|
"signatureSubscribe",
|
||||||
|
"signatureUnsubscribe",
|
||||||
|
"slotSubscribe",
|
||||||
|
"slotUnsubscribe",
|
||||||
|
"slotsUpdatesSubscribe",
|
||||||
|
"slotsUpdatesUnsubscribe",
|
||||||
|
"voteSubscribe",
|
||||||
|
"voteUnsubscribe",
|
||||||
|
] {
|
||||||
|
assert!(lifecycle_source.contains(method), "missing standard WebSocket method mapping: {method}");
|
||||||
|
}
|
||||||
|
let _account = ksp_onchain_transport_lib::SolanaStandardWsSession::account_subscribe;
|
||||||
|
let _block = ksp_onchain_transport_lib::SolanaStandardWsSession::block_subscribe;
|
||||||
|
let _logs = ksp_onchain_transport_lib::SolanaStandardWsSession::logs_subscribe;
|
||||||
|
let _program = ksp_onchain_transport_lib::SolanaStandardWsSession::program_subscribe;
|
||||||
|
let _root = ksp_onchain_transport_lib::SolanaStandardWsSession::root_subscribe;
|
||||||
|
let _signature = ksp_onchain_transport_lib::SolanaStandardWsSession::signature_subscribe;
|
||||||
|
let _slot = ksp_onchain_transport_lib::SolanaStandardWsSession::slot_subscribe;
|
||||||
|
let _slots_updates = ksp_onchain_transport_lib::SolanaStandardWsSession::slots_updates_subscribe;
|
||||||
|
let _vote = ksp_onchain_transport_lib::SolanaStandardWsSession::vote_subscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_8_pre_009_helius_surface_is_seven_standard_families_plus_transaction() {
|
||||||
|
let _account = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::account_subscribe;
|
||||||
|
let _program = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::program_subscribe;
|
||||||
|
let _logs = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::logs_subscribe;
|
||||||
|
let _signature = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::signature_subscribe;
|
||||||
|
let _slot = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::slot_subscribe;
|
||||||
|
let _root = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::root_subscribe;
|
||||||
|
let _slots_updates = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::slots_updates_subscribe;
|
||||||
|
let _transaction = ksp_onchain_transport_lib::HeliusLaserStreamWsSession::transaction_subscribe;
|
||||||
|
let protocol_source = include_str!("../src/ws_protocol_session.rs");
|
||||||
|
assert!(protocol_source.contains("unsupported_block"));
|
||||||
|
assert!(protocol_source.contains("unsupported_vote"));
|
||||||
|
assert!(!protocol_source.contains("unsupported_slots_updates"));
|
||||||
|
let cluster_source = include_str!("../src/ws_cluster.rs");
|
||||||
|
assert!(cluster_source.contains("impl crate::HeliusLaserStreamWsSession"));
|
||||||
|
assert!(cluster_source.contains("pub async fn slots_updates_subscribe"));
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::WsSubscriptionKind::SlotsUpdates.as_str(), "slots_updates");
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::WsSubscriptionKind::HeliusTransaction.as_str(), "helius_transaction");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_8_pre_009_config_secret_and_dependency_boundaries_remain_wired() {
|
||||||
|
let manifest_directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let workspace = manifest_directory.parent().and_then(std::path::Path::parent).expect("Transport integration test must resolve the workspace root");
|
||||||
|
let transport_manifest = std::fs::read_to_string(manifest_directory.join("Cargo.toml")).expect("Transport manifest must be readable");
|
||||||
|
for forbidden in ["ksp-config-lib", "ksp-store-api", "ksp-store-lib", "ksp-program-api", "ksp-program-lib", "tracing =", "tracing."] {
|
||||||
|
assert!(!transport_manifest.contains(forbidden), "forbidden direct Transport dependency detected: {forbidden}");
|
||||||
|
}
|
||||||
|
let config_manifest = std::fs::read_to_string(workspace.join("crates/ksp-config-lib/Cargo.toml")).expect("Config manifest must be readable");
|
||||||
|
assert!(config_manifest.contains("ksp-onchain-transport-lib"));
|
||||||
|
let config_transport =
|
||||||
|
std::fs::read_to_string(workspace.join("crates/ksp-config-lib/src/transport.rs")).expect("Config Transport adapter source must be readable");
|
||||||
|
assert!(config_transport.contains("WsProtocolKind::HeliusLaserStream"));
|
||||||
|
let transport_example = std::fs::read_to_string(workspace.join("config/examples/std.transport.example.json")).expect("Transport example must be readable");
|
||||||
|
assert!(transport_example.contains("\"kind\": \"helius_laserstream\""));
|
||||||
|
assert!(transport_example.contains("${KSP_SECRET_HELIUS_API_KEY"));
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_v0_2_8_pre_010_live_smoke_policy_preserves_secret_and_dependency_ownership() {
|
||||||
|
let manifest_directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let workspace = manifest_directory.parent().and_then(std::path::Path::parent).expect("Transport integration test must resolve the workspace root");
|
||||||
|
let transport_manifest = std::fs::read_to_string(manifest_directory.join("Cargo.toml")).expect("Transport manifest must be readable");
|
||||||
|
assert!(!transport_manifest.contains("ksp-config-lib"));
|
||||||
|
let transport_ws_smoke =
|
||||||
|
std::fs::read_to_string(manifest_directory.join("tests/websocket_devnet_smoke.rs")).expect("Transport WebSocket smoke must be readable");
|
||||||
|
assert!(!transport_ws_smoke.contains("KSP_SECRET_HELIUS_API_KEY"));
|
||||||
|
assert!(!transport_ws_smoke.contains("ConfigEnvironment"));
|
||||||
|
assert!(!workspace.join("crates/ksp-config-lib/tests/helius_websocket_smoke.rs").exists());
|
||||||
|
let readme = std::fs::read_to_string(manifest_directory.join("README.md")).expect("Transport README must be readable");
|
||||||
|
assert!(readme.contains("Aucun smoke Helius live supplémentaire n’est committé en `0.2.8-pre.010`"));
|
||||||
|
assert!(readme.contains("KSP_SECRET_HELIUS_API_KEY"));
|
||||||
|
let usage = std::fs::read_to_string(manifest_directory.join("USAGE.md")).expect("Transport USAGE must be readable");
|
||||||
|
assert!(usage.contains("### Smoke Helius live"));
|
||||||
|
assert!(usage.contains("surface KSP d’intégration/orchestration dédiée"));
|
||||||
|
assert!(usage.contains("cargo tree -p ksp-onchain-transport-lib"));
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/tests/yellowstone_orbitflare_smoke.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
//! Opt-in live OrbitFlare Devnet characterization smoke for authenticated provider-neutral Yellowstone gRPC Subscribe.
|
||||||
|
|
||||||
|
use std::io::IsTerminal; // rust-rules: trait-import
|
||||||
|
|
||||||
|
fn orbitflare_license_x_token() -> std::string::String {
|
||||||
|
assert!(
|
||||||
|
!std::io::stdin().is_terminal(),
|
||||||
|
"pipe the OrbitFlare Solana Free License Key to this ignored smoke on stdin; never pass it as a command-line argument"
|
||||||
|
);
|
||||||
|
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!("OrbitFlare License Key x-token must be readable from smoke stdin: {error}"),
|
||||||
|
}
|
||||||
|
let token = token.trim().to_owned();
|
||||||
|
assert!(!token.is_empty(), "OrbitFlare License Key x-token provided on smoke stdin must not be empty");
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn orbitflare_devnet_endpoint(x_token: &str) -> ksp_core_lib::Result<ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings> {
|
||||||
|
let url = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse("http://devnet.rpc.orbitflare.com:10000") {
|
||||||
|
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(
|
||||||
|
"orbitflare_devnet_yellowstone",
|
||||||
|
true,
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("orbitflare"),
|
||||||
|
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new("devnet"),
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
request.set_commitment(std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
|
||||||
|
return std::result::Result::Ok(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
#[ignore = "opt-in live OrbitFlare Devnet Yellowstone gRPC characterization; reads one Solana Free License Key x-token from stdin, waits for Slot plus standard server Ping, then closes"]
|
||||||
|
async fn orbitflare_devnet_yellowstone_streams_slots_and_emits_standard_server_ping_with_license_x_token() {
|
||||||
|
let x_token = orbitflare_license_x_token();
|
||||||
|
let endpoint = match orbitflare_devnet_endpoint(x_token.as_str()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
panic!("programmatic OrbitFlare Devnet Yellowstone settings must accept the License Key as secret x-token metadata: {error:?}")
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(endpoint.metadata().len(), 1);
|
||||||
|
assert_eq!(endpoint.metadata()[0].key(), "x-token");
|
||||||
|
assert!(endpoint.metadata()[0].is_secret());
|
||||||
|
assert!(!endpoint.url().uses_tls(), "the documented OrbitFlare shared Devnet endpoint is intentionally HTTP/2 plaintext");
|
||||||
|
let endpoint_debug = format!("{endpoint:?}");
|
||||||
|
assert!(!endpoint_debug.contains("orbitflare.com"), "OrbitFlare endpoint URL must not appear in endpoint Debug");
|
||||||
|
assert!(!endpoint_debug.contains(x_token.as_str()), "OrbitFlare License Key x-token must not appear in endpoint Debug");
|
||||||
|
assert!(!endpoint_debug.contains("X-ORBIT-KEY"), "OrbitFlare Customer API credentials must not enter Yellowstone settings");
|
||||||
|
let channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(&endpoint).await {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("OrbitFlare Devnet Yellowstone authenticated connection must succeed: {error:?}"),
|
||||||
|
};
|
||||||
|
assert_eq!(channel.endpoint_name(), "orbitflare_devnet_yellowstone");
|
||||||
|
assert_eq!(channel.provider().as_str(), "orbitflare");
|
||||||
|
assert_eq!(channel.cluster().as_str(), "devnet");
|
||||||
|
let request = match slot_request() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("OrbitFlare 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!("OrbitFlare Devnet standard Subscribe with License Key x-token must open: {error:?}"),
|
||||||
|
};
|
||||||
|
let deadline = tokio::time::sleep(std::time::Duration::from_secs(45));
|
||||||
|
tokio::pin!(deadline);
|
||||||
|
let mut slot_seen = false;
|
||||||
|
let mut ping_seen = false;
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
() = &mut deadline => break,
|
||||||
|
next_update = session.next_update() => {
|
||||||
|
let next_update = match next_update {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("OrbitFlare Devnet Yellowstone Subscribe update must decode: {error:?}"),
|
||||||
|
};
|
||||||
|
match next_update {
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Slot(update)) => {
|
||||||
|
if update.slot() > 0 {
|
||||||
|
slot_seen = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
std::option::Option::Some(ksp_onchain_transport_lib::YellowstoneSubscribeUpdate::Ping(_)) => ping_seen = true,
|
||||||
|
std::option::Option::Some(_) => {},
|
||||||
|
std::option::Option::None => break,
|
||||||
|
}
|
||||||
|
if slot_seen && ping_seen {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 live characterization, OrbitFlare close may time out waiting for provider half-close but must not fail for another reason: {error:?}"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert!(slot_seen, "OrbitFlare Devnet Yellowstone must publish at least one non-zero confirmed slot during the 45-second characterization window");
|
||||||
|
assert!(
|
||||||
|
ping_seen,
|
||||||
|
"OrbitFlare Devnet must emit the standard Yellowstone server Ping during the 45-second characterization window before any provider-specific heartbeat is justified"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
60
crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
Normal file
60
crates/ksp-onchain-transport-lib/unit_tests/grpc_channel.rs
Normal 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"));
|
||||||
|
}
|
||||||
198
crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
Normal file
198
crates/ksp-onchain-transport-lib/unit_tests/grpc_settings.rs
Normal 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());
|
||||||
|
}
|
||||||
685
crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs
Normal file
685
crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs
Normal 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;
|
||||||
|
}
|
||||||
777
crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs
Normal file
777
crates/ksp-onchain-transport-lib/unit_tests/grpc_subscribe.rs
Normal 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());
|
||||||
|
}
|
||||||
288
crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs
Normal file
288
crates/ksp-onchain-transport-lib/unit_tests/grpc_unary.rs
Normal 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);
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
@@ -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(
|
||||||
@@ -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(
|
||||||
@@ -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");
|
||||||
@@ -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");
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_helius_standard.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
use futures_util::SinkExt; // rust-rules: trait-import
|
||||||
|
use futures_util::StreamExt; // rust-rules: trait-import
|
||||||
|
|
||||||
|
fn helius_endpoint(url: &str) -> crate::WsEndpointSettings {
|
||||||
|
return crate::WsEndpointSettings::new(
|
||||||
|
"local_helius_standard_fixture",
|
||||||
|
true,
|
||||||
|
crate::WsProviderName::new("helius"),
|
||||||
|
crate::WsClusterName::new("local"),
|
||||||
|
crate::WsProtocolKind::HeliusLaserStream,
|
||||||
|
crate::WsEndpointUrl::parse(url).expect("local Helius WebSocket URL must parse"),
|
||||||
|
crate::WsSessionSettings::default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn bind_local_listener() -> (tokio::net::TcpListener, std::string::String) {
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("local listener must bind");
|
||||||
|
let address = listener.local_addr().expect("local listener must expose address");
|
||||||
|
return (listener, format!("ws://{address}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_request(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) -> serde_json::Value {
|
||||||
|
let message = websocket.next().await.expect("request message must exist").expect("request message must decode");
|
||||||
|
let text = message.to_text().expect("request must be text");
|
||||||
|
return serde_json::from_str(text).expect("request must contain JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_result(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, request: &serde_json::Value, result: serde_json::Value) {
|
||||||
|
let id = request.get("id").and_then(serde_json::Value::as_u64).expect("request id must be numeric");
|
||||||
|
let response = serde_json::json!({"jsonrpc":"2.0","id":id,"result":result});
|
||||||
|
websocket.send(tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())).await.expect("local response must send");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn expect_pair(
|
||||||
|
websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>,
|
||||||
|
subscribe_method: &str,
|
||||||
|
expected_params: serde_json::Value,
|
||||||
|
unsubscribe_method: &str,
|
||||||
|
remote_id: u64,
|
||||||
|
) {
|
||||||
|
let subscribe = read_request(websocket).await;
|
||||||
|
assert_eq!(subscribe["method"], serde_json::Value::String(subscribe_method.to_owned()));
|
||||||
|
assert_eq!(subscribe["params"], expected_params);
|
||||||
|
send_result(websocket, &subscribe, serde_json::json!(remote_id)).await;
|
||||||
|
let unsubscribe = read_request(websocket).await;
|
||||||
|
assert_eq!(unsubscribe["method"], serde_json::Value::String(unsubscribe_method.to_owned()));
|
||||||
|
assert_eq!(unsubscribe["params"], serde_json::json!([remote_id]));
|
||||||
|
send_result(websocket, &unsubscribe, serde_json::json!(true)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_close_frame(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) {
|
||||||
|
loop {
|
||||||
|
let message = websocket.next().await;
|
||||||
|
match message {
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => return,
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(_)) => {},
|
||||||
|
std::option::Option::Some(std::result::Result::Err(_)) | std::option::Option::None => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn helius_facade_reuses_exact_standard_wire_for_all_seven_currently_supported_families() {
|
||||||
|
let (listener, url) = bind_local_listener().await;
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let (stream, _) = listener.accept().await.expect("local server must accept client");
|
||||||
|
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
|
||||||
|
expect_pair(
|
||||||
|
&mut websocket,
|
||||||
|
"accountSubscribe",
|
||||||
|
serde_json::json!(["11111111111111111111111111111111", {"encoding":"base64","commitment":"confirmed"}]),
|
||||||
|
"accountUnsubscribe",
|
||||||
|
101,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
expect_pair(
|
||||||
|
&mut websocket,
|
||||||
|
"programSubscribe",
|
||||||
|
serde_json::json!(["11111111111111111111111111111111", {"encoding":"jsonParsed","filters":[{"dataSize":80}],"withContext":true}]),
|
||||||
|
"programUnsubscribe",
|
||||||
|
102,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
expect_pair(&mut websocket, "logsSubscribe", serde_json::json!(["all", {"commitment":"finalized"}]), "logsUnsubscribe", 103).await;
|
||||||
|
expect_pair(
|
||||||
|
&mut websocket,
|
||||||
|
"signatureSubscribe",
|
||||||
|
serde_json::json!(["fixture-signature", {"commitment":"confirmed","enableReceivedNotification":true}]),
|
||||||
|
"signatureUnsubscribe",
|
||||||
|
104,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
expect_pair(&mut websocket, "slotSubscribe", serde_json::json!([]), "slotUnsubscribe", 105).await;
|
||||||
|
expect_pair(&mut websocket, "rootSubscribe", serde_json::json!([]), "rootUnsubscribe", 106).await;
|
||||||
|
expect_pair(&mut websocket, "slotsUpdatesSubscribe", serde_json::json!([]), "slotsUpdatesUnsubscribe", 107).await;
|
||||||
|
wait_for_close_frame(&mut websocket).await;
|
||||||
|
});
|
||||||
|
let session = crate::HeliusLaserStreamWsSession::connect(helius_endpoint(url.as_str())).await.expect("Helius facade must connect");
|
||||||
|
let pubkey = "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>().expect("fixture pubkey must parse");
|
||||||
|
let account_config = crate::SolanaAccountSubscribeConfig::new(
|
||||||
|
std::option::Option::Some(crate::SolanaAccountEncoding::Base64),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||||
|
);
|
||||||
|
let mut account = session.account_subscribe(&pubkey, std::option::Option::Some(&account_config)).await.expect("Helius accountSubscribe must register");
|
||||||
|
assert!(account.unsubscribe().await.expect("Helius accountUnsubscribe must complete"));
|
||||||
|
let program_config = crate::SolanaProgramSubscribeConfig::new(
|
||||||
|
crate::SolanaAccountSubscribeConfig::new(
|
||||||
|
std::option::Option::Some(crate::SolanaAccountEncoding::JsonParsed),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
),
|
||||||
|
std::vec![crate::SolanaProgramAccountFilter::DataSize(80)],
|
||||||
|
std::option::Option::Some(true),
|
||||||
|
);
|
||||||
|
let mut program = session.program_subscribe(&pubkey, std::option::Option::Some(&program_config)).await.expect("Helius programSubscribe must register");
|
||||||
|
assert!(program.unsubscribe().await.expect("Helius programUnsubscribe must complete"));
|
||||||
|
let logs_config = crate::SolanaCommitmentConfig::new(std::option::Option::Some(crate::SolanaCommitment::Finalized));
|
||||||
|
let mut logs = session
|
||||||
|
.logs_subscribe(&crate::SolanaLogsSubscribeFilter::All, std::option::Option::Some(&logs_config))
|
||||||
|
.await
|
||||||
|
.expect("Helius logsSubscribe must register");
|
||||||
|
assert!(logs.unsubscribe().await.expect("Helius logsUnsubscribe must complete"));
|
||||||
|
let signature_config =
|
||||||
|
crate::SolanaSignatureSubscribeConfig::new(std::option::Option::Some(crate::SolanaCommitment::Confirmed), std::option::Option::Some(true));
|
||||||
|
let mut signature = session
|
||||||
|
.signature_subscribe("fixture-signature", std::option::Option::Some(&signature_config))
|
||||||
|
.await
|
||||||
|
.expect("Helius signatureSubscribe must register");
|
||||||
|
assert!(signature.unsubscribe().await.expect("Helius signatureUnsubscribe must complete"));
|
||||||
|
let mut slot = session.slot_subscribe().await.expect("Helius slotSubscribe must register");
|
||||||
|
assert!(slot.unsubscribe().await.expect("Helius slotUnsubscribe must complete"));
|
||||||
|
let mut root = session.root_subscribe().await.expect("Helius rootSubscribe must register");
|
||||||
|
assert!(root.unsubscribe().await.expect("Helius rootUnsubscribe must complete"));
|
||||||
|
let mut slots_updates = session.slots_updates_subscribe().await.expect("Helius slotsUpdatesSubscribe must register");
|
||||||
|
assert!(slots_updates.unsubscribe().await.expect("Helius slotsUpdatesUnsubscribe must complete"));
|
||||||
|
session.close().await.expect("Helius facade close must complete");
|
||||||
|
server.await.expect("local Helius peer task must complete");
|
||||||
|
}
|
||||||
@@ -0,0 +1,732 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs
|
||||||
|
// version: 5
|
||||||
|
|
||||||
|
use futures_util::SinkExt; // rust-rules: trait-import
|
||||||
|
use futures_util::StreamExt; // rust-rules: trait-import
|
||||||
|
|
||||||
|
fn pubkey(value: &str) -> ksp_core_lib::Pubkey {
|
||||||
|
return value.parse::<ksp_core_lib::Pubkey>().expect("fixture public key must parse");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn base_filter() -> crate::HeliusTransactionSubscribeFilter {
|
||||||
|
return crate::HeliusTransactionSubscribeFilter::new(
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
std::option::Option::Some("fixture-signature-secret-canary".to_owned()),
|
||||||
|
std::option::Option::Some(std::vec![pubkey("11111111111111111111111111111111")]),
|
||||||
|
std::option::Option::Some(std::vec![pubkey("SysvarC1ock11111111111111111111111111111111")]),
|
||||||
|
std::option::Option::Some(std::vec![pubkey("Vote111111111111111111111111111111111111111")]),
|
||||||
|
std::option::Option::Some(crate::HeliusTokenAccountsFilter::BalanceChanged),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_oversized_filter_rejected(filter: crate::HeliusTransactionSubscribeFilter) {
|
||||||
|
let request = crate::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::None);
|
||||||
|
let error = request.validate().expect_err("50,001 Helius account filters must fail before I/O");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||||
|
assert!(error.to_string().contains("invalid_rpc_parameters"));
|
||||||
|
assert!(!error.to_string().contains("11111111111111111111111111111111"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn helius_endpoint(url: &str) -> crate::WsEndpointSettings {
|
||||||
|
return helius_endpoint_with_session(url, crate::WsSessionSettings::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn helius_endpoint_with_session(url: &str, session: crate::WsSessionSettings) -> crate::WsEndpointSettings {
|
||||||
|
return crate::WsEndpointSettings::new(
|
||||||
|
"local_helius_transaction_fixture",
|
||||||
|
true,
|
||||||
|
crate::WsProviderName::new("helius"),
|
||||||
|
crate::WsClusterName::new("local"),
|
||||||
|
crate::WsProtocolKind::HeliusLaserStream,
|
||||||
|
crate::WsEndpointUrl::parse(url).expect("local Helius WebSocket URL must parse"),
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reconnect_session_settings(backoff: std::time::Duration) -> crate::WsSessionSettings {
|
||||||
|
let defaults = crate::WsSessionSettings::default();
|
||||||
|
return crate::WsSessionSettings::new(
|
||||||
|
std::time::Duration::from_millis(250),
|
||||||
|
std::time::Duration::from_millis(200),
|
||||||
|
crate::WsReconnectSettings::new(2, backoff, backoff),
|
||||||
|
crate::WsResubscribePolicy::ActiveSubscriptions,
|
||||||
|
defaults.command_queue_capacity(),
|
||||||
|
defaults.notification_queue_capacity(),
|
||||||
|
defaults.max_active_subscriptions(),
|
||||||
|
defaults.max_pending_requests(),
|
||||||
|
defaults.max_message_size_bytes(),
|
||||||
|
defaults.max_frame_size_bytes(),
|
||||||
|
defaults.max_write_buffer_size_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backpressure_session_settings() -> crate::WsSessionSettings {
|
||||||
|
let defaults = crate::WsSessionSettings::default();
|
||||||
|
return crate::WsSessionSettings::new(
|
||||||
|
std::time::Duration::from_millis(250),
|
||||||
|
std::time::Duration::from_millis(200),
|
||||||
|
crate::WsReconnectSettings::new(0, std::time::Duration::from_millis(10), std::time::Duration::from_millis(10)),
|
||||||
|
crate::WsResubscribePolicy::ActiveSubscriptions,
|
||||||
|
defaults.command_queue_capacity(),
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
defaults.max_pending_requests(),
|
||||||
|
defaults.max_message_size_bytes(),
|
||||||
|
defaults.max_frame_size_bytes(),
|
||||||
|
defaults.max_write_buffer_size_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adversarial_payload_session_settings() -> crate::WsSessionSettings {
|
||||||
|
let defaults = crate::WsSessionSettings::default();
|
||||||
|
return crate::WsSessionSettings::new(
|
||||||
|
std::time::Duration::from_millis(250),
|
||||||
|
std::time::Duration::from_millis(200),
|
||||||
|
crate::WsReconnectSettings::new(2, std::time::Duration::from_millis(20), std::time::Duration::from_millis(20)),
|
||||||
|
crate::WsResubscribePolicy::ActiveSubscriptions,
|
||||||
|
defaults.command_queue_capacity(),
|
||||||
|
defaults.notification_queue_capacity(),
|
||||||
|
defaults.max_active_subscriptions(),
|
||||||
|
defaults.max_pending_requests(),
|
||||||
|
256,
|
||||||
|
128,
|
||||||
|
defaults.max_write_buffer_size_bytes(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn bind_local_listener() -> (tokio::net::TcpListener, std::string::String) {
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("local listener must bind");
|
||||||
|
let address = listener.local_addr().expect("local listener must expose address");
|
||||||
|
return (listener, format!("ws://{address}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_request(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) -> serde_json::Value {
|
||||||
|
let message = websocket.next().await.expect("request message must exist").expect("request message must decode");
|
||||||
|
let text = message.to_text().expect("request must be text");
|
||||||
|
return serde_json::from_str(text).expect("request must contain JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_result(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, request: &serde_json::Value, result: serde_json::Value) {
|
||||||
|
let id = request.get("id").and_then(serde_json::Value::as_u64).expect("request id must be numeric");
|
||||||
|
let response = serde_json::json!({"jsonrpc":"2.0","id":id,"result":result});
|
||||||
|
websocket.send(tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())).await.expect("local response must send");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_error(
|
||||||
|
websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>,
|
||||||
|
request: &serde_json::Value,
|
||||||
|
code: i64,
|
||||||
|
message: &str,
|
||||||
|
data: serde_json::Value,
|
||||||
|
) {
|
||||||
|
let id = request.get("id").and_then(serde_json::Value::as_u64).expect("request id must be numeric");
|
||||||
|
let response = serde_json::json!({"jsonrpc":"2.0","id":id,"error":{"code":code,"message":message,"data":data}});
|
||||||
|
websocket.send(tokio_tungstenite::tungstenite::Message::Text(response.to_string().into())).await.expect("local error response must send");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_notification(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, subscription: u64, result: serde_json::Value) {
|
||||||
|
let notification = serde_json::json!({"jsonrpc":"2.0","method":"transactionNotification","params":{"subscription":subscription,"result":result}});
|
||||||
|
websocket.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into())).await.expect("local notification must send");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_root_notification(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>, subscription: u64, root: u64) {
|
||||||
|
let notification = serde_json::json!({"jsonrpc":"2.0","method":"rootNotification","params":{"subscription":subscription,"result":root}});
|
||||||
|
websocket
|
||||||
|
.send(tokio_tungstenite::tungstenite::Message::Text(notification.to_string().into()))
|
||||||
|
.await
|
||||||
|
.expect("local root notification must send");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_gap_count(session: &crate::HeliusLaserStreamWsSession, expected: u64) {
|
||||||
|
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||||
|
loop {
|
||||||
|
if session.snapshot().continuity_gap_count() >= expected && session.state() == crate::WsSessionState::Active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert!(tokio::time::Instant::now() < deadline, "Helius session continuity gap count must advance before timeout");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_overflow_count(session: &crate::HeliusLaserStreamWsSession, expected: u64) {
|
||||||
|
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||||
|
loop {
|
||||||
|
if session.snapshot().overflow_count() >= expected {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert!(tokio::time::Instant::now() < deadline, "Helius session overflow count must advance before timeout");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_session_subscription_count(session: &crate::HeliusLaserStreamWsSession, expected: usize) {
|
||||||
|
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||||
|
loop {
|
||||||
|
if session.snapshot().subscription_count() == expected {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert!(tokio::time::Instant::now() < deadline, "Helius session subscription count must settle before timeout");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_subscription_state<T>(subscription: &crate::WsSubscription<T>, expected: crate::WsSubscriptionState) {
|
||||||
|
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||||
|
loop {
|
||||||
|
if subscription.state() == expected {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert!(tokio::time::Instant::now() < deadline, "Helius logical subscription state must advance before timeout");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_close_frame(websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) {
|
||||||
|
loop {
|
||||||
|
let message = websocket.next().await;
|
||||||
|
match message {
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => return,
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(_)) => {},
|
||||||
|
std::option::Option::Some(std::result::Result::Err(_)) | std::option::Option::None => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helius_transaction_filter_and_option_enums_match_documented_wire_labels() {
|
||||||
|
assert_eq!(crate::HeliusTokenAccountsFilter::None.as_str(), "none");
|
||||||
|
assert_eq!(crate::HeliusTokenAccountsFilter::BalanceChanged.as_str(), "balanceChanged");
|
||||||
|
assert_eq!(crate::HeliusTokenAccountsFilter::All.as_str(), "all");
|
||||||
|
assert_eq!(crate::HeliusTransactionSubscribeEncoding::Base58.as_str(), "base58");
|
||||||
|
assert_eq!(crate::HeliusTransactionSubscribeEncoding::Base64.as_str(), "base64");
|
||||||
|
assert_eq!(crate::HeliusTransactionSubscribeEncoding::JsonParsed.as_str(), "jsonParsed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helius_transaction_subscribe_request_serializes_complete_documented_filter_and_options() {
|
||||||
|
let filter = base_filter();
|
||||||
|
let options = crate::HeliusTransactionSubscribeOptions::new(
|
||||||
|
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||||
|
std::option::Option::Some(crate::HeliusTransactionSubscribeEncoding::JsonParsed),
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionDetails::Accounts),
|
||||||
|
std::option::Option::Some(true),
|
||||||
|
std::option::Option::Some(0),
|
||||||
|
);
|
||||||
|
let request = crate::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::Some(options));
|
||||||
|
let params = super::helius_transaction_subscribe_params(&request).expect("complete documented Helius request must validate");
|
||||||
|
assert_eq!(
|
||||||
|
params,
|
||||||
|
std::vec![
|
||||||
|
serde_json::json!({
|
||||||
|
"vote": false,
|
||||||
|
"failed": false,
|
||||||
|
"signature": "fixture-signature-secret-canary",
|
||||||
|
"accountInclude": ["11111111111111111111111111111111"],
|
||||||
|
"accountExclude": ["SysvarC1ock11111111111111111111111111111111"],
|
||||||
|
"accountRequired": ["Vote111111111111111111111111111111111111111"],
|
||||||
|
"tokenAccounts": "balanceChanged"
|
||||||
|
}),
|
||||||
|
serde_json::json!({
|
||||||
|
"commitment": "confirmed",
|
||||||
|
"encoding": "jsonParsed",
|
||||||
|
"transactionDetails": "accounts",
|
||||||
|
"showRewards": true,
|
||||||
|
"maxSupportedTransactionVersion": 0
|
||||||
|
})
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(request.filter().vote(), std::option::Option::Some(false));
|
||||||
|
assert_eq!(request.filter().failed(), std::option::Option::Some(false));
|
||||||
|
assert_eq!(request.filter().signature(), std::option::Option::Some("fixture-signature-secret-canary"));
|
||||||
|
assert_eq!(request.filter().account_include().map(<[ksp_core_lib::Pubkey]>::len), std::option::Option::Some(1));
|
||||||
|
assert_eq!(request.filter().account_exclude().map(<[ksp_core_lib::Pubkey]>::len), std::option::Option::Some(1));
|
||||||
|
assert_eq!(request.filter().account_required().map(<[ksp_core_lib::Pubkey]>::len), std::option::Option::Some(1));
|
||||||
|
assert_eq!(request.filter().token_accounts(), std::option::Option::Some(crate::HeliusTokenAccountsFilter::BalanceChanged));
|
||||||
|
let options = request.options().expect("options must remain available");
|
||||||
|
assert_eq!(options.commitment(), std::option::Option::Some(crate::SolanaCommitment::Confirmed));
|
||||||
|
assert_eq!(options.encoding(), std::option::Option::Some(crate::HeliusTransactionSubscribeEncoding::JsonParsed));
|
||||||
|
assert_eq!(options.transaction_details(), std::option::Option::Some(crate::SolanaTransactionDetails::Accounts));
|
||||||
|
assert_eq!(options.show_rewards(), std::option::Option::Some(true));
|
||||||
|
assert_eq!(options.max_supported_transaction_version(), std::option::Option::Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helius_transaction_request_preserves_omitted_explicit_empty_and_explicit_none_states() {
|
||||||
|
let omitted = crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::None);
|
||||||
|
assert_eq!(
|
||||||
|
super::helius_transaction_subscribe_params(&omitted).expect("fully omitted optional request must validate"),
|
||||||
|
std::vec![serde_json::json!({})]
|
||||||
|
);
|
||||||
|
let explicit = crate::HeliusTransactionSubscribeRequest::new(
|
||||||
|
crate::HeliusTransactionSubscribeFilter::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(std::vec::Vec::new()),
|
||||||
|
std::option::Option::Some(std::vec::Vec::new()),
|
||||||
|
std::option::Option::Some(std::vec::Vec::new()),
|
||||||
|
std::option::Option::Some(crate::HeliusTokenAccountsFilter::None),
|
||||||
|
),
|
||||||
|
std::option::Option::Some(crate::HeliusTransactionSubscribeOptions::default()),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
super::helius_transaction_subscribe_params(&explicit).expect("explicit empty Helius request states must validate"),
|
||||||
|
std::vec![serde_json::json!({"accountInclude":[],"accountExclude":[],"accountRequired":[],"tokenAccounts":"none"}), serde_json::json!({})]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helius_transaction_filter_enforces_each_documented_fifty_thousand_account_bound() {
|
||||||
|
let key = pubkey("11111111111111111111111111111111");
|
||||||
|
let maximum = std::vec![key; 50_000];
|
||||||
|
let accepted = crate::HeliusTransactionSubscribeRequest::new(
|
||||||
|
crate::HeliusTransactionSubscribeFilter::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(maximum),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
),
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
assert!(accepted.validate().is_ok());
|
||||||
|
assert_oversized_filter_rejected(crate::HeliusTransactionSubscribeFilter::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(std::vec![key; 50_001]),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
));
|
||||||
|
assert_oversized_filter_rejected(crate::HeliusTransactionSubscribeFilter::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(std::vec![key; 50_001]),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
));
|
||||||
|
assert_oversized_filter_rejected(crate::HeliusTransactionSubscribeFilter::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(std::vec![key; 50_001]),
|
||||||
|
std::option::Option::None,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helius_transaction_details_require_max_supported_version_only_for_accounts_and_full() {
|
||||||
|
for details in [crate::SolanaTransactionDetails::Full, crate::SolanaTransactionDetails::Accounts] {
|
||||||
|
let options = crate::HeliusTransactionSubscribeOptions::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(details),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
let request = crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::Some(options));
|
||||||
|
let error = request.validate().expect_err("full/accounts details must require maxSupportedTransactionVersion");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_RPC_PARAMETERS);
|
||||||
|
}
|
||||||
|
for details in [crate::SolanaTransactionDetails::Signatures, crate::SolanaTransactionDetails::None] {
|
||||||
|
let options = crate::HeliusTransactionSubscribeOptions::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(details),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
);
|
||||||
|
let request = crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::Some(options));
|
||||||
|
assert!(request.validate().is_ok());
|
||||||
|
}
|
||||||
|
let full_with_version = crate::HeliusTransactionSubscribeOptions::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionDetails::Full),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(0),
|
||||||
|
);
|
||||||
|
let request =
|
||||||
|
crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::Some(full_with_version));
|
||||||
|
assert!(request.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helius_transaction_notification_decoder_preserves_full_signature_and_unknown_shapes() {
|
||||||
|
let full_value = serde_json::json!({
|
||||||
|
"transaction":{"transaction":["AAAA","base64"],"meta":{"err":null}},
|
||||||
|
"signature":"full-signature",
|
||||||
|
"slot":224341380,
|
||||||
|
"transactionIndex":42
|
||||||
|
});
|
||||||
|
let full = super::decode_helius_transaction_notification(full_value.clone()).expect("full Helius notification must decode");
|
||||||
|
match full {
|
||||||
|
crate::HeliusTransactionNotification::Full(notification) => {
|
||||||
|
assert_eq!(notification.transaction(), &full_value["transaction"]);
|
||||||
|
assert_eq!(notification.signature(), "full-signature");
|
||||||
|
assert_eq!(notification.slot(), 224341380);
|
||||||
|
assert_eq!(notification.transaction_index(), 42);
|
||||||
|
},
|
||||||
|
_ => panic!("transaction member must select the full Helius notification variant"),
|
||||||
|
}
|
||||||
|
let signature_value = serde_json::json!({
|
||||||
|
"signature":"signature-only",
|
||||||
|
"slot":224341381,
|
||||||
|
"transactionIndex":43,
|
||||||
|
"err":null,
|
||||||
|
"memo":"memo-canary",
|
||||||
|
"blockTime":1720000000,
|
||||||
|
"confirmationStatus":"confirmed"
|
||||||
|
});
|
||||||
|
let signature = super::decode_helius_transaction_notification(signature_value).expect("signature Helius notification must decode");
|
||||||
|
match signature {
|
||||||
|
crate::HeliusTransactionNotification::Signature(notification) => {
|
||||||
|
assert_eq!(notification.signature(), "signature-only");
|
||||||
|
assert_eq!(notification.slot(), 224341381);
|
||||||
|
assert_eq!(notification.transaction_index(), 43);
|
||||||
|
assert!(matches!(notification.err(), crate::SolanaWireField::Null));
|
||||||
|
assert!(matches!(notification.memo(), crate::SolanaWireField::Value(value) if value == "memo-canary"));
|
||||||
|
assert!(matches!(notification.block_time(), crate::SolanaWireField::Value(1720000000)));
|
||||||
|
assert!(matches!(notification.confirmation_status(), crate::SolanaWireField::Value(value) if value == "confirmed"));
|
||||||
|
},
|
||||||
|
_ => panic!("signature envelope must select the lightweight Helius notification variant"),
|
||||||
|
}
|
||||||
|
let unknown_value = serde_json::json!({"futureProviderShape":{"value":7}});
|
||||||
|
let unknown = super::decode_helius_transaction_notification(unknown_value.clone()).expect("unknown Helius notification must remain forward-compatible");
|
||||||
|
assert!(matches!(unknown, crate::HeliusTransactionNotification::Unknown(value) if value == unknown_value));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helius_transaction_filter_debug_omits_signature_and_account_values() {
|
||||||
|
let filter = base_filter();
|
||||||
|
let request = crate::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::None);
|
||||||
|
let debug = format!("{request:?}");
|
||||||
|
assert!(debug.contains("signature_present"));
|
||||||
|
assert!(debug.contains("account_include_count"));
|
||||||
|
assert!(!debug.contains("fixture-signature-secret-canary"));
|
||||||
|
assert!(!debug.contains("11111111111111111111111111111111"));
|
||||||
|
assert!(!debug.contains("SysvarC1ock11111111111111111111111111111111"));
|
||||||
|
assert!(!debug.contains("Vote111111111111111111111111111111111111111"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helius_transaction_notification_debug_omits_raw_provider_payloads() {
|
||||||
|
let full_value = serde_json::json!({
|
||||||
|
"transaction":{"raw":"MASSIVE-RAW-PAYLOAD-CANARY"},
|
||||||
|
"signature":"FULL-SIGNATURE-CANARY",
|
||||||
|
"slot":77,
|
||||||
|
"transactionIndex":3
|
||||||
|
});
|
||||||
|
let full = super::decode_helius_transaction_notification(full_value).expect("full Helius notification must decode");
|
||||||
|
let signature_value = serde_json::json!({
|
||||||
|
"signature":"SIGNATURE-MODE-CANARY",
|
||||||
|
"slot":78,
|
||||||
|
"transactionIndex":4,
|
||||||
|
"err":{"secret":"ERROR-DATA-CANARY"},
|
||||||
|
"memo":"MEMO-CANARY",
|
||||||
|
"blockTime":123,
|
||||||
|
"confirmationStatus":"CONFIRMATION-CANARY"
|
||||||
|
});
|
||||||
|
let signature = super::decode_helius_transaction_notification(signature_value).expect("signature Helius notification must decode");
|
||||||
|
let unknown = super::decode_helius_transaction_notification(serde_json::json!({"provider":"UNKNOWN-PAYLOAD-CANARY"}))
|
||||||
|
.expect("unknown Helius notification must remain forward-compatible");
|
||||||
|
let rendered = format!("{full:?} {signature:?} {unknown:?}");
|
||||||
|
assert!(rendered.contains("transaction: \"<omitted>\""));
|
||||||
|
assert!(rendered.contains("signature: \"<omitted>\""));
|
||||||
|
assert!(rendered.contains("err: \"value\""));
|
||||||
|
assert!(rendered.contains("memo: \"value\""));
|
||||||
|
assert!(rendered.contains("Unknown(\"<omitted>\")"));
|
||||||
|
for forbidden in [
|
||||||
|
"MASSIVE-RAW-PAYLOAD-CANARY",
|
||||||
|
"FULL-SIGNATURE-CANARY",
|
||||||
|
"SIGNATURE-MODE-CANARY",
|
||||||
|
"ERROR-DATA-CANARY",
|
||||||
|
"MEMO-CANARY",
|
||||||
|
"CONFIRMATION-CANARY",
|
||||||
|
"UNKNOWN-PAYLOAD-CANARY",
|
||||||
|
] {
|
||||||
|
assert!(!rendered.contains(forbidden));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn helius_provider_rpc_application_error_is_safe_and_does_not_fail_session() {
|
||||||
|
let (listener, url) = bind_local_listener().await;
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let (stream, _) = listener.accept().await.expect("local server must accept Helius client");
|
||||||
|
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local Helius handshake must succeed");
|
||||||
|
let transaction_subscribe = read_request(&mut websocket).await;
|
||||||
|
assert_eq!(transaction_subscribe["method"], serde_json::json!("transactionSubscribe"));
|
||||||
|
send_error(
|
||||||
|
&mut websocket,
|
||||||
|
&transaction_subscribe,
|
||||||
|
-32602,
|
||||||
|
"PROVIDER-MESSAGE-SECRET-CANARY",
|
||||||
|
serde_json::json!({"apiKey":"PROVIDER-ERROR-SECRET-CANARY","payload":"X".repeat(4096)}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let root_subscribe = read_request(&mut websocket).await;
|
||||||
|
assert_eq!(root_subscribe["method"], serde_json::json!("rootSubscribe"));
|
||||||
|
send_result(&mut websocket, &root_subscribe, serde_json::json!(72)).await;
|
||||||
|
send_root_notification(&mut websocket, 72, 88).await;
|
||||||
|
let root_unsubscribe = read_request(&mut websocket).await;
|
||||||
|
assert_eq!(root_unsubscribe["method"], serde_json::json!("rootUnsubscribe"));
|
||||||
|
assert_eq!(root_unsubscribe["params"], serde_json::json!([72]));
|
||||||
|
send_result(&mut websocket, &root_unsubscribe, serde_json::json!(true)).await;
|
||||||
|
wait_for_close_frame(&mut websocket).await;
|
||||||
|
});
|
||||||
|
let endpoint_url = format!("{url}/?api-key=HELIUS-ENDPOINT-SECRET-CANARY");
|
||||||
|
let session = crate::HeliusLaserStreamWsSession::connect(helius_endpoint(endpoint_url.as_str())).await.expect("Helius facade must connect");
|
||||||
|
let request = crate::HeliusTransactionSubscribeRequest::new(base_filter(), std::option::Option::None);
|
||||||
|
let error = session.transaction_subscribe(&request).await.expect_err("provider application error must reject only the logical subscribe request");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_RPC_APPLICATION_ERROR);
|
||||||
|
assert!(error.context().iter().any(|entry| return entry.key() == "rpc_code" && entry.value() == "-32602"));
|
||||||
|
assert!(error.context().iter().any(|entry| return entry.key() == "method" && entry.value() == "transactionSubscribe"));
|
||||||
|
let rendered = format!("{error:?} {error} {session:?} {:?}", session.snapshot());
|
||||||
|
for forbidden in ["PROVIDER-MESSAGE-SECRET-CANARY", "PROVIDER-ERROR-SECRET-CANARY", "HELIUS-ENDPOINT-SECRET-CANARY", "fixture-signature-secret-canary"] {
|
||||||
|
assert!(!rendered.contains(forbidden));
|
||||||
|
}
|
||||||
|
assert_eq!(session.state(), crate::WsSessionState::Active);
|
||||||
|
wait_for_session_subscription_count(&session, 0).await;
|
||||||
|
let mut root = session.root_subscribe().await.expect("session must accept a healthy subscription after provider application error");
|
||||||
|
assert_eq!(root.recv().await.expect("healthy root notification must arrive").expect("healthy root notification must decode"), 88);
|
||||||
|
assert!(root.unsubscribe().await.expect("healthy root unsubscribe must complete"));
|
||||||
|
session.close().await.expect("Helius fixture session must close");
|
||||||
|
server.await.expect("provider error fixture server must finish");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn helius_notification_method_mismatch_fails_only_transaction_subscription() {
|
||||||
|
let (listener, url) = bind_local_listener().await;
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let (stream, _) = listener.accept().await.expect("local server must accept Helius client");
|
||||||
|
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local Helius handshake must succeed");
|
||||||
|
let transaction_subscribe = read_request(&mut websocket).await;
|
||||||
|
send_result(&mut websocket, &transaction_subscribe, serde_json::json!(41)).await;
|
||||||
|
let root_subscribe = read_request(&mut websocket).await;
|
||||||
|
send_result(&mut websocket, &root_subscribe, serde_json::json!(42)).await;
|
||||||
|
send_root_notification(&mut websocket, 41, 5).await;
|
||||||
|
let cleanup = read_request(&mut websocket).await;
|
||||||
|
assert_eq!(cleanup["method"], serde_json::json!("transactionUnsubscribe"));
|
||||||
|
assert_eq!(cleanup["params"], serde_json::json!([41]));
|
||||||
|
send_result(&mut websocket, &cleanup, serde_json::json!(true)).await;
|
||||||
|
send_root_notification(&mut websocket, 42, 99).await;
|
||||||
|
wait_for_close_frame(&mut websocket).await;
|
||||||
|
});
|
||||||
|
let session = crate::HeliusLaserStreamWsSession::connect(helius_endpoint(url.as_str())).await.expect("Helius facade must connect");
|
||||||
|
let request = crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::None);
|
||||||
|
let mut transaction = session.transaction_subscribe(&request).await.expect("transaction subscription must register");
|
||||||
|
let mut root = session.root_subscribe().await.expect("root subscription must register");
|
||||||
|
wait_for_subscription_state(&transaction, crate::WsSubscriptionState::Failed).await;
|
||||||
|
assert_eq!(transaction.terminal_error_code(), std::option::Option::Some(crate::ERROR_CODE_WS_PROTOCOL_ERROR));
|
||||||
|
assert!(transaction.recv().await.is_none());
|
||||||
|
assert_eq!(session.state(), crate::WsSessionState::Active);
|
||||||
|
wait_for_session_subscription_count(&session, 1).await;
|
||||||
|
assert_eq!(root.recv().await.expect("healthy root notification must arrive").expect("healthy root notification must decode"), 99);
|
||||||
|
assert_eq!(root.state(), crate::WsSubscriptionState::Active);
|
||||||
|
session.close().await.expect("Helius fixture session must close");
|
||||||
|
server.await.expect("notification mismatch fixture server must finish");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn helius_oversized_inbound_payload_reconnects_before_provider_json_decode() {
|
||||||
|
let (listener, url) = bind_local_listener().await;
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let (first_stream, _) = listener.accept().await.expect("initial Helius client must connect");
|
||||||
|
let mut first = tokio_tungstenite::accept_async(first_stream).await.expect("initial Helius handshake must succeed");
|
||||||
|
first
|
||||||
|
.send(tokio_tungstenite::tungstenite::Message::Text("PROVIDER-PAYLOAD-CANARY".repeat(32).into()))
|
||||||
|
.await
|
||||||
|
.expect("oversized provider fixture payload must send");
|
||||||
|
let (replacement_stream, _) = listener.accept().await.expect("replacement Helius client must connect");
|
||||||
|
let mut replacement = tokio_tungstenite::accept_async(replacement_stream).await.expect("replacement Helius handshake must succeed");
|
||||||
|
let root_subscribe = read_request(&mut replacement).await;
|
||||||
|
assert_eq!(root_subscribe["method"], serde_json::json!("rootSubscribe"));
|
||||||
|
send_result(&mut replacement, &root_subscribe, serde_json::json!(91)).await;
|
||||||
|
let root_unsubscribe = read_request(&mut replacement).await;
|
||||||
|
assert_eq!(root_unsubscribe["method"], serde_json::json!("rootUnsubscribe"));
|
||||||
|
send_result(&mut replacement, &root_unsubscribe, serde_json::json!(true)).await;
|
||||||
|
wait_for_close_frame(&mut replacement).await;
|
||||||
|
});
|
||||||
|
let session = crate::HeliusLaserStreamWsSession::connect(helius_endpoint_with_session(url.as_str(), adversarial_payload_session_settings()))
|
||||||
|
.await
|
||||||
|
.expect("Helius facade must connect before adversarial payload");
|
||||||
|
wait_for_gap_count(&session, 1).await;
|
||||||
|
assert_eq!(session.state(), crate::WsSessionState::Active);
|
||||||
|
assert_eq!(session.snapshot().continuity_gap_count(), 1);
|
||||||
|
let mut root = session.root_subscribe().await.expect("recovered Helius session must remain usable");
|
||||||
|
assert!(root.unsubscribe().await.expect("recovered root subscription must unsubscribe"));
|
||||||
|
session.close().await.expect("recovered Helius session must close");
|
||||||
|
server.await.expect("oversized provider payload fixture server must finish");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn helius_transaction_live_handle_decodes_notification_and_unsubscribes_through_shared_actor() {
|
||||||
|
let (listener, url) = bind_local_listener().await;
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let (stream, _) = listener.accept().await.expect("local server must accept client");
|
||||||
|
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
|
||||||
|
let subscribe = read_request(&mut websocket).await;
|
||||||
|
assert_eq!(subscribe["method"], serde_json::json!("transactionSubscribe"));
|
||||||
|
assert_eq!(
|
||||||
|
subscribe["params"],
|
||||||
|
serde_json::json!([
|
||||||
|
{"failed":false,"accountInclude":["11111111111111111111111111111111"],"tokenAccounts":"balanceChanged"},
|
||||||
|
{"commitment":"confirmed","encoding":"jsonParsed","transactionDetails":"full","showRewards":false,"maxSupportedTransactionVersion":0}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
send_result(&mut websocket, &subscribe, serde_json::json!(4242)).await;
|
||||||
|
send_notification(
|
||||||
|
&mut websocket,
|
||||||
|
4242,
|
||||||
|
serde_json::json!({
|
||||||
|
"transaction":{"transaction":["AAAA","base64"],"meta":{"err":null}},
|
||||||
|
"signature":"live-signature",
|
||||||
|
"slot":99,
|
||||||
|
"transactionIndex":7
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let unsubscribe = read_request(&mut websocket).await;
|
||||||
|
assert_eq!(unsubscribe["method"], serde_json::json!("transactionUnsubscribe"));
|
||||||
|
assert_eq!(unsubscribe["params"], serde_json::json!([4242]));
|
||||||
|
send_result(&mut websocket, &unsubscribe, serde_json::json!(true)).await;
|
||||||
|
wait_for_close_frame(&mut websocket).await;
|
||||||
|
});
|
||||||
|
let session = crate::HeliusLaserStreamWsSession::connect(helius_endpoint(url.as_str())).await.expect("Helius facade must connect");
|
||||||
|
let filter = crate::HeliusTransactionSubscribeFilter::new(
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(std::vec![pubkey("11111111111111111111111111111111")]),
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::None,
|
||||||
|
std::option::Option::Some(crate::HeliusTokenAccountsFilter::BalanceChanged),
|
||||||
|
);
|
||||||
|
let options = crate::HeliusTransactionSubscribeOptions::new(
|
||||||
|
std::option::Option::Some(crate::SolanaCommitment::Confirmed),
|
||||||
|
std::option::Option::Some(crate::HeliusTransactionSubscribeEncoding::JsonParsed),
|
||||||
|
std::option::Option::Some(crate::SolanaTransactionDetails::Full),
|
||||||
|
std::option::Option::Some(false),
|
||||||
|
std::option::Option::Some(0),
|
||||||
|
);
|
||||||
|
let request = crate::HeliusTransactionSubscribeRequest::new(filter, std::option::Option::Some(options));
|
||||||
|
let mut subscription = session.transaction_subscribe(&request).await.expect("public Helius transaction subscription must register");
|
||||||
|
assert_eq!(subscription.kind(), crate::WsSubscriptionKind::HeliusTransaction);
|
||||||
|
let notification = subscription.recv().await.expect("Helius transaction notification must arrive").expect("Helius notification must decode");
|
||||||
|
match notification {
|
||||||
|
crate::HeliusTransactionNotification::Full(notification) => {
|
||||||
|
assert_eq!(notification.signature(), "live-signature");
|
||||||
|
assert_eq!(notification.slot(), 99);
|
||||||
|
assert_eq!(notification.transaction_index(), 7);
|
||||||
|
},
|
||||||
|
_ => panic!("full live payload must decode as HeliusTransactionNotification::Full"),
|
||||||
|
}
|
||||||
|
assert!(subscription.unsubscribe().await.expect("transactionUnsubscribe must complete"));
|
||||||
|
assert_eq!(subscription.state(), crate::WsSubscriptionState::Closed);
|
||||||
|
session.close().await.expect("Helius fixture session must close");
|
||||||
|
server.await.expect("local Helius transaction server must finish");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn helius_transaction_reconnect_remaps_remote_id_and_ignores_late_notification_after_unsubscribe() {
|
||||||
|
let (listener, url) = bind_local_listener().await;
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let (first_stream, _) = listener.accept().await.expect("initial Helius client must connect");
|
||||||
|
let mut first = tokio_tungstenite::accept_async(first_stream).await.expect("initial Helius handshake must succeed");
|
||||||
|
let first_subscribe = read_request(&mut first).await;
|
||||||
|
assert_eq!(first_subscribe["method"], serde_json::json!("transactionSubscribe"));
|
||||||
|
send_result(&mut first, &first_subscribe, serde_json::json!(41)).await;
|
||||||
|
send_notification(&mut first, 41, serde_json::json!({"signature":"generation-one","slot":1,"transactionIndex":0})).await;
|
||||||
|
drop(first);
|
||||||
|
let (second_stream, _) = listener.accept().await.expect("replacement Helius client must connect");
|
||||||
|
let mut second = tokio_tungstenite::accept_async(second_stream).await.expect("replacement Helius handshake must succeed");
|
||||||
|
let second_subscribe = read_request(&mut second).await;
|
||||||
|
assert_eq!(second_subscribe["method"], serde_json::json!("transactionSubscribe"));
|
||||||
|
assert_eq!(second_subscribe["params"], first_subscribe["params"]);
|
||||||
|
send_result(&mut second, &second_subscribe, serde_json::json!(99)).await;
|
||||||
|
send_notification(&mut second, 99, serde_json::json!({"signature":"generation-two","slot":2,"transactionIndex":1})).await;
|
||||||
|
let unsubscribe = read_request(&mut second).await;
|
||||||
|
assert_eq!(unsubscribe["method"], serde_json::json!("transactionUnsubscribe"));
|
||||||
|
assert_eq!(unsubscribe["params"], serde_json::json!([99]));
|
||||||
|
send_notification(&mut second, 99, serde_json::json!({"signature":"late-after-cancel","slot":3,"transactionIndex":2})).await;
|
||||||
|
send_result(&mut second, &unsubscribe, serde_json::json!(true)).await;
|
||||||
|
wait_for_close_frame(&mut second).await;
|
||||||
|
});
|
||||||
|
let settings = reconnect_session_settings(std::time::Duration::from_millis(20));
|
||||||
|
let session = crate::HeliusLaserStreamWsSession::connect(helius_endpoint_with_session(url.as_str(), settings)).await.expect("Helius facade must connect");
|
||||||
|
let request = crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::None);
|
||||||
|
let mut subscription = session.transaction_subscribe(&request).await.expect("initial Helius transaction subscription must register");
|
||||||
|
let stable_id = subscription.id();
|
||||||
|
let first = subscription.recv().await.expect("first generation notification must arrive").expect("first generation notification must decode");
|
||||||
|
assert!(matches!(first, crate::HeliusTransactionNotification::Signature(ref value) if value.signature() == "generation-one"));
|
||||||
|
let second = tokio::time::timeout(std::time::Duration::from_secs(2), subscription.recv())
|
||||||
|
.await
|
||||||
|
.expect("resubscribed Helius notification must remain bounded")
|
||||||
|
.expect("resubscribed Helius channel must remain open")
|
||||||
|
.expect("resubscribed Helius notification must decode");
|
||||||
|
assert!(matches!(second, crate::HeliusTransactionNotification::Signature(ref value) if value.signature() == "generation-two"));
|
||||||
|
assert_eq!(subscription.id(), stable_id);
|
||||||
|
assert_eq!(subscription.state(), crate::WsSubscriptionState::Active);
|
||||||
|
wait_for_gap_count(&session, 1).await;
|
||||||
|
assert_eq!(session.snapshot().continuity_gap_count(), 1);
|
||||||
|
assert!(subscription.unsubscribe().await.expect("Helius transaction cancellation must complete"));
|
||||||
|
assert_eq!(subscription.state(), crate::WsSubscriptionState::Closed);
|
||||||
|
assert!(tokio::time::timeout(std::time::Duration::from_millis(100), subscription.recv()).await.expect("closed Helius channel must settle").is_none());
|
||||||
|
session.close().await.expect("Helius fixture session must close");
|
||||||
|
server.await.expect("local reconnect Helius server must finish");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn helius_transaction_backpressure_fails_only_slow_subscription_and_uses_transaction_unsubscribe_cleanup() {
|
||||||
|
let (listener, url) = bind_local_listener().await;
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let (stream, _) = listener.accept().await.expect("local server must accept Helius client");
|
||||||
|
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local Helius handshake must succeed");
|
||||||
|
let transaction_subscribe = read_request(&mut websocket).await;
|
||||||
|
assert_eq!(transaction_subscribe["method"], serde_json::json!("transactionSubscribe"));
|
||||||
|
send_result(&mut websocket, &transaction_subscribe, serde_json::json!(41)).await;
|
||||||
|
let root_subscribe = read_request(&mut websocket).await;
|
||||||
|
assert_eq!(root_subscribe["method"], serde_json::json!("rootSubscribe"));
|
||||||
|
send_result(&mut websocket, &root_subscribe, serde_json::json!(42)).await;
|
||||||
|
send_notification(&mut websocket, 41, serde_json::json!({"signature":"queued","slot":1,"transactionIndex":0})).await;
|
||||||
|
send_notification(&mut websocket, 41, serde_json::json!({"signature":"overflow","slot":2,"transactionIndex":1})).await;
|
||||||
|
let cleanup = read_request(&mut websocket).await;
|
||||||
|
assert_eq!(cleanup["method"], serde_json::json!("transactionUnsubscribe"));
|
||||||
|
assert_eq!(cleanup["params"], serde_json::json!([41]));
|
||||||
|
send_result(&mut websocket, &cleanup, serde_json::json!(true)).await;
|
||||||
|
send_root_notification(&mut websocket, 42, 99).await;
|
||||||
|
wait_for_close_frame(&mut websocket).await;
|
||||||
|
});
|
||||||
|
let session = crate::HeliusLaserStreamWsSession::connect(helius_endpoint_with_session(url.as_str(), backpressure_session_settings()))
|
||||||
|
.await
|
||||||
|
.expect("Helius facade must connect");
|
||||||
|
let request = crate::HeliusTransactionSubscribeRequest::new(crate::HeliusTransactionSubscribeFilter::default(), std::option::Option::None);
|
||||||
|
let mut slow = session.transaction_subscribe(&request).await.expect("slow Helius transaction subscription must register");
|
||||||
|
let mut healthy = session.root_subscribe().await.expect("healthy Helius root subscription must register");
|
||||||
|
wait_for_subscription_state(&slow, crate::WsSubscriptionState::Failed).await;
|
||||||
|
wait_for_overflow_count(&session, 1).await;
|
||||||
|
assert_eq!(slow.terminal_error_code(), std::option::Option::Some(crate::ERROR_CODE_WS_BACKPRESSURE_OVERFLOW));
|
||||||
|
assert_eq!(session.state(), crate::WsSessionState::Active);
|
||||||
|
assert_eq!(session.snapshot().subscription_count(), 1);
|
||||||
|
let queued = slow.recv().await.expect("first Helius notification must remain queued").expect("queued Helius notification must decode");
|
||||||
|
assert!(matches!(queued, crate::HeliusTransactionNotification::Signature(ref value) if value.signature() == "queued"));
|
||||||
|
assert!(slow.recv().await.is_none());
|
||||||
|
assert_eq!(healthy.recv().await.expect("healthy root notification must arrive").expect("healthy root notification must decode"), 99);
|
||||||
|
assert_eq!(healthy.state(), crate::WsSubscriptionState::Active);
|
||||||
|
assert_eq!(healthy.terminal_error_code(), std::option::Option::None);
|
||||||
|
session.close().await.expect("Helius fixture session must close");
|
||||||
|
server.await.expect("local Helius backpressure server must finish");
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_lifecycle.rs
|
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_lifecycle.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
fn non_zero(value: u64) -> std::num::NonZeroU64 {
|
fn non_zero(value: u64) -> std::num::NonZeroU64 {
|
||||||
return std::num::NonZeroU64::new(value).expect("test ID must be non-zero");
|
return std::num::NonZeroU64::new(value).expect("test ID must be non-zero");
|
||||||
@@ -123,3 +123,13 @@ fn websocket_unstable_subscription_partition_is_exact() {
|
|||||||
assert_eq!(kind.is_unstable(), unstable);
|
assert_eq!(kind.is_unstable(), unstable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helius_transaction_subscription_kind_maps_exact_provider_method_triplet_without_expanding_standard_partition() {
|
||||||
|
let kind = crate::WsSubscriptionKind::HeliusTransaction;
|
||||||
|
assert_eq!(kind.as_str(), "helius_transaction");
|
||||||
|
assert_eq!(kind.subscribe_method(), "transactionSubscribe");
|
||||||
|
assert_eq!(kind.unsubscribe_method(), "transactionUnsubscribe");
|
||||||
|
assert_eq!(kind.notification_method(), "transactionNotification");
|
||||||
|
assert!(!kind.is_unstable());
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_protocol_session.rs
|
||||||
|
// version: 3
|
||||||
|
|
||||||
|
use futures_util::StreamExt; // rust-rules: trait-import
|
||||||
|
|
||||||
|
fn endpoint(url: &str, protocol: crate::WsProtocolKind) -> crate::WsEndpointSettings {
|
||||||
|
return crate::WsEndpointSettings::new(
|
||||||
|
"local_protocol_fixture",
|
||||||
|
true,
|
||||||
|
crate::WsProviderName::new("local-fixture"),
|
||||||
|
crate::WsClusterName::new("local"),
|
||||||
|
protocol,
|
||||||
|
crate::WsEndpointUrl::parse(url).expect("local WebSocket URL must parse"),
|
||||||
|
crate::WsSessionSettings::default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn bind_local_listener() -> (tokio::net::TcpListener, std::string::String) {
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("local listener must bind");
|
||||||
|
let address = listener.local_addr().expect("local listener must expose address");
|
||||||
|
return (listener, format!("ws://{address}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn accept_until_close(listener: tokio::net::TcpListener) {
|
||||||
|
let (stream, _) = listener.accept().await.expect("local peer must accept connection");
|
||||||
|
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
|
||||||
|
while let std::option::Option::Some(message) = websocket.next().await {
|
||||||
|
let message = message.expect("local peer message must decode");
|
||||||
|
if message.is_close() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn protocol_facades_share_the_existing_physical_session_path() {
|
||||||
|
let (standard_listener, standard_url) = bind_local_listener().await;
|
||||||
|
let standard_server = tokio::spawn(accept_until_close(standard_listener));
|
||||||
|
let standard = crate::SolanaStandardWsSession::connect(endpoint(standard_url.as_str(), crate::WsProtocolKind::SolanaStandard))
|
||||||
|
.await
|
||||||
|
.expect("standard facade must connect");
|
||||||
|
assert_eq!(standard.snapshot().protocol(), crate::WsProtocolKind::SolanaStandard);
|
||||||
|
standard.close().await.expect("standard facade must close");
|
||||||
|
standard_server.await.expect("standard peer task must finish");
|
||||||
|
let (helius_listener, helius_url) = bind_local_listener().await;
|
||||||
|
let helius_server = tokio::spawn(accept_until_close(helius_listener));
|
||||||
|
let helius_url = format!("{helius_url}/?api-key=SECRET-CANARY");
|
||||||
|
let helius = crate::HeliusLaserStreamWsSession::connect(endpoint(helius_url.as_str(), crate::WsProtocolKind::HeliusLaserStream))
|
||||||
|
.await
|
||||||
|
.expect("Helius facade must connect");
|
||||||
|
assert_eq!(helius.snapshot().protocol(), crate::WsProtocolKind::HeliusLaserStream);
|
||||||
|
let rendered = format!("{helius:?}");
|
||||||
|
assert!(!rendered.contains("SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains(helius_url.as_str()));
|
||||||
|
helius.close().await.expect("Helius facade must close");
|
||||||
|
helius_server.await.expect("Helius peer task must finish");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn historical_generic_constructor_remains_standard_only_before_network_io() {
|
||||||
|
let endpoint = endpoint("ws://127.0.0.1:9", crate::WsProtocolKind::HeliusLaserStream);
|
||||||
|
let error = crate::WsSession::connect(endpoint).await.expect_err("generic historical constructor must reject Helius protocol");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||||
|
assert_eq!(
|
||||||
|
error.context().iter().find(|entry| return entry.key() == "expected_protocol").map(|entry| return entry.value()),
|
||||||
|
std::option::Option::Some("solana_standard")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
error.context().iter().find(|entry| return entry.key() == "actual_protocol").map(|entry| return entry.value()),
|
||||||
|
std::option::Option::Some("helius_laserstream")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn typed_facades_reject_protocol_mismatch_before_network_io() {
|
||||||
|
let helius_error = crate::HeliusLaserStreamWsSession::connect(endpoint("ws://127.0.0.1:9", crate::WsProtocolKind::SolanaStandard))
|
||||||
|
.await
|
||||||
|
.expect_err("Helius facade must reject standard endpoint");
|
||||||
|
assert_eq!(helius_error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||||
|
let standard_error = crate::SolanaStandardWsSession::connect(endpoint("ws://127.0.0.1:9", crate::WsProtocolKind::HeliusLaserStream))
|
||||||
|
.await
|
||||||
|
.expect_err("standard facade must reject Helius endpoint");
|
||||||
|
assert_eq!(standard_error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn protocol_facades_define_no_second_actor_socket_or_public_inner_escape_hatch() {
|
||||||
|
let source = include_str!("../src/ws_protocol_session.rs");
|
||||||
|
assert!(!source.contains("tokio::spawn"));
|
||||||
|
assert!(!source.contains("tokio_tungstenite"));
|
||||||
|
assert!(!source.contains("WsSessionCommand"));
|
||||||
|
assert!(!source.contains("pub fn inner("));
|
||||||
|
assert!(!source.contains("pub fn into_inner("));
|
||||||
|
assert!(!source.contains("pub async fn account_subscribe"));
|
||||||
|
assert!(!source.contains("pub async fn block_subscribe"));
|
||||||
|
assert!(!source.contains("pub async fn logs_subscribe"));
|
||||||
|
assert!(!source.contains("pub async fn program_subscribe"));
|
||||||
|
assert!(!source.contains("pub async fn root_subscribe"));
|
||||||
|
assert!(!source.contains("pub async fn signature_subscribe"));
|
||||||
|
assert!(!source.contains("pub async fn slot_subscribe"));
|
||||||
|
assert!(!source.contains("pub async fn slots_updates_subscribe"));
|
||||||
|
assert!(!source.contains("pub async fn vote_subscribe"));
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
|
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
|
||||||
// version: 7
|
// version: 11
|
||||||
|
|
||||||
use futures_util::SinkExt; // rust-rules: trait-import
|
use futures_util::SinkExt; // rust-rules: trait-import
|
||||||
use futures_util::StreamExt; // rust-rules: trait-import
|
use futures_util::StreamExt; // rust-rules: trait-import
|
||||||
@@ -20,6 +20,22 @@ fn local_endpoint_with_session(url: &str, session: crate::WsSessionSettings) ->
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn helius_local_endpoint(url: &str) -> crate::WsEndpointSettings {
|
||||||
|
return helius_local_endpoint_with_session(url, crate::WsSessionSettings::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn helius_local_endpoint_with_session(url: &str, session: crate::WsSessionSettings) -> crate::WsEndpointSettings {
|
||||||
|
return crate::WsEndpointSettings::new(
|
||||||
|
"local_helius_ws",
|
||||||
|
true,
|
||||||
|
crate::WsProviderName::new("helius-fixture"),
|
||||||
|
crate::WsClusterName::new("local"),
|
||||||
|
crate::WsProtocolKind::HeliusLaserStream,
|
||||||
|
crate::WsEndpointUrl::parse(url).expect("local Helius test WebSocket URL must parse"),
|
||||||
|
session,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn session_settings(
|
fn session_settings(
|
||||||
command_timeout: std::time::Duration,
|
command_timeout: std::time::Duration,
|
||||||
close_timeout: std::time::Duration,
|
close_timeout: std::time::Duration,
|
||||||
@@ -166,6 +182,26 @@ async fn wait_for_close_frame(websocket: &mut tokio_tungstenite::WebSocketStream
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn yield_runtime_steps() {
|
||||||
|
for _ in 0..16 {
|
||||||
|
tokio::task::yield_now().await;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_observed_ping_with_io_progress(ping_rx: &mut tokio::sync::mpsc::UnboundedReceiver<()>) -> std::time::Duration {
|
||||||
|
let started = tokio::time::Instant::now();
|
||||||
|
tokio::time::resume();
|
||||||
|
let observed = tokio::time::timeout(std::time::Duration::from_secs(1), ping_rx.recv()).await;
|
||||||
|
tokio::time::pause();
|
||||||
|
let elapsed = tokio::time::Instant::now().duration_since(started);
|
||||||
|
assert!(
|
||||||
|
matches!(observed, std::result::Result::Ok(std::option::Option::Some(()))),
|
||||||
|
"heartbeat Ping was not observed during bounded real I/O progress"
|
||||||
|
);
|
||||||
|
return elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn websocket_session_connects_and_round_trips_internal_json_rpc() {
|
async fn websocket_session_connects_and_round_trips_internal_json_rpc() {
|
||||||
let (listener, url) = bind_local_listener().await;
|
let (listener, url) = bind_local_listener().await;
|
||||||
@@ -400,6 +436,191 @@ async fn websocket_ping_flushes_automatic_pong_and_keeps_session_active() {
|
|||||||
server.await.expect("local server task must complete");
|
server.await.expect("local server task must complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn helius_heartbeat_policy_is_provider_owned_and_fixed_to_sixty_seconds() {
|
||||||
|
assert!(super::helius_heartbeat_enabled(crate::WsProtocolKind::HeliusLaserStream));
|
||||||
|
assert!(!super::helius_heartbeat_enabled(crate::WsProtocolKind::SolanaStandard));
|
||||||
|
assert_eq!(super::HELIUS_WS_HEARTBEAT_INTERVAL, std::time::Duration::from_secs(60));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn helius_heartbeat_sends_ping_at_sixty_seconds_and_rearms() {
|
||||||
|
let (listener, url) = bind_local_listener().await;
|
||||||
|
let (ping_tx, mut ping_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let (stream, _) = listener.accept().await.expect("local Helius server must accept client");
|
||||||
|
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local Helius handshake must succeed");
|
||||||
|
loop {
|
||||||
|
let message = websocket.next().await;
|
||||||
|
match message {
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Ping(_))) => {
|
||||||
|
ping_tx.send(()).expect("heartbeat observation channel must remain open");
|
||||||
|
websocket.flush().await.expect("automatic heartbeat Pong must flush");
|
||||||
|
},
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => return,
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(_)) => {},
|
||||||
|
std::option::Option::Some(std::result::Result::Err(_)) | std::option::Option::None => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let session = crate::HeliusLaserStreamWsSession::connect(helius_local_endpoint(url.as_str())).await.expect("Helius fixture session must connect");
|
||||||
|
tokio::time::pause();
|
||||||
|
yield_runtime_steps().await;
|
||||||
|
tokio::time::advance(std::time::Duration::from_secs(59)).await;
|
||||||
|
yield_runtime_steps().await;
|
||||||
|
assert!(matches!(ping_rx.try_recv(), std::result::Result::Err(tokio::sync::mpsc::error::TryRecvError::Empty)));
|
||||||
|
tokio::time::advance(std::time::Duration::from_secs(1)).await;
|
||||||
|
let first_io_elapsed = wait_for_observed_ping_with_io_progress(&mut ping_rx).await;
|
||||||
|
let pre_second_interval =
|
||||||
|
std::time::Duration::from_secs(59).checked_sub(first_io_elapsed).expect("bounded first Ping I/O progress must remain below one second");
|
||||||
|
tokio::time::advance(pre_second_interval).await;
|
||||||
|
yield_runtime_steps().await;
|
||||||
|
assert!(matches!(ping_rx.try_recv(), std::result::Result::Err(tokio::sync::mpsc::error::TryRecvError::Empty)));
|
||||||
|
tokio::time::advance(std::time::Duration::from_secs(1) + first_io_elapsed).await;
|
||||||
|
let _second_io_elapsed = wait_for_observed_ping_with_io_progress(&mut ping_rx).await;
|
||||||
|
session.close().await.expect("Helius heartbeat fixture session must close");
|
||||||
|
tokio::time::resume();
|
||||||
|
server.await.expect("Helius heartbeat fixture server must complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn standard_session_never_emits_helius_provider_heartbeat() {
|
||||||
|
let (listener, url) = bind_local_listener().await;
|
||||||
|
let (ping_tx, mut ping_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let (stream, _) = listener.accept().await.expect("local standard server must accept client");
|
||||||
|
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local standard handshake must succeed");
|
||||||
|
loop {
|
||||||
|
let message = websocket.next().await;
|
||||||
|
match message {
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Ping(_))) => {
|
||||||
|
ping_tx.send(()).expect("standard heartbeat observation channel must remain open");
|
||||||
|
websocket.flush().await.expect("automatic Pong must flush");
|
||||||
|
},
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => return,
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(_)) => {},
|
||||||
|
std::option::Option::Some(std::result::Result::Err(_)) | std::option::Option::None => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("standard fixture session must connect");
|
||||||
|
tokio::time::pause();
|
||||||
|
tokio::time::advance(std::time::Duration::from_secs(180)).await;
|
||||||
|
yield_runtime_steps().await;
|
||||||
|
assert!(matches!(ping_rx.try_recv(), std::result::Result::Err(tokio::sync::mpsc::error::TryRecvError::Empty)));
|
||||||
|
session.close().await.expect("standard fixture session must close");
|
||||||
|
tokio::time::resume();
|
||||||
|
server.await.expect("standard heartbeat absence fixture server must complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn helius_explicit_close_cancels_heartbeat_before_deadline() {
|
||||||
|
let (listener, url) = bind_local_listener().await;
|
||||||
|
let (ping_tx, mut ping_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let (stream, _) = listener.accept().await.expect("local Helius close server must accept client");
|
||||||
|
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local Helius close handshake must succeed");
|
||||||
|
loop {
|
||||||
|
let message = websocket.next().await;
|
||||||
|
match message {
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Ping(_))) => {
|
||||||
|
ping_tx.send(()).expect("close heartbeat observation channel must remain open");
|
||||||
|
websocket.flush().await.expect("automatic heartbeat Pong must flush");
|
||||||
|
},
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => return,
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(_)) => {},
|
||||||
|
std::option::Option::Some(std::result::Result::Err(_)) | std::option::Option::None => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let session = crate::HeliusLaserStreamWsSession::connect(helius_local_endpoint(url.as_str())).await.expect("Helius close fixture session must connect");
|
||||||
|
tokio::time::pause();
|
||||||
|
yield_runtime_steps().await;
|
||||||
|
tokio::time::advance(std::time::Duration::from_secs(30)).await;
|
||||||
|
yield_runtime_steps().await;
|
||||||
|
assert!(matches!(ping_rx.try_recv(), std::result::Result::Err(tokio::sync::mpsc::error::TryRecvError::Empty)));
|
||||||
|
session.close().await.expect("Helius close fixture session must close before heartbeat deadline");
|
||||||
|
yield_runtime_steps().await;
|
||||||
|
assert!(!matches!(ping_rx.try_recv(), std::result::Result::Ok(())));
|
||||||
|
tokio::time::resume();
|
||||||
|
server.await.expect("Helius close fixture server must complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn helius_heartbeat_write_failure_maps_to_existing_reconnect_failure_outcome() {
|
||||||
|
let (client_io, peer_io) = tokio::io::duplex(64);
|
||||||
|
let mut websocket =
|
||||||
|
tokio_tungstenite::WebSocketStream::from_raw_socket(client_io, tokio_tungstenite::tungstenite::protocol::Role::Client, std::option::Option::None).await;
|
||||||
|
drop(peer_io);
|
||||||
|
let endpoint = helius_local_endpoint("ws://127.0.0.1:65535");
|
||||||
|
let (_shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(std::option::Option::None::<tokio::time::Instant>);
|
||||||
|
let session_id = crate::WsSessionId::new(std::num::NonZeroU64::new(1).expect("fixture session id must be non-zero"));
|
||||||
|
let outcome = super::send_helius_heartbeat(session_id, &endpoint, &mut websocket, &mut shutdown_rx).await;
|
||||||
|
match outcome {
|
||||||
|
super::WsActorIoOutcome::Failed { code, pending_message } => {
|
||||||
|
assert_eq!(code, crate::ERROR_CODE_WS_CONNECTION_FAILED);
|
||||||
|
assert_eq!(pending_message, "WebSocket connection failed while writing Helius heartbeat Ping");
|
||||||
|
},
|
||||||
|
_ => panic!("heartbeat write failure must use the existing reconnect failure outcome"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn helius_heartbeat_is_rearmed_from_successful_reconnect() {
|
||||||
|
let (listener, url) = bind_local_listener().await;
|
||||||
|
let (disconnect_tx, disconnect_rx) = tokio::sync::oneshot::channel::<()>();
|
||||||
|
let (replacement_tx, replacement_rx) = tokio::sync::oneshot::channel::<()>();
|
||||||
|
let (ping_tx, mut ping_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
let (first_stream, _) = listener.accept().await.expect("first Helius connection must be accepted");
|
||||||
|
let mut first = tokio_tungstenite::accept_async(first_stream).await.expect("first Helius handshake must succeed");
|
||||||
|
disconnect_rx.await.expect("disconnect trigger must arrive");
|
||||||
|
first.send(tokio_tungstenite::tungstenite::Message::Close(std::option::Option::None)).await.expect("first Helius close must send");
|
||||||
|
let (replacement_stream, _) = listener.accept().await.expect("replacement Helius connection must be accepted");
|
||||||
|
let mut replacement = tokio_tungstenite::accept_async(replacement_stream).await.expect("replacement Helius handshake must succeed");
|
||||||
|
replacement_tx.send(()).expect("replacement observation must be delivered");
|
||||||
|
loop {
|
||||||
|
let message = replacement.next().await;
|
||||||
|
match message {
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Ping(_))) => {
|
||||||
|
ping_tx.send(()).expect("replacement heartbeat observation channel must remain open");
|
||||||
|
replacement.flush().await.expect("replacement automatic Pong must flush");
|
||||||
|
},
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => return,
|
||||||
|
std::option::Option::Some(std::result::Result::Ok(_)) => {},
|
||||||
|
std::option::Option::Some(std::result::Result::Err(_)) | std::option::Option::None => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let settings = reconnect_session_settings(1, std::time::Duration::from_millis(20), crate::WsResubscribePolicy::ActiveSubscriptions);
|
||||||
|
let session = crate::HeliusLaserStreamWsSession::connect(helius_local_endpoint_with_session(url.as_str(), settings))
|
||||||
|
.await
|
||||||
|
.expect("Helius reconnect fixture session must connect");
|
||||||
|
disconnect_tx.send(()).expect("disconnect trigger must send");
|
||||||
|
tokio::time::timeout(std::time::Duration::from_secs(1), replacement_rx)
|
||||||
|
.await
|
||||||
|
.expect("replacement connection must remain bounded")
|
||||||
|
.expect("replacement connection must be observed");
|
||||||
|
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(1);
|
||||||
|
loop {
|
||||||
|
if session.state() == crate::WsSessionState::Active && session.snapshot().continuity_gap_count() == 1 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
assert!(tokio::time::Instant::now() < deadline, "Helius fixture must recover before heartbeat rearm check");
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
tokio::time::pause();
|
||||||
|
tokio::time::advance(std::time::Duration::from_secs(59)).await;
|
||||||
|
yield_runtime_steps().await;
|
||||||
|
assert!(matches!(ping_rx.try_recv(), std::result::Result::Err(tokio::sync::mpsc::error::TryRecvError::Empty)));
|
||||||
|
tokio::time::advance(std::time::Duration::from_secs(1)).await;
|
||||||
|
yield_runtime_steps().await;
|
||||||
|
assert!(matches!(ping_rx.try_recv(), std::result::Result::Ok(())));
|
||||||
|
session.close().await.expect("reconnected Helius fixture session must close");
|
||||||
|
tokio::time::resume();
|
||||||
|
server.await.expect("Helius reconnect heartbeat fixture server must complete");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn websocket_remote_close_consumes_bounded_reconnect_budget_before_terminal_failure() {
|
async fn websocket_remote_close_consumes_bounded_reconnect_budget_before_terminal_failure() {
|
||||||
let (listener, url) = bind_local_listener().await;
|
let (listener, url) = bind_local_listener().await;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs
|
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
fn valid_endpoint(name: &str, url_text: &str) -> crate::WsEndpointSettings {
|
fn valid_endpoint(name: &str, url_text: &str) -> crate::WsEndpointSettings {
|
||||||
return crate::WsEndpointSettings::new(
|
return crate::WsEndpointSettings::new(
|
||||||
@@ -47,8 +47,10 @@ fn websocket_endpoint_url_errors_do_not_echo_sensitive_url() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn websocket_protocol_kind_is_extensible_but_only_standard_is_available_now() {
|
fn websocket_protocol_kind_distinguishes_standard_and_helius_laserstream_websocket() {
|
||||||
assert_eq!(crate::WsProtocolKind::SolanaStandard.as_str(), "solana_standard");
|
assert_eq!(crate::WsProtocolKind::SolanaStandard.as_str(), "solana_standard");
|
||||||
|
assert_eq!(crate::WsProtocolKind::HeliusLaserStream.as_str(), "helius_laserstream");
|
||||||
|
assert_ne!(crate::WsProtocolKind::SolanaStandard, crate::WsProtocolKind::HeliusLaserStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
327
deltas/0.2.10/pre.001.md
Normal file
327
deltas/0.2.10/pre.001.md
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
<!-- file: deltas/0.2.10/pre.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.10-pre.001` — audit/sizing OrbitFlare Yellowstone gRPC
|
||||||
|
|
||||||
|
## 1. Base requise et vérifiée
|
||||||
|
|
||||||
|
Archive autoritaire fournie :
|
||||||
|
|
||||||
|
```text
|
||||||
|
khadhroony-solana-project-v0.2.9-full-from-gitea.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
État vérifié :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.9
|
||||||
|
deltas/0.2.9/rel.001.md présent
|
||||||
|
prompts/015-V0_2_10_START_PROMPT.md présent
|
||||||
|
metadata .git absente de l'archive Gitea
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette livraison ouvre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.10-pre.1
|
||||||
|
commit attendu = v0.2.10-pre.001
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Objet
|
||||||
|
|
||||||
|
`pre.001` exécute le gate documentaire obligatoire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
lecture base/règles/architecture
|
||||||
|
baseline stable
|
||||||
|
réaudit OrbitFlare actuel
|
||||||
|
réaudit Yellowstone actuel
|
||||||
|
auth control-plane/data-plane
|
||||||
|
endpoints/network/security
|
||||||
|
Free Devnet gRPC
|
||||||
|
heartbeat
|
||||||
|
capabilities/unary/replay
|
||||||
|
limits/quotas
|
||||||
|
architecture N1/N2 immuable
|
||||||
|
threat model
|
||||||
|
sizing et forecast
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun code provider lourd, SDK OrbitFlare, metadata secrète ou heartbeat supplémentaire n'est ajouté.
|
||||||
|
|
||||||
|
## 3. Baseline opérateur acquise
|
||||||
|
|
||||||
|
Preuves fournies le 2026-08-25 sur `v0.2.9` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py OK / clean
|
||||||
|
python3 scripts/audit_markdown_tables.py OK / 87 tables / 261 files
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test --workspace OK
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates fourni
|
||||||
|
```
|
||||||
|
|
||||||
|
Transport pendant le workspace test :
|
||||||
|
|
||||||
|
```text
|
||||||
|
383 unit
|
||||||
|
49 public_api
|
||||||
|
43 release_completeness
|
||||||
|
4 doctests
|
||||||
|
```
|
||||||
|
|
||||||
|
Les live smokes restent opt-in/ignored par défaut.
|
||||||
|
|
||||||
|
Le `cargo tree -p ksp-onchain-transport-lib` complet n'a pas encore été fourni et doit être rejoué avant commit.
|
||||||
|
|
||||||
|
## 4. Graphe de dépendances inspecté
|
||||||
|
|
||||||
|
Versions utiles visibles :
|
||||||
|
|
||||||
|
```text
|
||||||
|
yellowstone-grpc-proto 12.6.0
|
||||||
|
tonic 0.14.6
|
||||||
|
tonic-prost 0.14.6
|
||||||
|
prost 0.14.4
|
||||||
|
tokio 1.53.1
|
||||||
|
http 1.5.0
|
||||||
|
reqwest 0.13.4
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune divergence Tonic/Prost/Yellowstone ne justifie une intervention. Les doublons restants sont sans impact matériel pour OrbitFlare.
|
||||||
|
|
||||||
|
## 5. Réaudit OrbitFlare du 2026-08-25
|
||||||
|
|
||||||
|
### Free Devnet
|
||||||
|
|
||||||
|
Le pricing actuel confirme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Free 0 USD/mois
|
||||||
|
RPC 10 RPS
|
||||||
|
transactions 1 TPS
|
||||||
|
gRPC Devnet only
|
||||||
|
credit limits unlimited
|
||||||
|
```
|
||||||
|
|
||||||
|
Le CLI documente :
|
||||||
|
|
||||||
|
```text
|
||||||
|
RPC Devnet = http://devnet.rpc.orbitflare.com
|
||||||
|
gRPC Devnet = http://devnet.rpc.orbitflare.com:10000
|
||||||
|
```
|
||||||
|
|
||||||
|
Objectif opérationnel fermé : utiliser OrbitFlare comme provider gRPC Devnet gratuit pour les tests KSP futurs, en complément de PublicNode Mainnet/Testnet.
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
|
||||||
|
Classification :
|
||||||
|
|
||||||
|
```text
|
||||||
|
X-ORBIT-KEY Customer API only
|
||||||
|
Bearer Customer API v2
|
||||||
|
RPC api_key HTTP RPC only
|
||||||
|
account API key ne prouve pas une auth Yellowstone
|
||||||
|
gRPC Dashboard token possible selon licence/service
|
||||||
|
x-token mécanisme Yellowstone plausible si token gRPC distinct
|
||||||
|
aucune metadata documentée par SDK Go pour endpoints régionaux
|
||||||
|
```
|
||||||
|
|
||||||
|
La clé de compte opérateur ne doit pas être fournie au dépôt ni injectée par supposition dans gRPC.
|
||||||
|
|
||||||
|
### Heartbeat
|
||||||
|
|
||||||
|
OrbitFlare recommande un `SubscribeRequest::Ping` périodique. Yellowstone upstream envoie déjà un `SubscribeUpdate::Ping` périodique et KSP N1 répond automatiquement par un Ping standard sans modifier le dernier full request mémorisé.
|
||||||
|
|
||||||
|
Décision :
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun timer OrbitFlare ajouté au moteur
|
||||||
|
aucun changement YellowstoneGrpcSessionSettings
|
||||||
|
aucune modification grpc_stream.rs
|
||||||
|
pre.002 doit observer le Ping serveur live OrbitFlare Devnet
|
||||||
|
```
|
||||||
|
|
||||||
|
Si ce Ping est observé, la policy standard N1 est suffisante. Sinon, une divergence provider doit être démontrée puis composée au-dessus de N1 sans le modifier.
|
||||||
|
|
||||||
|
### Limits
|
||||||
|
|
||||||
|
```text
|
||||||
|
50 connexions gRPC simultanées par IP sur shared
|
||||||
|
subscriptions par connexion annoncées unlimited
|
||||||
|
idle timeout environ 10 minutes
|
||||||
|
RESOURCE_EXHAUSTED au dépassement
|
||||||
|
backoff exponentiel conseillé
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces valeurs restent provider-owned.
|
||||||
|
|
||||||
|
## 6. Réaudit Yellowstone actuel
|
||||||
|
|
||||||
|
État observé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
release GitHub courante v15.1.2+solana.4.2.0
|
||||||
|
yellowstone-grpc-proto publié 12.6.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune évolution matérielle ne remet en cause le standard N2 de `0.2.9`. Aucune dependency n'est modifiée.
|
||||||
|
|
||||||
|
## 7. Invariant architectural supersédant l'hypothèse initiale
|
||||||
|
|
||||||
|
Décision opérateur explicite :
|
||||||
|
|
||||||
|
```text
|
||||||
|
le moteur gRPC ne change jamais pour un provider
|
||||||
|
le moteur WebSocket ne change jamais pour un provider
|
||||||
|
les fonctionnalités provider se composent au-dessus du moteur
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour `0.2.10` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
N1 moteur Yellowstone = IMMUTABLE
|
||||||
|
N2 standard Solana = IMMUTABLE
|
||||||
|
N3 OrbitFlare = profile/capability/facade seulement si besoin démontré
|
||||||
|
```
|
||||||
|
|
||||||
|
Le prompt `015` reste autoritatif sauf sur toute formulation qui permettrait de mettre une policy OrbitFlare directement dans le moteur partagé.
|
||||||
|
|
||||||
|
## 8. Config V3
|
||||||
|
|
||||||
|
La shape existante suffit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
profile_id = orbitflare_devnet
|
||||||
|
provider = orbitflare
|
||||||
|
cluster = devnet
|
||||||
|
protocol = solana_yellowstone
|
||||||
|
url = http://devnet.rpc.orbitflare.com:10000
|
||||||
|
metadata = []
|
||||||
|
secret_metadata = []
|
||||||
|
```
|
||||||
|
|
||||||
|
Pas de V4. Pas de secret ajouté avant preuve live. Pas de champ heartbeat moteur.
|
||||||
|
|
||||||
|
## 9. Smoke prévu
|
||||||
|
|
||||||
|
`pre.002` doit commencer par un canari standard N2 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
connect OrbitFlare Devnet Free sans metadata
|
||||||
|
Subscribe slots confirmed
|
||||||
|
recevoir Slot non nul
|
||||||
|
observer SubscribeUpdate::Ping pendant une fenêtre bornée de caractérisation
|
||||||
|
fermer proprement
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis sonder séparément les unary et replay sans rendre le smoke minimal dépendant d'entitlements provider non garantis.
|
||||||
|
|
||||||
|
## 10. Forecast recalibré
|
||||||
|
|
||||||
|
Chemin standard probable :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.001 audit/sizing
|
||||||
|
pre.002 Config Devnet + characterization smoke standard N2
|
||||||
|
pre.003 gate technique/live final
|
||||||
|
pre.004 réconciliation documentaire finale
|
||||||
|
pre.005 publication minimale
|
||||||
|
rel.001 stable
|
||||||
|
```
|
||||||
|
|
||||||
|
Chemin avec divergence provider démontrée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.001 audit/sizing
|
||||||
|
pre.002 Config Devnet + characterization smoke
|
||||||
|
pre.003 provider overlay minimal au-dessus de N1/N2
|
||||||
|
pre.004 gate technique/live final
|
||||||
|
pre.005 réconciliation documentaire finale
|
||||||
|
pre.006 publication minimale
|
||||||
|
rel.001 stable
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md
|
||||||
|
deltas/0.2.10/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
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune implémentation provider ou Config runtime n'est introduite dans `pre.001`.
|
||||||
|
|
||||||
|
## 14. Sources externes réauditées
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://docs.orbitflare.com/llms.txt
|
||||||
|
https://docs.orbitflare.com/cli
|
||||||
|
https://docs.orbitflare.com/authentication
|
||||||
|
https://docs.orbitflare.com/data-streaming/yellowstone
|
||||||
|
https://docs.orbitflare.com/data-streaming/yellowstone-quickstart
|
||||||
|
https://docs.orbitflare.com/sdk/go-grpc
|
||||||
|
https://orbitflare.com/pricing
|
||||||
|
https://orbitflare.com/products/rpc-nodes
|
||||||
|
https://orbitflare.com/products/solana-grpc
|
||||||
|
https://github.com/rpcpool/yellowstone-grpc
|
||||||
|
https://github.com/rpcpool/yellowstone-grpc/releases
|
||||||
|
https://github.com/rpcpool/yellowstone-grpc/blob/master/README.md
|
||||||
|
https://github.com/rpcpool/yellowstone-grpc/blob/master/yellowstone-grpc-proto/proto/geyser.proto
|
||||||
|
```
|
||||||
|
|
||||||
|
## 15. Validations de préparation
|
||||||
|
|
||||||
|
Le sandbox a exécuté :
|
||||||
|
|
||||||
|
```text
|
||||||
|
inspection archive/version/rel/prompt OK
|
||||||
|
lecture code N1/N2/Config et docs 0.2.9 OK
|
||||||
|
réaudit OrbitFlare/Yellowstone actuel OK
|
||||||
|
inspection cargo tree --duplicates fourni par opérateur OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Le sandbox ne dispose pas de Cargo ; aucun `cargo check/clippy/test/tree` post-overlay n'est déclaré réussi localement.
|
||||||
|
|
||||||
|
## 16. Validation opérateur attendue avant commit
|
||||||
|
|
||||||
|
Comme `pre.001` change uniquement la version Cargo et la documentation :
|
||||||
|
|
||||||
|
```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.10
|
||||||
|
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 : aucun changement de dependency et aucun changement sous `crates/`.
|
||||||
196
deltas/0.2.10/pre.002-fix.001.md
Normal file
196
deltas/0.2.10/pre.002-fix.001.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<!-- file: deltas/0.2.10/pre.002-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.10-pre.002-fix.001` — OrbitFlare License Key `x-token`
|
||||||
|
|
||||||
|
## 1. Base et identité
|
||||||
|
|
||||||
|
Base exacte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.10-pre.002 commité
|
||||||
|
```
|
||||||
|
|
||||||
|
Le fix porte une correction runtime/configuration de l’auth provider. La version Cargo devient donc :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.10-pre.2.fix.1
|
||||||
|
commit attendu = v0.2.10-pre.002-fix.001
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Défaut démontré par le live `pre.002`
|
||||||
|
|
||||||
|
Le gate statique/workspace `pre.002` est vert. Le smoke live sans metadata a atteint OrbitFlare puis l’ouverture du stream a échoué de manière sûre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
grpc_operation = SubscribeOpen
|
||||||
|
grpc_status = Unauthenticated
|
||||||
|
grpc_code = The request does not have valid authentication credentials
|
||||||
|
```
|
||||||
|
|
||||||
|
Le Dashboard opérateur montre un produit `Solana Free` avec License Key `ORBIT-*` et `API Key Mode Active`.
|
||||||
|
|
||||||
|
La référence Yellowstone OrbitFlare précise ensuite le contrat data-plane :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ORBITFLARE_LICENSE_KEY
|
||||||
|
-> metadata gRPC x-token
|
||||||
|
-> Yellowstone
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `X-ORBIT-KEY` et le Bearer Device Flow restent des credentials Customer API et ne sont pas utilisés par Transport Yellowstone.
|
||||||
|
|
||||||
|
## 3. Correction Config V3
|
||||||
|
|
||||||
|
`config/std.transport.json` conserve le même endpoint et ajoute uniquement la metadata secrète requise :
|
||||||
|
|
||||||
|
```text
|
||||||
|
provider = orbitflare
|
||||||
|
cluster = devnet
|
||||||
|
protocol = solana_yellowstone
|
||||||
|
url = http://devnet.rpc.orbitflare.com:10000
|
||||||
|
metadata = []
|
||||||
|
secret_metadata = x-token <- ${KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN}
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun format Config V4 n’est créé.
|
||||||
|
|
||||||
|
`.env.example` inventorie désormais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
La valeur locale attendue est la License Key `ORBIT-*`. Une `X-ORBIT-KEY` Customer API ne doit jamais être placée dans cette variable.
|
||||||
|
|
||||||
|
## 4. Preuve Config déterministe
|
||||||
|
|
||||||
|
Le test `committed_v3_orbitflare_devnet_profile_maps_provider_neutral_yellowstone_grpc_with_license_x_token` injecte un canari secret via `ConfigEnvironment` et prouve :
|
||||||
|
|
||||||
|
```text
|
||||||
|
profil explicite orbitflare_devnet
|
||||||
|
companions HTTP/WS Devnet inchangés
|
||||||
|
endpoint Yellowstone OrbitFlare inchangé
|
||||||
|
metadata key = x-token
|
||||||
|
metadata classée secret
|
||||||
|
validation Transport PASS
|
||||||
|
URL provider absente du Debug
|
||||||
|
canari secret absent du Debug
|
||||||
|
X-ORBIT-KEY absent du Transport
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Smoke live corrigé
|
||||||
|
|
||||||
|
`yellowstone_orbitflare_smoke.rs` ne dépend toujours pas de Config et ne lit aucun environnement. Il suit le pattern de sécurité déjà utilisé par les smokes PublicNode :
|
||||||
|
|
||||||
|
```text
|
||||||
|
une License Key lue sur stdin
|
||||||
|
aucun secret en argument de ligne de commande
|
||||||
|
YellowstoneGrpcMetadataEntry::secret("x-token", ...)
|
||||||
|
reconnect max_retries = 0
|
||||||
|
Subscribe slots confirmed
|
||||||
|
fenêtre Slot + Ping = 45 s
|
||||||
|
close borné
|
||||||
|
```
|
||||||
|
|
||||||
|
Le smoke vérifie également que la License Key n’apparaît pas dans `Debug`.
|
||||||
|
|
||||||
|
Commande opérateur :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
read -rsp 'OrbitFlare License Key: ' ORBITFLARE_LICENSE_KEY; echo
|
||||||
|
printf '%s\n' "$ORBITFLARE_LICENSE_KEY" | cargo test -p ksp-onchain-transport-lib --test yellowstone_orbitflare_smoke -- --ignored --nocapture
|
||||||
|
unset ORBITFLARE_LICENSE_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Invariant architectural
|
||||||
|
|
||||||
|
Toujours inchangés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/grpc_settings.rs
|
||||||
|
src/grpc_channel.rs
|
||||||
|
src/grpc_unary.rs
|
||||||
|
src/grpc_subscribe.rs
|
||||||
|
src/grpc_stream.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Le standard Yellowstone N2 reste inchangé. Aucun SDK OrbitFlare runtime n’est ajouté.
|
||||||
|
|
||||||
|
L’auth provider est entièrement composée par la surface metadata secrète déjà existante.
|
||||||
|
|
||||||
|
## 7. Heartbeat
|
||||||
|
|
||||||
|
Ce fix ne matérialise aucun heartbeat OrbitFlare.
|
||||||
|
|
||||||
|
Le rerun authentifié doit d’abord qualifier :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Slot non nul
|
||||||
|
SubscribeUpdate::Ping serveur
|
||||||
|
```
|
||||||
|
|
||||||
|
Si les deux sont observés, N1 répond déjà automatiquement au Ping standard et aucune couche provider heartbeat n’est requise.
|
||||||
|
|
||||||
|
Si le stream authentifié fonctionne mais n’émet aucun Ping serveur dans la fenêtre de caractérisation, `pre.003` devient une tranche provider-specific au-dessus de N1/N2. Il reste interdit d’ajouter un timer OrbitFlare au moteur gRPC.
|
||||||
|
|
||||||
|
## 8. Validation disponible dans l’environnement de génération
|
||||||
|
|
||||||
|
Exécuté :
|
||||||
|
|
||||||
|
```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.10
|
||||||
|
Markdown table audit: clean (99 table(s), 99 file(s))
|
||||||
|
|
||||||
|
config/std.transport.json
|
||||||
|
JSON parse PASS
|
||||||
|
JSON Schema validation PASS
|
||||||
|
|
||||||
|
comparaison N1 avec pre.002
|
||||||
|
grpc_settings.rs inchangé
|
||||||
|
grpc_channel.rs inchangé
|
||||||
|
grpc_unary.rs inchangé
|
||||||
|
grpc_subscribe.rs inchangé
|
||||||
|
grpc_stream.rs inchangé
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune commande Cargo n’est déclarée exécutée dans l’environnement de génération.
|
||||||
|
|
||||||
|
## 9. Gates opérateur
|
||||||
|
|
||||||
|
Après application du fix :
|
||||||
|
|
||||||
|
```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.10
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis exécuter le live authentifié décrit en section 5.
|
||||||
|
|
||||||
|
## 10. Fichiers ajoutés/modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
.env.example
|
||||||
|
config/std.transport.json
|
||||||
|
crates/ksp-config-lib/unit_tests/transport.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/yellowstone_orbitflare_smoke.rs
|
||||||
|
docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md
|
||||||
|
deltas/0.2.10/pre.002-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Le delta commité `deltas/0.2.10/pre.002.md` reste immuable.
|
||||||
287
deltas/0.2.10/pre.002.md
Normal file
287
deltas/0.2.10/pre.002.md
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
<!-- file: deltas/0.2.10/pre.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.10-pre.002` — OrbitFlare Devnet Config + characterization smoke
|
||||||
|
|
||||||
|
## 1. Base et version
|
||||||
|
|
||||||
|
Base de travail :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.10-pre.001
|
||||||
|
```
|
||||||
|
|
||||||
|
Validation opérateur fournie sur cette base le 2026-08-25 :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py PASS
|
||||||
|
python3 scripts/audit_markdown_tables.py PASS / 97 tables / 233 files
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS
|
||||||
|
cargo test --workspace PASS
|
||||||
|
cargo tree -p ksp-onchain-transport-lib fourni
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates fourni
|
||||||
|
```
|
||||||
|
|
||||||
|
Le graphe confirme notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
yellowstone-grpc-proto 12.6.0
|
||||||
|
tonic 0.14.6
|
||||||
|
tonic-prost 0.14.6
|
||||||
|
prost 0.14.4
|
||||||
|
tokio 1.53.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette livraison passe à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.10-pre.2
|
||||||
|
commit attendu = v0.2.10-pre.002
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Invariant architectural
|
||||||
|
|
||||||
|
Aucun moteur physique n'est modifié :
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/grpc_settings.rs inchangé
|
||||||
|
src/grpc_channel.rs inchangé
|
||||||
|
src/grpc_unary.rs inchangé
|
||||||
|
src/grpc_subscribe.rs inchangé
|
||||||
|
src/grpc_stream.rs inchangé
|
||||||
|
```
|
||||||
|
|
||||||
|
Le standard Yellowstone N2 reste également inchangé.
|
||||||
|
|
||||||
|
OrbitFlare est matérialisé uniquement par :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Config provider profile
|
||||||
|
provider-neutral Yellowstone smoke
|
||||||
|
provider validation/documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun SDK OrbitFlare, second client Tonic, proto provider, heartbeat moteur ou metadata secrète n'est ajouté.
|
||||||
|
|
||||||
|
## 3. Profil Config V3 `orbitflare_devnet`
|
||||||
|
|
||||||
|
`config/std.transport.json` ajoute un profil dédié :
|
||||||
|
|
||||||
|
```text
|
||||||
|
profile_id = orbitflare_devnet
|
||||||
|
```
|
||||||
|
|
||||||
|
Il conserve les companions standards déjà utilisés sur Devnet :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP = https://api.devnet.solana.com
|
||||||
|
WS = wss://api.devnet.solana.com
|
||||||
|
```
|
||||||
|
|
||||||
|
et ajoute exactement un endpoint Yellowstone :
|
||||||
|
|
||||||
|
```text
|
||||||
|
name = orbitflare_solana_devnet_yellowstone
|
||||||
|
provider = orbitflare
|
||||||
|
cluster = devnet
|
||||||
|
protocol = solana_yellowstone
|
||||||
|
url = http://devnet.rpc.orbitflare.com:10000
|
||||||
|
metadata = []
|
||||||
|
secret_metadata = []
|
||||||
|
```
|
||||||
|
|
||||||
|
Le format reste V3. `.env.example` n'est pas modifié et aucune API key de compte OrbitFlare n'est reclassifiée comme credential gRPC.
|
||||||
|
|
||||||
|
`ksp-config-lib` ajoute un test déterministe qui prouve :
|
||||||
|
|
||||||
|
```text
|
||||||
|
sélection explicite orbitflare_devnet
|
||||||
|
companions HTTP/WS Devnet préservés
|
||||||
|
provider/cluster/protocol gRPC corrects
|
||||||
|
URL plaintext conservée telle quelle
|
||||||
|
aucune metadata inventée
|
||||||
|
Debug gRPC sans URL provider
|
||||||
|
absence de X-ORBIT-KEY dans Transport settings
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Smoke OrbitFlare Devnet
|
||||||
|
|
||||||
|
Nouveau test opt-in :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/tests/yellowstone_orbitflare_smoke.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test utilise directement les API publiques Yellowstone existantes de Transport, sans dépendre de Config :
|
||||||
|
|
||||||
|
```text
|
||||||
|
endpoint Devnet OrbitFlare sans metadata
|
||||||
|
reconnect max_retries = 0
|
||||||
|
Subscribe slots
|
||||||
|
commitment = confirmed
|
||||||
|
fenêtre de caractérisation = 45 s
|
||||||
|
preuve Slot non nul
|
||||||
|
preuve SubscribeUpdate::Ping serveur
|
||||||
|
close borné
|
||||||
|
```
|
||||||
|
|
||||||
|
Le `Ping` reçu reste traité par le moteur N1 déjà livré en `0.2.9`, qui répond automatiquement sur le même stream. Le test n'ajoute aucune policy heartbeat.
|
||||||
|
|
||||||
|
Commande live :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test -p ksp-onchain-transport-lib --test yellowstone_orbitflare_smoke -- --ignored --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
Interprétation :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Slot + Ping PASS
|
||||||
|
OrbitFlare reste standard ; aucune façade/policy provider heartbeat requise
|
||||||
|
|
||||||
|
connection/auth failure
|
||||||
|
classifier le service/licence réel avant toute metadata
|
||||||
|
|
||||||
|
Slot PASS mais Ping absent
|
||||||
|
qualifier une divergence heartbeat provider avant toute implémentation spécifique
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Audit Markdown étendu
|
||||||
|
|
||||||
|
`scripts/audit_markdown_tables.py` passe en version 2 et contrôle désormais aussi l'équivalent structurel de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
\n\s*\n\s*\n
|
||||||
|
```
|
||||||
|
|
||||||
|
c'est-à-dire au moins deux lignes vides consécutives hors fenced code block.
|
||||||
|
|
||||||
|
Le contrôle :
|
||||||
|
|
||||||
|
```text
|
||||||
|
considère une ligne whitespace-only comme vide
|
||||||
|
signale fichier + première ligne + longueur de la séquence
|
||||||
|
n'interprète pas les blancs internes aux ``` / ~~~ fenced blocks
|
||||||
|
ignore les répertoires générés ou tiers en parcours récursif
|
||||||
|
```
|
||||||
|
|
||||||
|
Répertoires récursifs exclus :
|
||||||
|
|
||||||
|
```text
|
||||||
|
.git
|
||||||
|
.idea
|
||||||
|
.venv
|
||||||
|
__pycache__
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
target
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette exclusion rend le résultat déterministe entre une archive Gitea propre et un checkout développeur contenant des `node_modules`. Le nombre de fichiers audités peut donc diminuer par rapport à l'ancien script sans signifier une perte de couverture KSP-owned.
|
||||||
|
|
||||||
|
Une seule séquence de deux lignes vides existait dans le scope KSP actif de l'archive : elle est supprimée dans `crates/ksp-onchain-transport-lib/README.md`.
|
||||||
|
|
||||||
|
`docs/rules/RULES_DOCUMENTATION.md` formalise `DOC-BLANK-001` à `DOC-BLANK-003`, y compris l'immuabilité des anciens deltas publiés.
|
||||||
|
|
||||||
|
## 6. Documents de plan/validation
|
||||||
|
|
||||||
|
Le plan 017 et la validation 013 passent en version 2 pour enregistrer :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.001 operator gate fermé
|
||||||
|
profil orbitflare_devnet matérialisé
|
||||||
|
smoke de caractérisation matérialisé
|
||||||
|
N1/N2 inchangés
|
||||||
|
live Devnet encore PENDING
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun README/USAGE OrbitFlare durable n'est finalisé ici ; cette responsabilité reste réservée au couloir documentaire final.
|
||||||
|
|
||||||
|
## 7. Validation disponible dans l'environnement de génération
|
||||||
|
|
||||||
|
Exécuté sans Cargo :
|
||||||
|
|
||||||
|
```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.10
|
||||||
|
clean / 98 tables / 98 KSP-owned files
|
||||||
|
|
||||||
|
config/std.transport.json
|
||||||
|
JSON parse PASS
|
||||||
|
JSON Schema validation PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Le nouvel audit Markdown a aussi été caractérisé avec des fixtures temporaires :
|
||||||
|
|
||||||
|
```text
|
||||||
|
deux lignes vides hors fence FAIL attendu
|
||||||
|
plusieurs lignes vides dans fence PASS attendu
|
||||||
|
node_modules récursif ignoré attendu
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune commande Cargo n'est déclarée exécutée dans l'environnement de génération.
|
||||||
|
|
||||||
|
## 8. Gates opérateur `pre.002`
|
||||||
|
|
||||||
|
Exécuter :
|
||||||
|
|
||||||
|
```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.10
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis le live dédié :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test -p ksp-onchain-transport-lib --test yellowstone_orbitflare_smoke -- --ignored --nocapture
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.002` ne ferme son gate provider que lorsque le résultat live est classifié. Un échec d'auth ou l'absence de `Ping` ne doit jamais être corrigé par modification du moteur N1.
|
||||||
|
|
||||||
|
## 9. Fichiers ajoutés/modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
config/std.transport.json
|
||||||
|
crates/ksp-config-lib/unit_tests/transport.rs
|
||||||
|
crates/ksp-onchain-transport-lib/README.md
|
||||||
|
crates/ksp-onchain-transport-lib/tests/yellowstone_orbitflare_smoke.rs
|
||||||
|
docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/rules/RULES_DOCUMENTATION.md
|
||||||
|
docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md
|
||||||
|
scripts/audit_markdown_tables.py
|
||||||
|
deltas/0.2.10/pre.002.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Suite conditionnelle
|
||||||
|
|
||||||
|
Si le live retourne `Slot + Ping PASS` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.003 = gate technique/live final
|
||||||
|
```
|
||||||
|
|
||||||
|
Si une divergence provider réelle est démontrée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.003 = tranche provider-specific minimale au-dessus de N1/N2
|
||||||
|
pre.004 = gate technique/live final
|
||||||
|
```
|
||||||
|
|
||||||
|
Le couloir documentaire final puis la prerelease de publication restent distincts dans les deux cas.
|
||||||
252
deltas/0.2.10/pre.003.md
Normal file
252
deltas/0.2.10/pre.003.md
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
<!-- file: deltas/0.2.10/pre.003.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.10-pre.003` — gate technique/live final OrbitFlare Yellowstone
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
Base directe attendue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison : 0.2.10-pre.002-fix.001
|
||||||
|
Cargo : 0.2.10-pre.2.fix.1
|
||||||
|
commit : v0.2.10-pre.002-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
Identité de cette tranche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison : 0.2.10-pre.003
|
||||||
|
Cargo : 0.2.10-pre.3
|
||||||
|
commit : v0.2.10-pre.003
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette prerelease est exclusivement le couloir **technique/live final** de `0.2.10`. Elle ne porte aucune réconciliation README/USAGE/plan/validation, aucune finalisation CHANGELOG/ROADMAP et aucun prompt `0.2.11`.
|
||||||
|
|
||||||
|
## 2. Gate acquis de `pre.002-fix.001`
|
||||||
|
|
||||||
|
La validation opérateur reçue le 2026-08-25 ferme le défaut d'auth classifié dans `pre.002`.
|
||||||
|
|
||||||
|
Le gate déterministe est intégralement vert :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py clean / 0 export candidate / clean
|
||||||
|
python3 scripts/audit_markdown_tables.py PASS, 99 tables / 99 files
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS
|
||||||
|
cargo test --workspace PASS
|
||||||
|
Config unit 114/114 PASS
|
||||||
|
Config ownership 5/5 PASS
|
||||||
|
Config public API 15/15 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Le smoke OrbitFlare Devnet authentifié est également vert :
|
||||||
|
|
||||||
|
```text
|
||||||
|
endpoint http://devnet.rpc.orbitflare.com:10000
|
||||||
|
auth License Key ORBIT-* -> secret metadata x-token
|
||||||
|
Subscribe PASS
|
||||||
|
Slot > 0 PASS
|
||||||
|
server Ping PASS
|
||||||
|
close borné PASS
|
||||||
|
résultat 1 passed / 0 failed
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette preuve ferme la branche de divergence heartbeat : OrbitFlare émet le `SubscribeUpdate::Ping` Yellowstone standard et le moteur N1 `0.2.9` possède déjà la réponse automatique déterministe. Aucun heartbeat provider, aucune façade OrbitFlare et aucune modification N1/N2 ne sont nécessaires.
|
||||||
|
|
||||||
|
## 3. Responsabilité de `pre.003`
|
||||||
|
|
||||||
|
`pre.003` ne matérialise aucun nouveau comportement. Elle rejoue et ferme les preuves techniques finales après le fix d'auth :
|
||||||
|
|
||||||
|
```text
|
||||||
|
version Cargo prerelease synchronisée
|
||||||
|
workspace complet sans warning
|
||||||
|
invariants Rust/Markdown
|
||||||
|
Config V3 OrbitFlare x-token non régressée
|
||||||
|
Yellowstone N1/N2 non régressés
|
||||||
|
HTTP 52 current + 14 historical non régressés
|
||||||
|
WebSocket standard et Helius non régressés
|
||||||
|
smoke live OrbitFlare Devnet final
|
||||||
|
smokes PublicNode Mainnet/Testnet finaux si credentials opérateur disponibles
|
||||||
|
graphes Cargo Transport et doublons inspectés
|
||||||
|
aucun SDK OrbitFlare runtime
|
||||||
|
aucune dépendance Transport -> Config
|
||||||
|
```
|
||||||
|
|
||||||
|
Le payload de `pre.003` ne change ni code runtime, ni test, ni Config, ni schema, ni dépendance. Le seul changement technique est le signal de version Cargo obligatoire pour une prerelease non-fix.
|
||||||
|
|
||||||
|
## 4. Invariants à fermer
|
||||||
|
|
||||||
|
Les fichiers moteur Yellowstone restent hors modification :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/src/grpc_settings.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/grpc_channel.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/grpc_unary.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/grpc_subscribe.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/grpc_stream.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Le gate doit confirmer :
|
||||||
|
|
||||||
|
```text
|
||||||
|
N1 gRPC provider-neutral inchangé
|
||||||
|
N2 Yellowstone standard inchangé
|
||||||
|
OrbitFlare composé par endpoint + provider + cluster + x-token Config V3
|
||||||
|
X-ORBIT-KEY absent du data-plane Yellowstone
|
||||||
|
License Key jamais exposée par Debug/CLI
|
||||||
|
pas de format Config V4
|
||||||
|
pas de provider proto
|
||||||
|
pas de second raw Tonic client
|
||||||
|
pas de orbitflare-sdk runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Gate déterministe final
|
||||||
|
|
||||||
|
Après application de `pre.003` :
|
||||||
|
|
||||||
|
```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.10
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Seuils minimaux attendus :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Config unit 114
|
||||||
|
Config ownership 5
|
||||||
|
Config public API 15
|
||||||
|
Transport unit 383
|
||||||
|
Transport public API 49
|
||||||
|
Transport release completeness 43
|
||||||
|
Transport doctests 4
|
||||||
|
workspace dependency canary 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Toute régression imputable à KSP ouvre `pre.003-fix.NNN`. Elle ne doit pas être repoussée dans le couloir documentaire `pre.004`.
|
||||||
|
|
||||||
|
## 6. Smoke OrbitFlare Devnet final
|
||||||
|
|
||||||
|
Saisir la License Key sans l'inscrire dans l'historique :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
read -rsp 'OrbitFlare License Key: ' ORBITFLARE_LICENSE_KEY; echo
|
||||||
|
printf '%s\n' "$ORBITFLARE_LICENSE_KEY" \
|
||||||
|
| cargo test -p ksp-onchain-transport-lib --test yellowstone_orbitflare_smoke -- --ignored --nocapture
|
||||||
|
unset ORBITFLARE_LICENSE_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
orbitflare_devnet_yellowstone_streams_slots_and_emits_standard_server_ping_with_license_x_token ... ok
|
||||||
|
1 passed
|
||||||
|
0 failed
|
||||||
|
0 ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
Le smoke doit encore prouver dans une même session bornée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
auth x-token acceptée
|
||||||
|
Subscribe standard ouvert
|
||||||
|
Slot non nul reçu
|
||||||
|
SubscribeUpdate::Ping standard observé
|
||||||
|
fermeture bornée
|
||||||
|
```
|
||||||
|
|
||||||
|
Un incident externe OrbitFlare/réseau démontré comme tel est documenté comme `EXTERNAL BLOCK`. Un échec de mapping, metadata, moteur, protocol ou fermeture imputable à KSP ouvre un fix technique.
|
||||||
|
|
||||||
|
## 7. PublicNode et graphes finaux
|
||||||
|
|
||||||
|
Les smokes PublicNode Mainnet/Testnet restent les canaris provider du standard livré par `0.2.9`. Si les deux credentials opérateur sont disponibles pendant ce gate, les rejouer :
|
||||||
|
|
||||||
|
```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 lorsque rejoué :
|
||||||
|
|
||||||
|
```text
|
||||||
|
2 passed
|
||||||
|
0 failed
|
||||||
|
0 ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
Inspecter ensuite :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
cargo tree --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
Points obligatoires :
|
||||||
|
|
||||||
|
```text
|
||||||
|
yellowstone-grpc-proto 12.6.x sur la stack attendue
|
||||||
|
Tonic/Prost KSP-owned sans seconde génération incompatible
|
||||||
|
aucun yellowstone-grpc-client runtime
|
||||||
|
aucun orbitflare-sdk runtime
|
||||||
|
aucune dépendance ksp-onchain-transport-lib -> ksp-config-lib
|
||||||
|
aucun tracing direct Transport
|
||||||
|
aucun nouveau doublon introduit par 0.2.10
|
||||||
|
```
|
||||||
|
|
||||||
|
Les smokes PublicNode ne deviennent pas un blocker si leurs credentials ne sont simplement pas disponibles au moment du gate : leur non-régression déterministe reste couverte par les suites Transport/Config et le code live n'a pas changé. En revanche, tout échec effectivement observé lors d'un rerun doit être classifié, jamais ignoré.
|
||||||
|
|
||||||
|
## 8. Critère de fermeture technique
|
||||||
|
|
||||||
|
`pre.003` est fermée lorsque :
|
||||||
|
|
||||||
|
```text
|
||||||
|
gate déterministe final vert
|
||||||
|
OrbitFlare Devnet Subscribe -> Slot + Ping live PASS
|
||||||
|
graphes Cargo finaux inspectés
|
||||||
|
N1 inchangé
|
||||||
|
N2 inchangé
|
||||||
|
Config V3 x-token cohérente et redacted
|
||||||
|
aucun provider overlay nécessaire
|
||||||
|
aucun heartbeat provider nécessaire
|
||||||
|
aucun SDK OrbitFlare ajouté
|
||||||
|
aucun défaut technique reporté vers pre.004
|
||||||
|
```
|
||||||
|
|
||||||
|
Une fois ce gate acquis, la prochaine tranche est obligatoirement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.10-pre.004 — réconciliation documentaire finale
|
||||||
|
```
|
||||||
|
|
||||||
|
Elle portera plan, validation, README/USAGE et références durables. Elle ne doit pas porter de nouveau correctif technique.
|
||||||
|
|
||||||
|
## 9. Modifications de cette livraison
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
# version: 264 -> 265
|
||||||
|
workspace.package.version: 0.2.10-pre.2.fix.1 -> 0.2.10-pre.3
|
||||||
|
|
||||||
|
deltas/0.2.10/pre.003.md
|
||||||
|
ajout du présent gate technique/live final
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun autre fichier n'est modifié.
|
||||||
128
deltas/0.2.10/pre.004.md
Normal file
128
deltas/0.2.10/pre.004.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<!-- file: deltas/0.2.10/pre.004.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.10-pre.004` — réconciliation documentaire finale OrbitFlare Yellowstone
|
||||||
|
|
||||||
|
## 1. Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.10-pre.003
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette tranche est exclusivement le couloir de réconciliation documentaire finale. Le gate technique/live de `pre.003` est vert ; aucun correctif runtime, Config exécutable, test, schema, dependency ou feature n'est introduit ici.
|
||||||
|
|
||||||
|
## 2. Version technique
|
||||||
|
|
||||||
|
Comme toute prerelease non-fix, la version Cargo est synchronisée mécaniquement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.10-pre.4
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune sémantique runtime ne change.
|
||||||
|
|
||||||
|
## 3. Preuve technique réconciliée
|
||||||
|
|
||||||
|
Le gate opérateur `pre.003` du 2026-08-25 est fermé avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all PASS
|
||||||
|
Rust workspace audit PASS / 0 export candidate
|
||||||
|
Markdown audit PASS / 99 tables / 100 files
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS
|
||||||
|
Config unit 114/114 PASS
|
||||||
|
Config ownership 5/5 PASS
|
||||||
|
Config public API 15/15 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
|
||||||
|
OrbitFlare Devnet Subscribe + Slot + Ping 1/1 PASS, 5.19 s
|
||||||
|
```
|
||||||
|
|
||||||
|
Les graphes Cargo direct Transport, doublons Transport et doublons workspace ont été inspectés. La pile reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
yellowstone-grpc-proto 12.6.0
|
||||||
|
tonic / tonic-prost 0.14.6
|
||||||
|
prost / prost-types 0.14.4
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun `yellowstone-grpc-client` runtime, aucun `orbitflare-sdk` runtime et aucune dépendance `Transport -> Config` n'ont été ajoutés.
|
||||||
|
|
||||||
|
## 4. OrbitFlare final
|
||||||
|
|
||||||
|
Contrat durable réconcilié :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Devnet endpoint http://devnet.rpc.orbitflare.com:10000
|
||||||
|
auth License Key ORBIT-* -> secret metadata x-token
|
||||||
|
Config secret KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN
|
||||||
|
Subscribe standard Yellowstone N2
|
||||||
|
live Slot non nul + server Ping
|
||||||
|
N1 inchangé
|
||||||
|
N2 inchangé
|
||||||
|
provider facade non nécessaire
|
||||||
|
heartbeat N3 non nécessaire
|
||||||
|
Config V4 non nécessaire
|
||||||
|
```
|
||||||
|
|
||||||
|
Le premier échec `pre.002` sans metadata reste conservé dans son delta historique comme caractérisation ayant conduit au fix d'auth `pre.002-fix.001`.
|
||||||
|
|
||||||
|
## 5. Documents réconciliés
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md
|
||||||
|
crates/ksp-onchain-transport-lib/README.md
|
||||||
|
crates/ksp-onchain-transport-lib/USAGE.md
|
||||||
|
docs/000-README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Les anciennes affirmations `PENDING` sont remplacées par les résultats `pre.003` réellement observés. README/USAGE documentent désormais le profil OrbitFlare Devnet et son smoke live durable.
|
||||||
|
|
||||||
|
## 6. Frontière avec `pre.005`
|
||||||
|
|
||||||
|
Cette tranche ne touche pas :
|
||||||
|
|
||||||
|
```text
|
||||||
|
CHANGELOG.md
|
||||||
|
ROADMAP.md
|
||||||
|
prompt suivant
|
||||||
|
```
|
||||||
|
|
||||||
|
La décision sur la release suivante est prise pendant cette réconciliation, sans générer encore son prompt : Helius LaserStream gRPC est reporté dans les TODO Yellowstone sans numéro de release faute d'accès live raisonnable ; la séquence active avance et `0.2.11` devient `off-chain price transport`, suivie de `0.2.12` Price Desk + intégration Wallet Desk, `0.2.13` interface/wire foundation et `0.2.14` program-api foundation. Le futur `ROADMAP.md` de `pre.005` devra refléter exactement ce report et cette renumérotation.
|
||||||
|
|
||||||
|
Une fois cette décision prise, `pre.005` reste strictement publication-minimal : prompt suivant + `CHANGELOG.md` + `ROADMAP.md` + signal Cargo + delta.
|
||||||
|
|
||||||
|
Si un défaut technique de `0.2.10` est découvert après ce gate documentaire, il ne doit pas être glissé dans `pre.005`; ouvrir une nouvelle prerelease du couloir approprié puis rejouer les tranches finales nécessaires.
|
||||||
|
|
||||||
|
## 7. Fichiers modifiés/ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
docs/000-README.md
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
docs/plans/017-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC_PLAN.md
|
||||||
|
docs/validation/013-V0_2_10_ORBITFLARE_YELLOWSTONE_GRPC.md
|
||||||
|
crates/ksp-onchain-transport-lib/README.md
|
||||||
|
crates/ksp-onchain-transport-lib/USAGE.md
|
||||||
|
deltas/0.2.10/pre.004.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 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.10
|
||||||
|
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 : aucune surface technique ou dépendance n'est modifiée.
|
||||||
132
deltas/0.2.10/pre.005.md
Normal file
132
deltas/0.2.10/pre.005.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<!-- file: deltas/0.2.10/pre.005.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.10-pre.005` — préparation de publication minimale
|
||||||
|
|
||||||
|
## 1. Base
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.10-pre.004
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette tranche est exclusivement le couloir de préparation de publication. Elle ne modifie aucun code, test, schema, config, README, USAGE, plan, validation ou règle normative.
|
||||||
|
|
||||||
|
## 2. Gate `pre.003` et réconciliation `pre.004`
|
||||||
|
|
||||||
|
Le gate technique final `pre.003` a confirmé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Rust workspace audit PASS
|
||||||
|
Markdown audit PASS
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS
|
||||||
|
cargo test --workspace PASS
|
||||||
|
OrbitFlare live Subscribe -> Slot+Ping PASS en 5.19 s
|
||||||
|
graphes Cargo inspectés
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.004` a ensuite réconcilié les documents durables : plan/validation OrbitFlare, README/USAGE Transport, index documentaire et séquence fonctionnelle. Helius LaserStream gRPC a été reporté dans les TODO faute d'accès live raisonnable sans abonnement payant, sans remise en cause de sa compatibilité wire Yellowstone largement observée.
|
||||||
|
|
||||||
|
## 3. Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.10-pre.5
|
||||||
|
```
|
||||||
|
|
||||||
|
La modification est mécanique ; aucune sémantique runtime ne change.
|
||||||
|
|
||||||
|
## 4. `CHANGELOG.md`
|
||||||
|
|
||||||
|
Le changelog stable est préparé pour `0.2.10 — OrbitFlare Yellowstone gRPC` avec les faits consolidés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
N1/N2 Yellowstone inchangés
|
||||||
|
Config V3 orbitflare_devnet
|
||||||
|
x-token = License Key via secret_metadata
|
||||||
|
premier live sans metadata -> Unauthenticated
|
||||||
|
fix auth documenté
|
||||||
|
live Subscribe -> Slot + standard Ping PASS
|
||||||
|
second live final PASS
|
||||||
|
aucun heartbeat/provider SDK/Config V4 ajouté
|
||||||
|
workspace + graphes finaux verts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. `ROADMAP.md`
|
||||||
|
|
||||||
|
`0.2.10` passe à l'état réalisé. Helius LaserStream gRPC quitte la séquence numérotée et rejoint les TODO Yellowstone. La séquence active devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.11 off-chain price transport
|
||||||
|
0.2.12 Price Desk + intégration prix Wallet Desk
|
||||||
|
0.2.13 interface/wire foundation
|
||||||
|
0.2.14 program-api foundation
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Audit préparatoire de la future `0.2.11`
|
||||||
|
|
||||||
|
Avant de rédiger le prompt suivant, un audit externe rapide au 2026-08-25 a comparé plusieurs familles de sources :
|
||||||
|
|
||||||
|
```text
|
||||||
|
CoinGecko agrégateur, REST Demo, multi quote USD/EUR
|
||||||
|
Jupiter V3 prix Solana heuristique issu des swaps, USD seulement
|
||||||
|
Pyth Hermes oracle, REST/SSE, publish_time/confidence, auth en transition immédiate
|
||||||
|
Birdeye market data Solana, spot/batch/historique, API key
|
||||||
|
DexScreener prix DEX par pair/pool, accès public courant
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce snapshot ne choisit pas le provider. Il démontre surtout que les sémantiques diffèrent et que `0.2.11-pre.001` doit auditer/choisir explicitement le contrat prix, le modèle numérique, la fraîcheur et le provider initial avant code lourd.
|
||||||
|
|
||||||
|
Pyth annonce un changement d'authentification le 2026-08-26 à 16:00 UTC ; le prompt exige donc un réaudit immédiat de Pyth à l'ouverture et interdit de figer le snapshot de `pre.005`.
|
||||||
|
|
||||||
|
## 7. Prompt `0.2.11`
|
||||||
|
|
||||||
|
Ajout de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/016-V0_2_11_START_PROMPT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Le prompt ouvre `0.2.11 — Off-chain price transport` uniquement depuis `v0.2.10` et impose `pre.001 = lecture + audit actuel providers + matrice sémantique + numeric model + threat model + sizing + planification`.
|
||||||
|
|
||||||
|
Le scope initial reste volontairement borné :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-offchain-transport-lib
|
||||||
|
prix SOL/USD + SOL/EUR
|
||||||
|
premier provider live-testable sans abonnement payant dédié
|
||||||
|
Config -> Off-chain Transport
|
||||||
|
pas de Price Desk
|
||||||
|
pas de Wallet Desk
|
||||||
|
pas d'OHLCV/historique/quotes/metadata/multi-provider complexe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Fichiers modifiés/ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
CHANGELOG.md
|
||||||
|
ROADMAP.md
|
||||||
|
prompts/016-V0_2_11_START_PROMPT.md
|
||||||
|
deltas/0.2.10/pre.005.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun autre fichier ne doit changer dans cette tranche.
|
||||||
|
|
||||||
|
## 9. Gate opérateur
|
||||||
|
|
||||||
|
Comme cette 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.10
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 10. Suite
|
||||||
|
|
||||||
|
Si ce gate est vert, `0.2.10-rel.001` est la seule étape restante. Elle est limitée à la mécanique de publication stable : version Cargo `0.2.10`, delta `rel.001`, commit final et tag stable `v0.2.10`, sans rattrapage technique ou documentaire.
|
||||||
197
deltas/0.2.10/rel.001.md
Normal file
197
deltas/0.2.10/rel.001.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<!-- file: deltas/0.2.10/rel.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.10-rel.001` — publication stable OrbitFlare Yellowstone gRPC
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
Base directe attendue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.10-pre.005
|
||||||
|
workspace.package.version = 0.2.10-pre.5
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit attendu pour cette livraison :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.10-rel.001
|
||||||
|
```
|
||||||
|
|
||||||
|
Tag stable attendu après validation :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.10
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Responsabilité de `rel.001`
|
||||||
|
|
||||||
|
Cette livraison effectue uniquement la mécanique de publication stable définie par le cycle KSP.
|
||||||
|
|
||||||
|
Elle :
|
||||||
|
|
||||||
|
- passe `workspace.package.version` de `0.2.10-pre.5` à `0.2.10` ;
|
||||||
|
- 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. Preuves techniques acquises avant publication
|
||||||
|
|
||||||
|
Le gate technique/live final `0.2.10-pre.003` a été validé le **25 août 2026** avec :
|
||||||
|
|
||||||
|
```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
|
||||||
|
cargo check --workspace PASS
|
||||||
|
cargo clippy --workspace --all-targets PASS
|
||||||
|
cargo test -p ksp-config-lib PASS
|
||||||
|
cargo test -p ksp-onchain-transport-lib PASS
|
||||||
|
cargo test -p ksp-core-lib --test workspace_dependencies PASS
|
||||||
|
cargo test --workspace PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Les suites directement concernées restent notamment :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Config unit 114 passed
|
||||||
|
Transport unit 383 passed
|
||||||
|
Transport public API 49 passed
|
||||||
|
Transport release completeness 43 passed
|
||||||
|
Transport doctests 4 passed
|
||||||
|
```
|
||||||
|
|
||||||
|
Le smoke live final OrbitFlare a également été rejoué en `pre.003` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
OrbitFlare Devnet Yellowstone
|
||||||
|
x-token = License Key
|
||||||
|
Subscribe -> Slot + standard server Ping
|
||||||
|
1/1 PASS en 5.19 s
|
||||||
|
```
|
||||||
|
|
||||||
|
Les graphes Cargo finaux ont été inspectés dans le même couloir. `pre.004` et `pre.005` n'ont ensuite modifié aucune source runtime, dépendance, config ou schema.
|
||||||
|
|
||||||
|
## 4. Version stable publiée
|
||||||
|
|
||||||
|
La version Cargo devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.10
|
||||||
|
```
|
||||||
|
|
||||||
|
La release stable publiée est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.10 — OrbitFlare Yellowstone gRPC
|
||||||
|
```
|
||||||
|
|
||||||
|
Les acquis fonctionnels et documentaires finalisés avant `rel.001` comprennent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
réutilisation stricte du moteur N1 Yellowstone gRPC existant
|
||||||
|
réutilisation stricte du contrat N2 Yellowstone standard existant
|
||||||
|
aucune modification provider du moteur gRPC
|
||||||
|
profil Config V3 orbitflare_devnet
|
||||||
|
endpoint Devnet http://devnet.rpc.orbitflare.com:10000
|
||||||
|
authentification gRPC x-token via License Key secrète
|
||||||
|
premier smoke sans metadata -> Unauthenticated, caractérisé puis corrigé
|
||||||
|
live authentifié Subscribe -> Slot + standard Ping PASS
|
||||||
|
aucun heartbeat provider supplémentaire
|
||||||
|
aucun SDK OrbitFlare ajouté
|
||||||
|
aucun Config V4
|
||||||
|
```
|
||||||
|
|
||||||
|
La release confirme donc qu'OrbitFlare compose au-dessus de N1/N2 par configuration provider et metadata secrète, sans divergence protocolaire nécessitant une façade gRPC dédiée.
|
||||||
|
|
||||||
|
## 5. Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.2.10/rel.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Fichiers supprimés
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Validations de préparation du delta
|
||||||
|
|
||||||
|
Contrôles statiques de l'overlay :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml version 0.2.10
|
||||||
|
payload rel.001 2 fichiers exactement
|
||||||
|
aucun fichier de rattrapage présent
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune nouvelle compilation n'est requise pour construire l'archive elle-même. Le gate stable final doit toutefois être exécuté après application du delta et avant le commit/tag.
|
||||||
|
|
||||||
|
## 9. Validation stable après application
|
||||||
|
|
||||||
|
Exécuter avant publication :
|
||||||
|
|
||||||
|
```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.10
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Le changement étant limité au signal de version Cargo et au présent delta :
|
||||||
|
|
||||||
|
- aucun smoke live OrbitFlare n'est à rejouer si l'overlay appliqué est exactement `0.2.10-rel.001` ;
|
||||||
|
- aucun `cargo tree` n'est à rejouer ;
|
||||||
|
- aucun smoke PublicNode n'est à rejouer.
|
||||||
|
|
||||||
|
## 10. Commit et tag stable
|
||||||
|
|
||||||
|
Après succès du gate stable :
|
||||||
|
|
||||||
|
```text
|
||||||
|
commit : v0.2.10-rel.001
|
||||||
|
tag : v0.2.10
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag intermédiaire `rel.001` n'est requis.
|
||||||
|
|
||||||
|
Commandes Git typiques :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add Cargo.toml deltas/0.2.10/rel.001.md
|
||||||
|
git commit -m "v0.2.10-rel.001"
|
||||||
|
git tag v0.2.10
|
||||||
|
git push
|
||||||
|
git push origin v0.2.10
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Suite
|
||||||
|
|
||||||
|
Après publication du tag stable `v0.2.10`, ouvrir :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.11-pre.001 — Off-chain price transport
|
||||||
|
```
|
||||||
|
|
||||||
|
avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/016-V0_2_11_START_PROMPT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.001` reste une tranche d'audit actuel des providers, sémantiques de prix, modèle numérique, fraîcheur/provenance, threat model et sizing avant toute implémentation lourde.
|
||||||
|
|
||||||
|
Helius LaserStream gRPC reste reporté dans les TODO Yellowstone jusqu'à disponibilité d'un accès live pertinent ; il ne fait plus partie de la séquence numérotée active.
|
||||||
269
deltas/0.2.8/pre.001-fix.001.md
Normal file
269
deltas/0.2.8/pre.001-fix.001.md
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.001-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.001-fix.001` — séparation des façades WebSocket et forecast visible
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
Ce correctif s'applique exclusivement après :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.001
|
||||||
|
workspace.package.version = 0.2.8-pre.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Il corrige le **plan et la matrice de validation** de `pre.001` avant toute implémentation de `pre.002`.
|
||||||
|
|
||||||
|
Le correctif est documentaire uniquement. Conformément à `VER-ID-008` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison = 0.2.8-pre.001-fix.001
|
||||||
|
workspace.package.version = 0.2.8-pre.1 # inchangé
|
||||||
|
commit attendu = v0.2.8-pre.001-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Motif du fix
|
||||||
|
|
||||||
|
Le plan initial faisait porter à un `WsSession` public commun toute la surface WebSocket, puis utilisait une capability matrix pour rejeter avant I/O les méthodes standard non supportées par Helius.
|
||||||
|
|
||||||
|
Après revue du code réel `v0.2.7`, cette forme est jugée trop permissive au niveau API : les neuf wrappers standard sont directement implémentés sur `WsSession`. Un endpoint Helius aurait donc pu être représenté par un type exposant publiquement `block_subscribe`, `slots_updates_subscribe` et `vote_subscribe`, même si ces appels étaient ensuite rejetés.
|
||||||
|
|
||||||
|
Décision corrigée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
séparer les façades publiques par protocole
|
||||||
|
partager intégralement le moteur physique/lifecycle
|
||||||
|
rendre les méthodes provider non supportées absentes de la façade Helius
|
||||||
|
conserver une validation interne defense-in-depth
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Architecture corrigée
|
||||||
|
|
||||||
|
Cible :
|
||||||
|
|
||||||
|
```text
|
||||||
|
WsSession
|
||||||
|
moteur physique partagé
|
||||||
|
actor/socket/reconnect/queues
|
||||||
|
│
|
||||||
|
┌────────────┴────────────┐
|
||||||
|
▼ ▼
|
||||||
|
SolanaStandardWsSession HeliusLaserStreamWsSession
|
||||||
|
9 familles 6 standard communes
|
||||||
|
+ transaction
|
||||||
|
+ heartbeat policy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Surface standard
|
||||||
|
|
||||||
|
```text
|
||||||
|
Account
|
||||||
|
Block
|
||||||
|
Logs
|
||||||
|
Program
|
||||||
|
Root
|
||||||
|
Signature
|
||||||
|
Slot
|
||||||
|
SlotsUpdates
|
||||||
|
Vote
|
||||||
|
```
|
||||||
|
|
||||||
|
### Surface Helius
|
||||||
|
|
||||||
|
```text
|
||||||
|
Account
|
||||||
|
Logs
|
||||||
|
Program
|
||||||
|
Root
|
||||||
|
Signature
|
||||||
|
Slot
|
||||||
|
HeliusTransaction
|
||||||
|
```
|
||||||
|
|
||||||
|
Absents de la façade Helius :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Block
|
||||||
|
SlotsUpdates
|
||||||
|
Vote
|
||||||
|
```
|
||||||
|
|
||||||
|
Les DTOs standard réellement identiques restent partagés. Les DTOs Helius sont créés uniquement pour les contrats provider-specific (`transactionSubscribe`, `tokenAccounts`, notification transaction, etc.).
|
||||||
|
|
||||||
|
`WsSession` reste compatible avec la surface standard publiée en `0.2.7`. La façade Helius ne doit fournir aucun escape hatch public (`inner`/`into_inner`) qui permettrait de récupérer un handle générique et de contourner sa surface.
|
||||||
|
|
||||||
|
## 4. Capability matrix — rôle corrigé
|
||||||
|
|
||||||
|
La capability matrix n'est plus la première barrière publique. Elle devient une protection interne :
|
||||||
|
|
||||||
|
```text
|
||||||
|
API/façade correcte
|
||||||
|
-> méthodes impossibles absentes
|
||||||
|
-> validation descriptor/constructor avant I/O
|
||||||
|
-> actor commun
|
||||||
|
```
|
||||||
|
|
||||||
|
Matrice interne conservée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
SolanaStandard:
|
||||||
|
Account Block Logs Program Root Signature Slot SlotsUpdates Vote -> allowed
|
||||||
|
HeliusTransaction -> rejected
|
||||||
|
|
||||||
|
HeliusLaserStream:
|
||||||
|
Account Logs Program Root Signature Slot HeliusTransaction -> allowed
|
||||||
|
Block SlotsUpdates Vote -> rejected
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Gate Cargo désormais fermé
|
||||||
|
|
||||||
|
Les commandes manquantes de `pre.001` ont été fournies par l'opérateur :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
Résolution observée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
futures-util 0.3.34
|
||||||
|
tokio 1.53.1
|
||||||
|
tokio-tungstenite 0.30.0
|
||||||
|
reqwest 0.13.4
|
||||||
|
new Helius SDK aucun
|
||||||
|
```
|
||||||
|
|
||||||
|
Doublons ciblés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
syn 2.0.119 / 3.0.3
|
||||||
|
webpki-roots 0.26.11 / 1.0.9
|
||||||
|
```
|
||||||
|
|
||||||
|
Ils sont transitifs dans le graphe actuel et ne bloquent pas le gate.
|
||||||
|
|
||||||
|
**Verdict : gate `pre.001` positif après ce fix documentaire.**
|
||||||
|
|
||||||
|
## 6. Forecast souple recalibré
|
||||||
|
|
||||||
|
Le forecast revient à `pre.001 -> pre.011` parce que la séparation de façade mérite une tranche propre et que les six wrappers standard supportés par Helius doivent être validés séparément de la mécanique de connexion commune.
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.001 audit/sizing/matrice/dependencies
|
||||||
|
pre.001-fix.001 architecture de façades + gate Cargo + forecast visible
|
||||||
|
|
||||||
|
pre.002 socle protocolaire : protocol kind + façades + connexion physique partagée
|
||||||
|
pre.003 6 familles standard Helius + absence typée Block/SlotsUpdates/Vote
|
||||||
|
pre.004 Config V2 helius_laserstream + secret strategy
|
||||||
|
pre.005 transactionSubscribe/unsubscribe request/filter/options/tokenAccounts
|
||||||
|
pre.006 transactionNotification + reconnect/resubscribe/unsubscribe races
|
||||||
|
pre.007 heartbeat/idle lifecycle
|
||||||
|
pre.008 adversarial provider/security/backpressure
|
||||||
|
pre.009 compliance + standard WS 18/18 + HTTP 52/14 + Config/API canaries
|
||||||
|
pre.010 live smoke opt-in si sûr + README/USAGE + dependency audit
|
||||||
|
pre.011 workspace final + fermeture docs/matrix/indexes + prompt 0.2.9
|
||||||
|
rel.001 stable
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque tranche vise nominalement 15–20 minutes. Le plan contient désormais un tableau immédiatement visible avec objectif, preuves, budget et critères de split pour chaque tranche.
|
||||||
|
|
||||||
|
`pre.011` n'est pas une deadline ; `pre.012+` ou des fixes sont ajoutés si nécessaire.
|
||||||
|
|
||||||
|
## 7. Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.2.8/pre.001-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Fichiers volontairement inchangés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
ROADMAP.md
|
||||||
|
CHANGELOG.md
|
||||||
|
.env.example
|
||||||
|
config/**
|
||||||
|
crates/**
|
||||||
|
docs/000-README.md
|
||||||
|
docs/plans/000-README.md
|
||||||
|
docs/validation/000-README.md
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
deltas/0.2.8/pre.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Le delta `pre.001` publié reste immutable ; ce fix ne le réécrit pas.
|
||||||
|
|
||||||
|
## 10. Validations et preuves
|
||||||
|
|
||||||
|
### Réellement fournies par l'opérateur avant ce fix
|
||||||
|
|
||||||
|
```text
|
||||||
|
baseline v0.2.7 : fmt/audit/check/clippy/test workspace = OK
|
||||||
|
cargo tree transport = exécuté
|
||||||
|
cargo tree transport --duplicates = exécuté
|
||||||
|
```
|
||||||
|
|
||||||
|
### Réellement exécutées dans l'environnement de préparation du fix
|
||||||
|
|
||||||
|
```text
|
||||||
|
inspection du code public WsSession/WsSubscriptionKind/WsSessionSnapshot = OK
|
||||||
|
lecture VERSION_WORKFLOW.md pour VER-ID-008 = OK
|
||||||
|
contrôle overlay documentaire = OK
|
||||||
|
```
|
||||||
|
|
||||||
|
L'audit Rust workspace est réexécuté sur le workspace reconstitué après application de l'overlay lorsque le script est disponible.
|
||||||
|
|
||||||
|
### Non exécutées pour ce fix documentaire
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Motif : aucun code/build/runtime/config n'est modifié par ce correctif et l'environnement de préparation ne fournit pas Cargo.
|
||||||
|
|
||||||
|
## 11. Décisions prises
|
||||||
|
|
||||||
|
```text
|
||||||
|
façades publiques séparées par protocole
|
||||||
|
moteur WsSession actor unique partagé
|
||||||
|
SolanaStandardWsSession = 9 familles standard
|
||||||
|
HeliusLaserStreamWsSession = 6 familles standard + transaction
|
||||||
|
Block/SlotsUpdates/Vote absents de la façade Helius
|
||||||
|
capability matrix conservée defense-in-depth
|
||||||
|
DTOs communs réutilisés si wire identique
|
||||||
|
DTOs Helius dédiés seulement aux divergences
|
||||||
|
pas d'escape hatch Helius vers raw WsSession
|
||||||
|
forecast visible et recalibré jusqu'à pre.011
|
||||||
|
Cargo pre.1 inchangé car fix documentaire
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. Questions ouvertes laissées à `pre.002+`
|
||||||
|
|
||||||
|
```text
|
||||||
|
forme interne minimale pour partager la connexion physique sans dupliquer l'actor
|
||||||
|
forme exacte des constructeurs des deux façades
|
||||||
|
extension minimale de WsSubscriptionKind/snapshot pour HeliusTransaction
|
||||||
|
forme typed des différents transactionDetails insuffisamment documentés
|
||||||
|
wire éventuel futur de enhanced/filtered accountSubscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces questions ne remettent pas en cause la frontière décidée : **surface publique séparée, moteur physique partagé**.
|
||||||
|
|
||||||
|
## 13. Prochaine tranche
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.002
|
||||||
|
```
|
||||||
|
|
||||||
|
Mission : matérialiser uniquement le socle protocolaire et les façades/constructeurs autour du moteur `WsSession` existant. Ne pas commencer `transactionSubscribe` ni Config Helius dans cette tranche.
|
||||||
174
deltas/0.2.8/pre.001-fix.002.md
Normal file
174
deltas/0.2.8/pre.001-fix.002.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.001-fix.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.001-fix.002` — forecast KSP compact et namespace LaserStream WebSocket
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
Ce correctif s'applique exclusivement après :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.001-fix.001
|
||||||
|
workspace.package.version = 0.2.8-pre.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Il reste **documentaire uniquement** : aucun code, Config runtime, schema, manifest ou dépendance n'est modifié.
|
||||||
|
|
||||||
|
```text
|
||||||
|
livraison = 0.2.8-pre.001-fix.002
|
||||||
|
workspace.package.version = 0.2.8-pre.1 # inchangé
|
||||||
|
commit attendu = v0.2.8-pre.001-fix.002
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Motifs du fix
|
||||||
|
|
||||||
|
Deux corrections de lisibilité/contrat sont nécessaires avant `pre.002`.
|
||||||
|
|
||||||
|
### 2.1 Forecast
|
||||||
|
|
||||||
|
Le tableau introduit par `pre.001-fix.001` ne correspond pas au format employé par les plans KSP récents :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.5 -> bloc "Prévision souple révisée", fixes regroupés avec la candidate concernée
|
||||||
|
0.2.7 -> bloc "Forecast recalibré", une ligne compacte par prerelease avec état DONE
|
||||||
|
```
|
||||||
|
|
||||||
|
Le forecast `0.2.8` revient donc à cette forme compacte. Les `fix.*` sont rattachés visuellement à leur `pre.NNN` et peuvent porter leur propre changement d'état sans être présentés comme de nouvelles tranches planifiées.
|
||||||
|
|
||||||
|
### 2.2 Nom LaserStream
|
||||||
|
|
||||||
|
`Helius LaserStream` désigne chez Helius plusieurs surfaces produit. `0.2.8` ne couvre que **LaserStream WebSocket** ; le futur LaserStream gRPC reste distinct et hors scope.
|
||||||
|
|
||||||
|
Le code court suivant est conservé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
WsProtocolKind::HeliusLaserStream
|
||||||
|
as_str() = "helius_laserstream"
|
||||||
|
```
|
||||||
|
|
||||||
|
mais uniquement parce qu'il est possédé par un namespace explicitement WebSocket :
|
||||||
|
|
||||||
|
```text
|
||||||
|
WsProtocolKind
|
||||||
|
profiles[].ws_endpoints[].kind
|
||||||
|
HeliusLaserStreamWsSession
|
||||||
|
```
|
||||||
|
|
||||||
|
Dans tout contexte où cet ownership n'est pas visible, la désignation durable est **Helius LaserStream WebSocket**.
|
||||||
|
|
||||||
|
Le futur gRPC devra utiliser son propre backend/type/Config et ne pourra jamais être un alias du contrat WS. Son nom exact n'est pas anticipé dans `0.2.8` et sera choisi pendant son audit normatif.
|
||||||
|
|
||||||
|
## 3. Forecast souple corrigé
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.001 DONE — audit Helius actuel + matrice + architecture + threat model + dependencies + sizing
|
||||||
|
fix.001 DONE — séparation des façades standard/Helius + moteur unique + gate Cargo fermé
|
||||||
|
fix.002 DONE — forecast normalisé + namespace LaserStream WebSocket/gRPC clarifié
|
||||||
|
|
||||||
|
pre.002 socle protocolaire : WsProtocolKind::HeliusLaserStream + façades standard/Helius
|
||||||
|
+ connexion physique partagée + guards, sans duplication de l'actor
|
||||||
|
pre.003 surface Helius standard supportée : account/logs/program/root/signature/slot
|
||||||
|
+ absence typée de block/slotsUpdates/vote sur Helius + non-régression standard 9/9
|
||||||
|
pre.004 Config V2 helius_laserstream + schema/fixtures + mapping Config -> Transport
|
||||||
|
+ stratégie de secret Helius et redaction URL
|
||||||
|
pre.005 transactionSubscribe request typed + filters/options/tokenAccounts + transactionUnsubscribe
|
||||||
|
+ bounds 50k + maxSupportedTransactionVersion conditionnel
|
||||||
|
pre.006 transactionNotification + actor integration + reconnect/resubscribe/unsubscribe races
|
||||||
|
+ late notifications + backpressure ciblée
|
||||||
|
pre.007 heartbeat Helius WebSocket/idle + timers + interaction reconnect/control frames/shutdown
|
||||||
|
pre.008 provider adversarial lifecycle + capability guards + payload/backpressure + security/redaction
|
||||||
|
pre.009 compliance Helius WebSocket + non-régressions Solana standard 18/18 + HTTP 52/14
|
||||||
|
+ Config/API/dependency-firewall canaries
|
||||||
|
pre.010 smoke Helius WebSocket live opt-in si stratégie sûre + README/USAGE
|
||||||
|
+ cargo tree direct/duplicates final
|
||||||
|
pre.011 validation workspace finale + fermeture plan/matrice/indexes + prompt 0.2.9
|
||||||
|
rel.001 publication stable stricte
|
||||||
|
```
|
||||||
|
|
||||||
|
Règles : budget nominal d'environ 15–20 minutes par nouvelle prerelease ; fixes insérables sans changer artificiellement le forecast ; split dès qu'une tranche masque plusieurs problèmes indépendants ; `pre.011` n'est pas une deadline.
|
||||||
|
|
||||||
|
## 4. Nomenclature durable WebSocket / gRPC
|
||||||
|
|
||||||
|
Règle adoptée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
contexte typé/config WS visible : HeliusLaserStream / helius_laserstream autorisé
|
||||||
|
prose ou metadata ambiguë : "Helius LaserStream WebSocket" obligatoire
|
||||||
|
future surface gRPC : backend/type/Config distincts ; jamais WsProtocolKind/WsEndpointSettings
|
||||||
|
```
|
||||||
|
|
||||||
|
Conséquences :
|
||||||
|
|
||||||
|
- `HeliusLaserStreamWsSession` reste le nom de façade visé en `0.2.8` ;
|
||||||
|
- `helius_laserstream` reste acceptable comme `kind` sous `ws_endpoints[]` ;
|
||||||
|
- aucune dépendance, protobuf, SDK ou Config gRPC n'est introduite ;
|
||||||
|
- le futur audit gRPC choisira son propre discriminateur sans être contraint par le code court WS ;
|
||||||
|
- la documentation doit toujours qualifier explicitement WebSocket ou gRPC lorsqu'un lecteur pourrait confondre les deux.
|
||||||
|
|
||||||
|
## 5. Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.2.8/pre.001-fix.002.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Fichiers volontairement inchangés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
ROADMAP.md
|
||||||
|
CHANGELOG.md
|
||||||
|
.env.example
|
||||||
|
config/**
|
||||||
|
crates/**
|
||||||
|
docs/000-README.md
|
||||||
|
docs/plans/000-README.md
|
||||||
|
docs/validation/000-README.md
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
deltas/0.2.8/pre.001.md
|
||||||
|
deltas/0.2.8/pre.001-fix.001.md
|
||||||
|
prompts/013-V0_2_8_START_PROMPT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deltas déjà publiés restent immutables.
|
||||||
|
|
||||||
|
## 8. Validations
|
||||||
|
|
||||||
|
Preuves opérateur déjà acquises et inchangées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
baseline v0.2.7 fmt/audit/check/clippy/test workspace = OK
|
||||||
|
cargo tree transport = exécuté/inspecté
|
||||||
|
cargo tree transport --duplicates = exécuté/inspecté
|
||||||
|
```
|
||||||
|
|
||||||
|
Validations du fix documentaire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
comparaison forecast avec plans 0.2.5 / 0.2.7 = effectuée
|
||||||
|
cohérence plan / validation / delta = contrôlée
|
||||||
|
audit Rust workspace après overlay = à exécuter si script disponible
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun gate Cargo supplémentaire n'est créé par ce fix puisque le runtime, le build, la Config et les dépendances restent inchangés.
|
||||||
|
|
||||||
|
## 9. Verdict et prochaine tranche
|
||||||
|
|
||||||
|
```text
|
||||||
|
gate pre.001 = positif
|
||||||
|
Cargo = 0.2.8-pre.1 inchangé
|
||||||
|
architecture = façades séparées, moteur unique
|
||||||
|
forecast = compact, fixes groupés, pre.001 -> pre.011
|
||||||
|
nom WS = HeliusLaserStream / helius_laserstream sous ownership WebSocket
|
||||||
|
futur gRPC = explicitement distinct, hors 0.2.8
|
||||||
|
prochaine tranche = 0.2.8-pre.002
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.002` peut matérialiser le socle protocolaire/façades. Il ne doit pas commencer Config Helius ni `transactionSubscribe`.
|
||||||
268
deltas/0.2.8/pre.001.md
Normal file
268
deltas/0.2.8/pre.001.md
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.001` — audit/sizing Helius LaserStream WebSocket
|
||||||
|
|
||||||
|
## 1. Base requise et vérifiée
|
||||||
|
|
||||||
|
Archive autoritaire fournie :
|
||||||
|
|
||||||
|
```text
|
||||||
|
khadhroony-solana-project-v0.2.7-full-from-gitea.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
État vérifié :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.7
|
||||||
|
deltas/0.2.7/rel.001.md présent
|
||||||
|
prompts/013-V0_2_8_START_PROMPT.md présent
|
||||||
|
metadata .git absente de l'archive
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette livraison ouvre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.8-pre.1
|
||||||
|
commit attendu = v0.2.8-pre.001
|
||||||
|
aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Objet
|
||||||
|
|
||||||
|
`pre.001` est strictement le gate **audit + brainstorming + sizing** de `0.2.8 — Helius LaserStream WebSocket`.
|
||||||
|
|
||||||
|
Il ne modifie :
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun fichier Rust
|
||||||
|
aucun schema/config runtime
|
||||||
|
aucune dependency
|
||||||
|
aucun README/USAGE Transport
|
||||||
|
aucun secret/environment runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
Il crée le plan durable, ouvre la matrice de validation, synchronise les index et recalcule la prévision souple.
|
||||||
|
|
||||||
|
## 3. Baseline acquise
|
||||||
|
|
||||||
|
Preuve opérateur jointe avant ouverture :
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Le sandbox de préparation a aussi exécuté :
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py OK
|
||||||
|
```
|
||||||
|
|
||||||
|
mais ne contient pas `cargo`; il ne déclare donc aucun gate Cargo local réussi.
|
||||||
|
|
||||||
|
Les graphes requis n'étaient pas présents dans le log opérateur et restent à exécuter avant commit :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Résultat de l'audit Helius du 2026-08-23
|
||||||
|
|
||||||
|
Terminologie/endpoints :
|
||||||
|
|
||||||
|
```text
|
||||||
|
produit courant = LaserStream WebSocket
|
||||||
|
Enhanced WebSockets = ancien nom intégré au produit courant
|
||||||
|
mainnet = wss://mainnet.helius-rpc.com/?api-key=...
|
||||||
|
devnet = wss://devnet.helius-rpc.com/?api-key=...
|
||||||
|
api-key = Secret query credential
|
||||||
|
LaserStream gRPC / Gatekeeper beta = hors scope
|
||||||
|
```
|
||||||
|
|
||||||
|
Surface retenue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Helius supporte les paires standard :
|
||||||
|
account, logs, program, root, signature, slot
|
||||||
|
|
||||||
|
Helius ne supporte pas d'après l'index exhaustif :
|
||||||
|
block, slotsUpdates, vote
|
||||||
|
|
||||||
|
Helius extension :
|
||||||
|
transactionSubscribe
|
||||||
|
transactionUnsubscribe
|
||||||
|
notification = transactionNotification
|
||||||
|
```
|
||||||
|
|
||||||
|
La documentation Helius diverge sur `slotsUpdates`; l'index exhaustif `LaserStream WebSocket Methods` le classe explicitement parmi les méthodes unstable non supportées, tandis que `websocket/llms.txt` le place aussi dans une section « stable ». Le gate retient **non supporté** et exige un rejet KSP avant I/O pour `HeliusLaserStream`.
|
||||||
|
|
||||||
|
`transactionSubscribe` expose actuellement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
vote
|
||||||
|
failed
|
||||||
|
signature
|
||||||
|
accountInclude <= 50_000
|
||||||
|
accountExclude <= 50_000
|
||||||
|
accountRequired <= 50_000
|
||||||
|
tokenAccounts = none | balanceChanged | all
|
||||||
|
commitment
|
||||||
|
encoding = base58 | base64 | jsonParsed
|
||||||
|
transactionDetails = full | signatures | accounts | none
|
||||||
|
showRewards
|
||||||
|
maxSupportedTransactionVersion
|
||||||
|
```
|
||||||
|
|
||||||
|
`maxSupportedTransactionVersion` est requis par la référence lorsque `transactionDetails` vaut `accounts` ou `full`.
|
||||||
|
|
||||||
|
`notifyOn` est toujours visible dans les références account/program mais est désormais **deprecated et no-op depuis Agave 4.2**. Il ne sera pas ajouté à KSP.
|
||||||
|
|
||||||
|
La documentation continue de parler d'« enhanced/filtered accountSubscribe » sans publier, dans les références courantes auditées, un wire provider supplémentaire assez exact pour une API typed. Cette capacité est reportée explicitement au lieu d'être inventée.
|
||||||
|
|
||||||
|
## 5. Décisions du gate
|
||||||
|
|
||||||
|
```text
|
||||||
|
WsProtocolKind cible = HeliusLaserStream
|
||||||
|
wire/config string = helius_laserstream
|
||||||
|
provider metadata = helius
|
||||||
|
session actor = WsSession existant, aucun second client
|
||||||
|
new subscription kind = HeliusTransaction
|
||||||
|
provider capability = validation déterministe avant I/O
|
||||||
|
Helius standard support = Account Logs Program Root Signature Slot
|
||||||
|
Helius standard reject = Block SlotsUpdates Vote
|
||||||
|
notifyOn = non exposé
|
||||||
|
tokenAccounts = enum provider typed
|
||||||
|
heartbeat = actor Helius-only, cible 60 s
|
||||||
|
Config = ajout kind V2, même ws_endpoints[]
|
||||||
|
credential = URL résolue par Config derrière WsEndpointUrl
|
||||||
|
new dependency = aucune
|
||||||
|
WebSocket historical replay= aucune promesse
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Threat model retenu
|
||||||
|
|
||||||
|
Points couverts par le plan :
|
||||||
|
|
||||||
|
```text
|
||||||
|
api-key dans query URL et erreurs handshake
|
||||||
|
heartbeat concurrent avec reconnect/close
|
||||||
|
late messages après transactionUnsubscribe
|
||||||
|
remote ids transitoires
|
||||||
|
provider capability mismatch
|
||||||
|
provider RPC errors sans session death automatique
|
||||||
|
transaction payload volumineux
|
||||||
|
3 listes de filtres jusqu'à 50k chacune
|
||||||
|
queue/frame/message bounds
|
||||||
|
unknown provider fields/modes de notification
|
||||||
|
continuity gaps après reconnect
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Forecast recalibré
|
||||||
|
|
||||||
|
Le forecast initial `pre.001 -> pre.011` est resserré car `notifyOn` n'est pas une capacité utile et aucun wire account/program provider additionnel précis n'est actuellement publiable.
|
||||||
|
|
||||||
|
Forecast courant :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.001 audit/sizing/matrice
|
||||||
|
pre.002 protocol descriptor + subscription kind + capability/redaction
|
||||||
|
pre.003 Config V2 helius_laserstream + secret strategy
|
||||||
|
pre.004 transactionSubscribe/unsubscribe request/filter/options
|
||||||
|
pre.005 transactionNotification + reconnect/resubscribe/unsubscribe races
|
||||||
|
pre.006 heartbeat/idle lifecycle
|
||||||
|
pre.007 adversarial provider/security/backpressure
|
||||||
|
pre.008 compliance + WS 18/18 + HTTP 52/14 + Config canaries
|
||||||
|
pre.009 live smoke opt-in si sûr + README/USAGE + cargo graphs
|
||||||
|
pre.010 workspace final + docs/matrix + prompt 0.2.9
|
||||||
|
rel.001 stable
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque tranche vise ~15–20 minutes. Le forecast peut être scindé/étendu si une ambiguïté normative ou une difficulté de lifecycle le justifie. `pre.010` n'est pas une deadline.
|
||||||
|
|
||||||
|
## 8. Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
deltas/0.2.8/pre.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 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
|
||||||
|
```
|
||||||
|
|
||||||
|
`ROADMAP.md` possède déjà l'entrée globale `0.2.8`. Les documents/runtime Config/Transport ne changent pas avant le gate positif.
|
||||||
|
|
||||||
|
## 11. Validations réellement exécutées dans le sandbox de préparation
|
||||||
|
|
||||||
|
Avant modification :
|
||||||
|
|
||||||
|
```text
|
||||||
|
inspection archive/version/rel/prompt OK
|
||||||
|
lecture règles/architecture/plans/validation/code OK
|
||||||
|
réaudit officiel Helius actuel OK
|
||||||
|
inspection versions publiques dépendances OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Après génération de l'overlay, l'audit Python a été réexécuté :
|
||||||
|
|
||||||
|
```text
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. Validations impossibles dans le sandbox
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Cause : binaire `cargo` absent.
|
||||||
|
|
||||||
|
## 13. Validation opérateur requise avant commit
|
||||||
|
|
||||||
|
Appliquer l'overlay puis exécuter :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
`cargo test --workspace` a déjà été fourni vert sur la base `v0.2.7`; `pre.001` ne change aucun code/runtime, mais il peut être rejoué si l'opérateur souhaite un checkpoint complet de la nouvelle version Cargo.
|
||||||
|
|
||||||
|
Le gate `pre.001` ne devient entièrement positif qu'après revue des deux graphes Cargo. Aucun `pre.002` runtime ne doit commencer avant cela.
|
||||||
250
deltas/0.2.8/pre.002-fix.001.md
Normal file
250
deltas/0.2.8/pre.002-fix.001.md
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.002-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.002-fix.001` — Clippy et normalisation documentaire
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Ce correctif ferme le défaut Clippy observé après application de `0.2.8-pre.002` et audite l'organisation des documents actifs `0.2.8` afin d'éviter les sections désordonnées ou les fichiers fourre-tout.
|
||||||
|
|
||||||
|
La livraison `pre.002.md` reste immutable ; ce fix porte uniquement les corrections nouvelles.
|
||||||
|
|
||||||
|
Version workspace :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.2.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Livraison / commit attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.002-fix.001
|
||||||
|
v0.2.8-pre.002-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag prerelease.
|
||||||
|
|
||||||
|
## 2. Clippy : défaut reproduit par l'opérateur
|
||||||
|
|
||||||
|
Le checkpoint opérateur de `pre.002` a donné :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py clean
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets FAIL
|
||||||
|
cargo test -p ksp-onchain-transport-lib OK
|
||||||
|
cargo test --workspace OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Le seul blocage est `clippy::implicit_return` dans le nouveau fichier :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_protocol_session.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Cinq diagnostics sont concernés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
1 retour explicite manquant après la boucle `while let` de `accept_until_close`
|
||||||
|
2 predicates `find` sans `return` explicite
|
||||||
|
2 closures `map` sans `return` explicite
|
||||||
|
```
|
||||||
|
|
||||||
|
Le runtime WebSocket, les façades protocolaires et les tests fonctionnels ne sont pas en échec : le ciblé Transport et le workspace complet passent avant ce fix.
|
||||||
|
|
||||||
|
## 3. Correction Rust
|
||||||
|
|
||||||
|
Le fix ajoute uniquement les retours explicites exigés par la politique Clippy KSP dans la fixture de test.
|
||||||
|
|
||||||
|
Aucun changement n'est apporté à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
WsSession
|
||||||
|
SolanaStandardWsSession
|
||||||
|
HeliusLaserStreamWsSession
|
||||||
|
WsProtocolKind
|
||||||
|
wire WebSocket
|
||||||
|
reconnect/backpressure/shutdown
|
||||||
|
Config
|
||||||
|
transactionSubscribe
|
||||||
|
heartbeat
|
||||||
|
```
|
||||||
|
|
||||||
|
Comme le correctif modifie du code de test consommé par le build, Cargo passe conformément à `VERSION_WORKFLOW.md` de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.2
|
||||||
|
```
|
||||||
|
|
||||||
|
à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.2.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Audit structurel des documents
|
||||||
|
|
||||||
|
Références appliquées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/rules/RULES_DOCUMENTATION.md
|
||||||
|
docs/rules/FILE_CONTRACTS.md
|
||||||
|
docs/plans/000-README.md
|
||||||
|
docs/validation/000-README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Constats :
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/plans : 000-README puis 001..015, ordre cohérent, aucune collision
|
||||||
|
docs/validation : 000-README puis 001..011, ordre cohérent, aucune collision
|
||||||
|
plan 015 : une seule responsabilité, planifier/auditer la release 0.2.8
|
||||||
|
validation 011 : une seule responsabilité, conserver critères/matrices/preuves 0.2.8
|
||||||
|
deltas/0.2.8 : journal de livraison séparé, aucun second changelog dans docs/
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun nouveau répertoire ou fichier documentaire durable n'est nécessaire. Les plans historiques `0.2.5` à `0.2.7` sont de taille comparable ou supérieure ; la taille du plan `015` ne justifie donc pas à elle seule un split.
|
||||||
|
|
||||||
|
Deux défauts d'organisation internes sont toutefois corrigés :
|
||||||
|
|
||||||
|
1. le plan présentait les décisions de façade dans une première section puis des décisions d'architecture détaillées beaucoup plus loin ; elles sont regroupées dans une seule section `Architecture et frontières de protocole` ;
|
||||||
|
2. la validation répétait le forecast détaillé alors que `FILE_CONTRACTS.md` attribue cette responsabilité au plan ; cette duplication est supprimée.
|
||||||
|
|
||||||
|
L'ordre du plan actif devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
état courant
|
||||||
|
forecast souple
|
||||||
|
sources / baseline
|
||||||
|
héritage v0.2.7
|
||||||
|
audit Helius
|
||||||
|
matrice normative
|
||||||
|
architecture et frontières
|
||||||
|
threat model
|
||||||
|
dépendances
|
||||||
|
stratégie de validation / smoke
|
||||||
|
questions reportées
|
||||||
|
critères de split / clôture
|
||||||
|
checkpoint courant
|
||||||
|
```
|
||||||
|
|
||||||
|
La validation reste organisée autour de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
références
|
||||||
|
gate pre.001
|
||||||
|
matrices provider/façades
|
||||||
|
contrat transaction à valider
|
||||||
|
checklists lifecycle/non-régression/sécurité
|
||||||
|
smoke
|
||||||
|
preuves pre.002/fix
|
||||||
|
contrôle structurel docs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. État de validation `pre.002` reporté correctement
|
||||||
|
|
||||||
|
Les preuves opérateur reçues sont intégrées à `docs/validation/011-*` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport unit tests 313 passed
|
||||||
|
Transport public API tests 37 passed
|
||||||
|
release completeness 25 passed
|
||||||
|
doctor tests compile_fail 2 passed
|
||||||
|
cargo test -p Transport OK
|
||||||
|
cargo test --workspace OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Le seul gate restant à revalider après application du fix est la chaîne complète, en particulier Clippy.
|
||||||
|
|
||||||
|
## 6. Fichiers de la livraison
|
||||||
|
|
||||||
|
Nouveau :
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.2.8/pre.002-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Modifiés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_protocol_session.rs
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun autre fichier n'est nécessaire.
|
||||||
|
|
||||||
|
## 7. Validations exécutées dans le sandbox de préparation
|
||||||
|
|
||||||
|
Exécuté après le fix :
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Audit structurel local des documents :
|
||||||
|
|
||||||
|
```text
|
||||||
|
DOC STRUCTURE AUDIT: clean
|
||||||
|
docs/plans = 000..015 ordonné
|
||||||
|
docs/validation = 000..011 ordonné
|
||||||
|
H2/H3 plan actif = cohérent
|
||||||
|
H2/H3 validation = cohérent
|
||||||
|
```
|
||||||
|
|
||||||
|
Le sandbox ne fournit pas Cargo/rustfmt ; aucune commande Cargo n'est donc déclarée réussie pour le fix lui-même.
|
||||||
|
|
||||||
|
## 8. Gate opérateur
|
||||||
|
|
||||||
|
Après application :
|
||||||
|
|
||||||
|
```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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
aucun `clippy::implicit_return`
|
||||||
|
aucun warning Rust
|
||||||
|
313+ tests Transport hérités/pré.002 verts
|
||||||
|
37+ public API verts
|
||||||
|
25+ release completeness verts
|
||||||
|
2 doctests compile_fail verts
|
||||||
|
workspace complet vert
|
||||||
|
```
|
||||||
|
|
||||||
|
Si ce checkpoint est vert, `pre.002` + `pre.002-fix.001` sont `DONE` et `pre.003` peut commencer.
|
||||||
|
|
||||||
|
## 9. Suite
|
||||||
|
|
||||||
|
`pre.003` reste inchangé : ajouter uniquement sur `HeliusLaserStreamWsSession` les six familles standard Helius supportées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
account
|
||||||
|
logs
|
||||||
|
program
|
||||||
|
root
|
||||||
|
signature
|
||||||
|
slot
|
||||||
|
```
|
||||||
|
|
||||||
|
avec réutilisation du wire standard et absence durable de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
block
|
||||||
|
slotsUpdates
|
||||||
|
vote
|
||||||
|
```
|
||||||
|
|
||||||
|
Config Helius, `transactionSubscribe` et heartbeat restent hors `pre.003` conformément au forecast.
|
||||||
278
deltas/0.2.8/pre.002.md
Normal file
278
deltas/0.2.8/pre.002.md
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.002` — Socle protocolaire et façades WebSocket
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Cette tranche matérialise le socle protocolaire décidé par `pre.001-fix.001`/`fix.002` sans commencer Config Helius, `transactionSubscribe` ni le heartbeat provider.
|
||||||
|
|
||||||
|
Version workspace :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Livraison :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.002
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.8-pre.002
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag prerelease.
|
||||||
|
|
||||||
|
## 2. Protocol kind WebSocket
|
||||||
|
|
||||||
|
`WsProtocolKind` contient désormais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
SolanaStandard -> solana_standard
|
||||||
|
HeliusLaserStream -> helius_laserstream
|
||||||
|
```
|
||||||
|
|
||||||
|
Le nom `HeliusLaserStream` appartient explicitement au namespace WebSocket. Cette tranche n'ajoute aucun type, discriminateur ou backend LaserStream gRPC.
|
||||||
|
|
||||||
|
## 3. Deux façades, un seul moteur physique
|
||||||
|
|
||||||
|
Nouvelles façades publiques :
|
||||||
|
|
||||||
|
```text
|
||||||
|
SolanaStandardWsSession
|
||||||
|
HeliusLaserStreamWsSession
|
||||||
|
```
|
||||||
|
|
||||||
|
Elles contiennent un `WsSession` privé et délèguent toutes les opérations physiques au moteur acquis en `0.2.7`.
|
||||||
|
|
||||||
|
Le chemin partagé est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
facade::connect
|
||||||
|
-> WsSession::connect_for_protocol # crate-internal guard
|
||||||
|
-> WsSession::connect_physical # unique physical constructor
|
||||||
|
-> tokio::spawn(run_ws_session_actor)
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun second :
|
||||||
|
|
||||||
|
```text
|
||||||
|
socket type
|
||||||
|
actor
|
||||||
|
WsSessionCommand
|
||||||
|
pending registry
|
||||||
|
subscription registry
|
||||||
|
reconnect loop
|
||||||
|
backpressure path
|
||||||
|
shutdown path
|
||||||
|
snapshot model
|
||||||
|
```
|
||||||
|
|
||||||
|
n'est créé.
|
||||||
|
|
||||||
|
## 4. Compatibilité `WsSession` historique
|
||||||
|
|
||||||
|
`WsSession::connect` reste public pour les consommateurs `0.2.7`, mais devient explicitement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
standard-only
|
||||||
|
```
|
||||||
|
|
||||||
|
Un endpoint `WsProtocolKind::HeliusLaserStream` présenté à ce constructeur est rejeté avant toute I/O avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ERROR_CODE_INVALID_SETTINGS
|
||||||
|
field = ws_endpoints.protocol
|
||||||
|
expected_protocol = solana_standard
|
||||||
|
actual_protocol = helius_laserstream
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces contextes sont des descriptors sûrs ; aucune URL ou credential n'est copiée.
|
||||||
|
|
||||||
|
La façade Helius n'expose ni `inner()` ni `into_inner()` et ne permet donc pas de récupérer un `WsSession` générique afin de contourner sa surface provider-specific.
|
||||||
|
|
||||||
|
## 5. Surface standard dans la nouvelle façade
|
||||||
|
|
||||||
|
`SolanaStandardWsSession` délègue immédiatement les neuf wrappers standard acquis, sans dupliquer leur wire ou leurs decoders :
|
||||||
|
|
||||||
|
```text
|
||||||
|
account_subscribe
|
||||||
|
block_subscribe
|
||||||
|
logs_subscribe
|
||||||
|
program_subscribe
|
||||||
|
root_subscribe
|
||||||
|
signature_subscribe
|
||||||
|
slot_subscribe
|
||||||
|
slots_updates_subscribe
|
||||||
|
vote_subscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
Les méthodes historiques correspondantes restent aussi disponibles sur `WsSession` pour compatibilité.
|
||||||
|
|
||||||
|
## 6. Surface Helius volontairement minimale dans `pre.002`
|
||||||
|
|
||||||
|
`HeliusLaserStreamWsSession` expose uniquement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
connect
|
||||||
|
id
|
||||||
|
snapshot
|
||||||
|
state
|
||||||
|
close
|
||||||
|
```
|
||||||
|
|
||||||
|
Elle n'expose encore aucune subscription. Cela garde la tranche sur le socle et réserve à `pre.003` l'ajout contrôlé des six familles Helius documentées comme compatibles avec le wire standard :
|
||||||
|
|
||||||
|
```text
|
||||||
|
account
|
||||||
|
logs
|
||||||
|
program
|
||||||
|
root
|
||||||
|
signature
|
||||||
|
slot
|
||||||
|
```
|
||||||
|
|
||||||
|
`block`, `slotsUpdates` et `vote` restent absents. Des rustdocs `compile_fail` verrouillent dès cette tranche l'absence de `block_subscribe` et de `into_inner`.
|
||||||
|
|
||||||
|
## 7. Tests et canaries ajoutés
|
||||||
|
|
||||||
|
Unitaires Transport :
|
||||||
|
|
||||||
|
```text
|
||||||
|
WsProtocolKind expose deux descriptors distincts
|
||||||
|
les deux façades ouvrent et ferment un WebSocket contre un peer local
|
||||||
|
snapshot de chaque façade conserve le protocol kind attendu
|
||||||
|
WsSession::connect rejette Helius avant I/O
|
||||||
|
chaque façade rejette le mauvais protocol kind avant I/O
|
||||||
|
Debug Helius ne projette pas une api-key canary présente dans l'URL
|
||||||
|
module de façade sans second tokio::spawn / tokio_tungstenite / WsSessionCommand
|
||||||
|
module de façade sans getter inner/into_inner public
|
||||||
|
```
|
||||||
|
|
||||||
|
Public API :
|
||||||
|
|
||||||
|
```text
|
||||||
|
WsProtocolKind::HeliusLaserStream visible au crate-root
|
||||||
|
SolanaStandardWsSession visible au crate-root
|
||||||
|
HeliusLaserStreamWsSession visible au crate-root
|
||||||
|
9 wrappers standard accessibles via SolanaStandardWsSession
|
||||||
|
release-completeness conserve les 9 kinds standard et les deux descripteurs protocolaires
|
||||||
|
WsSession historique toujours visible
|
||||||
|
```
|
||||||
|
|
||||||
|
Les tests `compile_fail` de la rustdoc couvrent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HeliusLaserStreamWsSession::block_subscribe absent
|
||||||
|
HeliusLaserStreamWsSession::into_inner absent
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Hors périmètre préservé
|
||||||
|
|
||||||
|
Cette tranche ne modifie pas :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-config-lib
|
||||||
|
config/std.transport.json
|
||||||
|
config/schemas/std.transport.schema.json
|
||||||
|
.env.example
|
||||||
|
WsSubscriptionKind
|
||||||
|
transactionSubscribe / transactionUnsubscribe
|
||||||
|
notification transaction Helius
|
||||||
|
heartbeat/idle timer
|
||||||
|
HTTP 52/14
|
||||||
|
Store / Program / Wallet
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dépendance Rust n'est ajoutée.
|
||||||
|
|
||||||
|
## 9. Fichiers de la livraison
|
||||||
|
|
||||||
|
Nouveaux :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_protocol_session.rs
|
||||||
|
deltas/0.2.8/pre.002.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Modifiés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_session.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_settings.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Validation exécutée dans le sandbox de préparation
|
||||||
|
|
||||||
|
Exécuté après modification :
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Le sandbox de préparation ne fournit pas `cargo`/`rustfmt`. Les commandes compilées ne sont donc pas déclarées réussies ici.
|
||||||
|
|
||||||
|
## 11. Gates opérateur avant commit
|
||||||
|
|
||||||
|
Après application de l'overlay :
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis, si le ciblé est vert :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Points à surveiller spécifiquement dans la sortie :
|
||||||
|
|
||||||
|
```text
|
||||||
|
les deux doctests compile_fail doivent réussir
|
||||||
|
aucun warning missing_docs/unreachable_pub
|
||||||
|
aucune régression des 309+ tests Transport hérités
|
||||||
|
aucune régression Config causée par l'ajout de la variante non_exhaustive
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. Suite
|
||||||
|
|
||||||
|
`0.2.8-pre.003` doit ajouter uniquement sur `HeliusLaserStreamWsSession` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
account_subscribe
|
||||||
|
logs_subscribe
|
||||||
|
program_subscribe
|
||||||
|
root_subscribe
|
||||||
|
signature_subscribe
|
||||||
|
slot_subscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
avec réutilisation exacte des DTOs/wire standard, puis prouver durablement que :
|
||||||
|
|
||||||
|
```text
|
||||||
|
block_subscribe absent
|
||||||
|
slots_updates_subscribe absent
|
||||||
|
vote_subscribe absent
|
||||||
|
```
|
||||||
|
|
||||||
|
La tranche `pre.003` ne doit toujours pas commencer Config Helius ni `transactionSubscribe`.
|
||||||
259
deltas/0.2.8/pre.003.md
Normal file
259
deltas/0.2.8/pre.003.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.003.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.003` — Surface standard Helius WebSocket
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Cette tranche expose sur `HeliusLaserStreamWsSession` uniquement les six familles WebSocket standard que l'audit Helius courant classe comme supportées, sans ajouter de wire provider parallèle et sans ouvrir encore Config Helius, `transactionSubscribe` ou le heartbeat.
|
||||||
|
|
||||||
|
Le checkpoint opérateur de `pre.002-fix.001` est intégralement vert ; aucune dette de gate n'est reportée dans cette tranche.
|
||||||
|
|
||||||
|
Version workspace :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Livraison / commit attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.003
|
||||||
|
v0.2.8-pre.003
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag prerelease.
|
||||||
|
|
||||||
|
## 2. Surface Helius ajoutée
|
||||||
|
|
||||||
|
`HeliusLaserStreamWsSession` expose désormais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
account_subscribe
|
||||||
|
program_subscribe
|
||||||
|
logs_subscribe
|
||||||
|
signature_subscribe
|
||||||
|
slot_subscribe
|
||||||
|
root_subscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
Chaque wrapper délègue au `WsSession` physique partagé et réutilise exactement les DTOs, encoders, decoders, `WsSubscriptionKind` et méthodes d'unsubscribe standard acquis en `0.2.7`.
|
||||||
|
|
||||||
|
Aucun type suivant n'est créé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HeliusAccountSubscribeConfig
|
||||||
|
HeliusAccountNotification
|
||||||
|
HeliusProgramSubscribeConfig
|
||||||
|
HeliusProgramNotification
|
||||||
|
HeliusLogsSubscribeFilter
|
||||||
|
HeliusLogsNotification
|
||||||
|
HeliusSignatureSubscribeConfig
|
||||||
|
HeliusSignatureNotification
|
||||||
|
HeliusSlotNotification
|
||||||
|
HeliusRootNotification
|
||||||
|
```
|
||||||
|
|
||||||
|
L'absence de ces copies est volontaire : aucun écart de wire Helius courant ne les justifie.
|
||||||
|
|
||||||
|
## 3. Surface Helius explicitement absente
|
||||||
|
|
||||||
|
La façade Helius continue de ne pas exposer :
|
||||||
|
|
||||||
|
```text
|
||||||
|
block_subscribe
|
||||||
|
slots_updates_subscribe
|
||||||
|
vote_subscribe
|
||||||
|
transaction_subscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
Les trois familles standard non supportées sont verrouillées par des rustdocs `compile_fail` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
blockSubscribe
|
||||||
|
slotsUpdatesSubscribe
|
||||||
|
voteSubscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
`transactionSubscribe` reste réservé à la tranche provider-specific dédiée du forecast.
|
||||||
|
|
||||||
|
## 4. Organisation du code
|
||||||
|
|
||||||
|
`ws_protocol_session.rs` reste limité au lifecycle des façades et à l'accès crate-private au moteur partagé. Les wrappers sont rangés auprès du propriétaire de leur wire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ws_accounts.rs
|
||||||
|
SolanaStandardWsSession : account / program
|
||||||
|
HeliusLaserStreamWsSession : account / program
|
||||||
|
|
||||||
|
ws_transactions.rs
|
||||||
|
SolanaStandardWsSession : logs / signature
|
||||||
|
HeliusLaserStreamWsSession : logs / signature
|
||||||
|
|
||||||
|
ws_cluster.rs
|
||||||
|
SolanaStandardWsSession : root / slot / slotsUpdates / vote
|
||||||
|
HeliusLaserStreamWsSession : root / slot
|
||||||
|
|
||||||
|
ws_blocks.rs
|
||||||
|
SolanaStandardWsSession : block uniquement
|
||||||
|
|
||||||
|
ws_protocol_session.rs
|
||||||
|
connect / id / snapshot / state / close
|
||||||
|
physical_session() crate-private
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette répartition évite de transformer le module de façade en fichier fourre-tout tout en conservant un seul actor/socket/registry.
|
||||||
|
|
||||||
|
Aucun accès public `inner()` / `into_inner()` / `physical_session()` n'est ajouté.
|
||||||
|
|
||||||
|
## 5. Fixture Helius standard
|
||||||
|
|
||||||
|
Une fixture locale dédiée couvre les six familles contre un peer WebSocket local Helius-typed.
|
||||||
|
|
||||||
|
Elle vérifie pour chaque paire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
nom subscribe exact
|
||||||
|
params exacts
|
||||||
|
remote subscription id
|
||||||
|
nom unsubscribe exact
|
||||||
|
params [remote_id]
|
||||||
|
result true
|
||||||
|
```
|
||||||
|
|
||||||
|
Couverture :
|
||||||
|
|
||||||
|
```text
|
||||||
|
accountSubscribe / accountUnsubscribe
|
||||||
|
programSubscribe / programUnsubscribe
|
||||||
|
logsSubscribe / logsUnsubscribe
|
||||||
|
signatureSubscribe / signatureUnsubscribe
|
||||||
|
slotSubscribe / slotUnsubscribe
|
||||||
|
rootSubscribe / rootUnsubscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
Les tests standard historiques restent propriétaires du décodage exact des notifications ; la nouvelle fixture prouve que la façade Helius traverse le même wire plutôt que de créer une seconde pile de décodage.
|
||||||
|
|
||||||
|
## 6. Canaries publiques et release completeness
|
||||||
|
|
||||||
|
Ajouts :
|
||||||
|
|
||||||
|
```text
|
||||||
|
public API : les six wrappers Helius sont accessibles depuis le crate-root
|
||||||
|
public API : les types de paramètres/résultats restent les types Solana partagés
|
||||||
|
release completeness : surface Helius courante = six familles standard, avant extension transaction
|
||||||
|
source canary : ws_protocol_session.rs ne redevient pas propriétaire des wrappers métier
|
||||||
|
```
|
||||||
|
|
||||||
|
Les neuf familles de `SolanaStandardWsSession` et les 18 opérations standard acquises restent inchangées.
|
||||||
|
|
||||||
|
## 7. Hors périmètre préservé
|
||||||
|
|
||||||
|
Cette tranche ne modifie pas :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-config-lib
|
||||||
|
config/std.transport.json
|
||||||
|
config/schemas/std.transport.schema.json
|
||||||
|
.env.example
|
||||||
|
WsSubscriptionKind
|
||||||
|
transactionSubscribe / transactionUnsubscribe
|
||||||
|
notification transaction Helius
|
||||||
|
heartbeat/idle timer
|
||||||
|
HTTP 52/14
|
||||||
|
Store / Program / Wallet
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dépendance Rust n'est ajoutée.
|
||||||
|
|
||||||
|
## 8. Preuve opérateur héritée avant ouverture
|
||||||
|
|
||||||
|
Le checkpoint fourni après `pre.002-fix.001` est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py clean
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test -p ksp-onchain-transport-lib OK
|
||||||
|
cargo test --workspace OK
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.002` + `pre.002-fix.001` sont donc considérés `DONE` avant ce delta.
|
||||||
|
|
||||||
|
## 9. Fichiers de la livraison
|
||||||
|
|
||||||
|
Nouveau :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_helius_standard.rs
|
||||||
|
deltas/0.2.8/pre.003.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Modifiés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_accounts.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_blocks.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_cluster.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_transactions.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_protocol_session.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Validation exécutée dans le sandbox de préparation
|
||||||
|
|
||||||
|
Exécuté après modification :
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
Le sandbox ne fournit pas Cargo/rustfmt ; les nouvelles fixtures et doctests ne sont donc pas déclarés compilés avant le checkpoint opérateur.
|
||||||
|
|
||||||
|
## 11. Gate opérateur
|
||||||
|
|
||||||
|
Après application :
|
||||||
|
|
||||||
|
```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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
nouvelle fixture Helius six familles verte
|
||||||
|
4 doctests compile_fail de HeliusLaserStreamWsSession verts
|
||||||
|
public API pre.003 verte
|
||||||
|
release completeness pre.003 verte
|
||||||
|
aucun warning/clippy
|
||||||
|
standard WebSocket 9/9 non régressé
|
||||||
|
workspace complet vert
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. Suite
|
||||||
|
|
||||||
|
Si le checkpoint est vert, `pre.004` ouvre uniquement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Config V2 kind = helius_laserstream
|
||||||
|
schema / fixtures
|
||||||
|
mapping Config -> Transport
|
||||||
|
secret Helius dans URL résolue par Config
|
||||||
|
redaction api-key
|
||||||
|
```
|
||||||
|
|
||||||
|
`transactionSubscribe` reste hors `pre.004` conformément au forecast courant.
|
||||||
182
deltas/0.2.8/pre.004-fix.001.md
Normal file
182
deltas/0.2.8/pre.004-fix.001.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.004-fix.001.md -->
|
||||||
|
<!-- version: 2 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.004-fix.001` — redaction Helius Config + couverture Devnet
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Ce fix corrige deux défauts circonscrits de `0.2.8-pre.004` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. faux négatif du nouveau canari de redaction `safe_value` Helius ;
|
||||||
|
2. représentation déterministe incomplète : mainnet était matérialisé, Devnet ne l'était pas encore.
|
||||||
|
```
|
||||||
|
|
||||||
|
Le résolveur Config, le mapping Config -> Transport et le moteur WebSocket restent inchangés.
|
||||||
|
|
||||||
|
Version workspace :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.4.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Livraison / commit attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.004-fix.001
|
||||||
|
v0.2.8-pre.004-fix.001
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag prerelease.
|
||||||
|
|
||||||
|
## 2. Preuve opérateur ayant déclenché le fix
|
||||||
|
|
||||||
|
Le checkpoint `pre.004` reçu le 2026-08-23 donne :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py clean
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test -p ksp-config-lib FAIL 109/110
|
||||||
|
cargo test -p ksp-onchain-transport-lib OK
|
||||||
|
cargo test --workspace FAIL sur le même test Config
|
||||||
|
```
|
||||||
|
|
||||||
|
L'échec exact est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
observé = wss://mainnet.helius-rpc.com/?api-key=********
|
||||||
|
attendu = ********
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test avait déjà prouvé avant cette assertion que le schema accepte `helius_laserstream`, que l'adapter produit `WsProtocolKind::HeliusLaserStream` et que l'URL runtime contient la clé Helius résolue.
|
||||||
|
|
||||||
|
## 3. Redaction Config confirmée
|
||||||
|
|
||||||
|
Le contrat historique Config est segmentaire pour une chaîne composée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
source composée safe_value
|
||||||
|
https://rpc.example/?token=${SECRET} -> https://rpc.example/?token=********
|
||||||
|
token=${SECRET} -> token=********
|
||||||
|
```
|
||||||
|
|
||||||
|
La projection sûre conserve donc les littéraux non sensibles et masque uniquement le segment secret.
|
||||||
|
|
||||||
|
Pour Helius :
|
||||||
|
|
||||||
|
```text
|
||||||
|
mainnet runtime = wss://mainnet.helius-rpc.com/?api-key=<clé réelle>
|
||||||
|
mainnet safe_value = wss://mainnet.helius-rpc.com/?api-key=********
|
||||||
|
|
||||||
|
devnet runtime = wss://devnet.helius-rpc.com/?api-key=<clé réelle>
|
||||||
|
devnet safe_value = wss://devnet.helius-rpc.com/?api-key=********
|
||||||
|
```
|
||||||
|
|
||||||
|
La clé réelle doit rester absente de `safe_value` et de toutes les représentations `Debug`.
|
||||||
|
|
||||||
|
## 4. Couverture Helius Devnet ajoutée
|
||||||
|
|
||||||
|
La documentation Helius WebSocket actuelle expose un endpoint unifié par réseau :
|
||||||
|
|
||||||
|
```text
|
||||||
|
mainnet wss://mainnet.helius-rpc.com/?api-key=<api-key>
|
||||||
|
devnet wss://devnet.helius-rpc.com/?api-key=<api-key>
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.004` avait documenté les deux réseaux dans le plan mais n'avait matérialisé que mainnet dans l'exemple et la fixture Config. Le fix complète ce manque.
|
||||||
|
|
||||||
|
Décision de structure :
|
||||||
|
|
||||||
|
```text
|
||||||
|
mainnet et devnet vivent dans des profils Config distincts ;
|
||||||
|
un même profil logique ne mélange pas les deux clusters ;
|
||||||
|
la même variable KSP_SECRET_HELIUS_API_KEY peut alimenter les deux URLs ;
|
||||||
|
config/std.transport.json reste standard-only.
|
||||||
|
```
|
||||||
|
|
||||||
|
L'exemple versionné ajoute un profil `devnet_helius`. La fixture Config ajoute un profil `helius_devnet` et le test charge explicitement ce profil en plus du profil mainnet par défaut.
|
||||||
|
|
||||||
|
## 5. Correction du test
|
||||||
|
|
||||||
|
Le canari devient :
|
||||||
|
|
||||||
|
```text
|
||||||
|
helius_laserstream_mainnet_and_devnet_api_key_map_to_protocol_and_safe_redaction
|
||||||
|
```
|
||||||
|
|
||||||
|
Il vérifie pour **mainnet et devnet** :
|
||||||
|
|
||||||
|
```text
|
||||||
|
provider = helius
|
||||||
|
cluster exact
|
||||||
|
protocol = WsProtocolKind::HeliusLaserStream
|
||||||
|
URL runtime exacte avec la clé résolue
|
||||||
|
safe_value exact avec segment ********
|
||||||
|
provenance = KSP_SECRET_HELIUS_API_KEY / Process
|
||||||
|
Debug sans la clé réelle
|
||||||
|
composition avec HeliusLaserStreamWsSession au moins sur le profil Devnet dédié
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Invariants non modifiés
|
||||||
|
|
||||||
|
Aucune modification n'est apportée à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ConfigEnvironment / moteur de résolution
|
||||||
|
sensitivity / provenance semantics
|
||||||
|
JSON Schema Transport V2
|
||||||
|
mapping helius_laserstream -> HeliusLaserStream
|
||||||
|
WsEndpointUrl
|
||||||
|
WsSession / façades / actor
|
||||||
|
config/std.transport.json canonique
|
||||||
|
transactionSubscribe
|
||||||
|
heartbeat
|
||||||
|
new dependency
|
||||||
|
```
|
||||||
|
|
||||||
|
`.env.example` conserve une seule variable `KSP_SECRET_HELIUS_API_KEY`, suffisante pour les deux endpoints Helius.
|
||||||
|
|
||||||
|
## 7. Fichiers de la livraison
|
||||||
|
|
||||||
|
Nouveau :
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.2.8/pre.004-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Modifiés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
config/examples/std.transport.example.json
|
||||||
|
crates/ksp-config-lib/unit_tests/fixtures/std.transport.json
|
||||||
|
crates/ksp-config-lib/unit_tests/transport.rs
|
||||||
|
crates/ksp-config-lib/USAGE.md
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Inchangés volontairement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
config/std.transport.json
|
||||||
|
config/schemas/std.transport.schema.json
|
||||||
|
.env.example
|
||||||
|
Transport WebSocket runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Validation à rejouer
|
||||||
|
|
||||||
|
```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-onchain-transport-lib
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.004` + `fix.001` restent non fermés jusqu'à réception de ce checkpoint intégralement vert.
|
||||||
139
deltas/0.2.8/pre.004-fix.002.md
Normal file
139
deltas/0.2.8/pre.004-fix.002.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.004-fix.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.004-fix.002` — provenance Helius Config composée
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Ce second fix de `pre.004` corrige uniquement une hypothèse erronée du canari Config Helius introduit par `pre.004-fix.001`.
|
||||||
|
|
||||||
|
Le checkpoint opérateur prouve que :
|
||||||
|
|
||||||
|
```text
|
||||||
|
fmt / audit / check / clippy verts
|
||||||
|
Transport vert
|
||||||
|
Config 109/110
|
||||||
|
échec provenance.len() observé = 2, attendu = 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le runtime Config, le schema, les profils Helius mainnet/devnet et le mapping Config -> Transport sont corrects et ne sont pas modifiés.
|
||||||
|
|
||||||
|
Version workspace :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.4.fix.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Livraison / commit attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.004-fix.002
|
||||||
|
v0.2.8-pre.004-fix.002
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag prerelease.
|
||||||
|
|
||||||
|
## 2. Cause exacte
|
||||||
|
|
||||||
|
L'URL Helius est une chaîne composée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-...}
|
||||||
|
wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-...}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le modèle de provenance Config distingue les segments littéraux et les substitutions d'environnement.
|
||||||
|
|
||||||
|
Avec une valeur fournie par le process, la provenance correcte est donc :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0 = ConfigValueProvenance::DocumentLiteral
|
||||||
|
1 = ConfigValueProvenance::EnvironmentProcess {
|
||||||
|
variable_name = KSP_SECRET_HELIUS_API_KEY
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test de `fix.001` attendait à tort un seul segment, comme pour une valeur constituée uniquement d'un placeholder.
|
||||||
|
|
||||||
|
## 3. Correction
|
||||||
|
|
||||||
|
Le canari :
|
||||||
|
|
||||||
|
```text
|
||||||
|
helius_laserstream_mainnet_and_devnet_api_key_map_to_protocol_and_safe_redaction
|
||||||
|
```
|
||||||
|
|
||||||
|
attend désormais, pour mainnet et devnet :
|
||||||
|
|
||||||
|
```text
|
||||||
|
provenance.len() = 2
|
||||||
|
provenance[0] = DocumentLiteral
|
||||||
|
provenance[1].environment_source() = Process
|
||||||
|
provenance[1].variable_name() = KSP_SECRET_HELIUS_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
Les assertions déjà présentes restent inchangées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
provider = helius
|
||||||
|
cluster = mainnet-beta / devnet
|
||||||
|
protocol = WsProtocolKind::HeliusLaserStream
|
||||||
|
runtime URL = vraie clé résolue
|
||||||
|
safe_value = URL avec api-key=********
|
||||||
|
Debug = aucune clé réelle
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Invariants non modifiés
|
||||||
|
|
||||||
|
Aucune modification n'est apportée à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ConfigEnvironment
|
||||||
|
ResolvedConfigJson / provenance implementation
|
||||||
|
sensitivity / redaction
|
||||||
|
config/schemas/std.transport.schema.json
|
||||||
|
config/std.transport.json
|
||||||
|
config/examples/std.transport.example.json
|
||||||
|
fixture std.transport.json
|
||||||
|
mapping helius_laserstream -> HeliusLaserStream
|
||||||
|
profils Helius mainnet/devnet
|
||||||
|
KSP_SECRET_HELIUS_API_KEY
|
||||||
|
WsEndpointUrl
|
||||||
|
WsSession / façades / actor
|
||||||
|
transactionSubscribe
|
||||||
|
heartbeat
|
||||||
|
dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.004-fix.001.md` reste immutable.
|
||||||
|
|
||||||
|
## 5. Fichiers de la livraison
|
||||||
|
|
||||||
|
Nouveau :
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.2.8/pre.004-fix.002.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Modifiés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-config-lib/unit_tests/transport.rs
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Validation à rejouer
|
||||||
|
|
||||||
|
```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-onchain-transport-lib
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.005` reste fermé jusqu'à réception de ce checkpoint intégralement vert.
|
||||||
174
deltas/0.2.8/pre.004.md
Normal file
174
deltas/0.2.8/pre.004.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.004.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.004` — Config V2 Helius LaserStream WebSocket
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Cette tranche ouvre Config V2 au protocole WebSocket `helius_laserstream`, matérialise son mapping vers `WsProtocolKind::HeliusLaserStream` et prouve la résolution/redaction de la clé Helius sans modifier le moteur WebSocket ni commencer `transactionSubscribe`.
|
||||||
|
|
||||||
|
Le checkpoint opérateur de `pre.003` est intégralement vert.
|
||||||
|
|
||||||
|
Version workspace :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.4
|
||||||
|
```
|
||||||
|
|
||||||
|
Livraison / commit attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.004
|
||||||
|
v0.2.8-pre.004
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag prerelease.
|
||||||
|
|
||||||
|
## 2. Discriminateur Config WebSocket
|
||||||
|
|
||||||
|
Le JSON Schema V2 accepte désormais exactement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
solana_standard
|
||||||
|
helius_laserstream
|
||||||
|
```
|
||||||
|
|
||||||
|
L'adapter `ksp-config-lib` mappe :
|
||||||
|
|
||||||
|
```text
|
||||||
|
solana_standard -> WsProtocolKind::SolanaStandard
|
||||||
|
helius_laserstream -> WsProtocolKind::HeliusLaserStream
|
||||||
|
```
|
||||||
|
|
||||||
|
Le discriminateur reste possédé par `profiles[].ws_endpoints[].kind`; aucun alias ou contrat gRPC n'est introduit.
|
||||||
|
|
||||||
|
## 3. Fixture et exemple Helius
|
||||||
|
|
||||||
|
La fixture `ksp-config-lib` ajoute un second endpoint WebSocket :
|
||||||
|
|
||||||
|
```text
|
||||||
|
name = fixture_helius_ws
|
||||||
|
provider = helius
|
||||||
|
cluster = mainnet-beta
|
||||||
|
kind = helius_laserstream
|
||||||
|
url = wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-fixture-helius-key}
|
||||||
|
```
|
||||||
|
|
||||||
|
`config/examples/std.transport.example.json` remplace l'ancien second endpoint WebSocket provider générique par un exemple Helius explicite :
|
||||||
|
|
||||||
|
```text
|
||||||
|
name = mainnet_helius_ws
|
||||||
|
provider = helius
|
||||||
|
kind = helius_laserstream
|
||||||
|
url = wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le document canonique `config/std.transport.json` reste volontairement inchangé et standard-only : la configuration par défaut ne doit pas embarquer un endpoint provider nécessitant une credential factice.
|
||||||
|
|
||||||
|
## 4. Secret Helius et redaction
|
||||||
|
|
||||||
|
`.env.example` inventorie :
|
||||||
|
|
||||||
|
```text
|
||||||
|
KSP_SECRET_HELIUS_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
sous forme commentée avec placeholder non secret.
|
||||||
|
|
||||||
|
Un nouveau test Config injecte une clé canari et prouve :
|
||||||
|
|
||||||
|
```text
|
||||||
|
l'URL runtime contient la clé résolue
|
||||||
|
le protocol runtime vaut HeliusLaserStream
|
||||||
|
la projection safe_value redige /ws_endpoints/1/url
|
||||||
|
ResolvedTransportConfig Debug ne contient pas la clé
|
||||||
|
```
|
||||||
|
|
||||||
|
Config reste l'unique propriétaire de l'environnement. Transport ne lit pas `std::env`.
|
||||||
|
|
||||||
|
## 5. Composition Config -> Transport
|
||||||
|
|
||||||
|
La fixture principale vérifie maintenant deux endpoints WebSocket :
|
||||||
|
|
||||||
|
```text
|
||||||
|
fixture_private_ws -> SolanaStandard
|
||||||
|
fixture_helius_ws -> HeliusLaserStream
|
||||||
|
```
|
||||||
|
|
||||||
|
Le second endpoint est également passé au constructeur public `HeliusLaserStreamWsSession::connect` sans polling afin de verrouiller la compatibilité de types Config -> Transport. Les guards runtime du protocole restent propriétaires de Transport.
|
||||||
|
|
||||||
|
## 6. Documentation ciblée
|
||||||
|
|
||||||
|
`ksp-config-lib/USAGE.md` documente les deux valeurs `kind`, le namespace WebSocket du discriminateur Helius et l'ownership/redaction de `KSP_SECRET_HELIUS_API_KEY`.
|
||||||
|
|
||||||
|
Le plan reste propriétaire du forecast ; la validation reste propriétaire des critères et preuves. Aucun nouveau document/fichier fourre-tout n'est créé.
|
||||||
|
|
||||||
|
## 7. Hors périmètre préservé
|
||||||
|
|
||||||
|
Cette tranche ne modifie pas :
|
||||||
|
|
||||||
|
```text
|
||||||
|
config/std.transport.json
|
||||||
|
WsSession / actor / reconnect / queues
|
||||||
|
les six wrappers Helius standard acquis en pre.003
|
||||||
|
WsSubscriptionKind
|
||||||
|
transactionSubscribe / transactionUnsubscribe
|
||||||
|
transactionNotification
|
||||||
|
heartbeat provider
|
||||||
|
HTTP 52/14
|
||||||
|
Wallet / Store / Program
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dépendance Rust n'est ajoutée.
|
||||||
|
|
||||||
|
## 8. Preuve opérateur héritée
|
||||||
|
|
||||||
|
Le checkpoint `pre.003` fourni le 2026-08-23 est :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py clean
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test -p ksp-onchain-transport-lib OK
|
||||||
|
cargo test --workspace OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Les preuves spécifiques Helius standard sont également vertes : 314 tests unitaires Transport, 38 public API, 26 release-completeness et 4 doctests compile-fail.
|
||||||
|
|
||||||
|
## 9. Fichiers de la livraison
|
||||||
|
|
||||||
|
Nouveau :
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.2.8/pre.004.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Modifiés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
.env.example
|
||||||
|
config/examples/std.transport.example.json
|
||||||
|
config/schemas/std.transport.schema.json
|
||||||
|
crates/ksp-config-lib/src/transport.rs
|
||||||
|
crates/ksp-config-lib/unit_tests/fixtures/std.transport.json
|
||||||
|
crates/ksp-config-lib/unit_tests/transport.rs
|
||||||
|
crates/ksp-config-lib/USAGE.md
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Validation à exécuter
|
||||||
|
|
||||||
|
```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-onchain-transport-lib
|
||||||
|
cargo test --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.004` reste `PREPARED` jusqu'à réception de ce checkpoint.
|
||||||
95
deltas/0.2.8/pre.005-fix.001.md
Normal file
95
deltas/0.2.8/pre.005-fix.001.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.005-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.005-fix.001` — canaris JSON, visibilité test/private et audit des chemins
|
||||||
|
|
||||||
|
## 1. Cause
|
||||||
|
|
||||||
|
Le checkpoint opérateur de `pre.005` compile le workspace normal mais échoue dès la compilation des tests Transport. Quatre assertions comparent un `Vec<serde_json::Value>` produit par les encodeurs de params à un `serde_json::Value` construit par `serde_json::json!([...])`, ce qui produit `E0277`.
|
||||||
|
|
||||||
|
Le même checkpoint révèle cinq warnings `unused import` au crate-root : cinq helpers Helius avaient été rendus `pub(crate)` et réexportés uniquement pour être appelés par les tests. Cette visibilité est contraire aux règles KSP : une visibilité n'est pas élargie pour les tests.
|
||||||
|
|
||||||
|
## 2. Correction des canaris
|
||||||
|
|
||||||
|
Les attentes de params utilisent maintenant des `Vec<Value>` explicites :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transactionSubscribe complet
|
||||||
|
omission complète
|
||||||
|
états []/none explicites
|
||||||
|
transactionUnsubscribe [remote_id]
|
||||||
|
```
|
||||||
|
|
||||||
|
Le wire attendu ne change pas.
|
||||||
|
|
||||||
|
## 3. Correction de visibilité
|
||||||
|
|
||||||
|
Les helpers suivants redeviennent strictement privés au module `ws_helius_transactions` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
helius_transaction_subscribe_params
|
||||||
|
helius_transaction_subscribe_method
|
||||||
|
helius_transaction_unsubscribe_method
|
||||||
|
decode_helius_transaction_subscribe_result
|
||||||
|
helius_transaction_unsubscribe_params
|
||||||
|
decode_helius_transaction_unsubscribe_result
|
||||||
|
```
|
||||||
|
|
||||||
|
Les cinq réexports `pub(crate)` de `lib.rs` sont supprimés. Les tests séparés y accèdent via `super::...`. Aucun `#[allow(dead_code)]` n'est conservé pour masquer une visibilité prématurée.
|
||||||
|
|
||||||
|
Les types réellement publics de `pre.005` restent réexportés au crate-root et les tests continuent à les consommer via `crate::Item`.
|
||||||
|
|
||||||
|
En `pre.006`, si un helper devient réellement partagé entre modules de production, il pourra être promu en `pub(crate)`, réexporté au crate-root et consommé via `crate::Item` conformément aux règles.
|
||||||
|
|
||||||
|
## 4. Durcissement des règles et de l'audit
|
||||||
|
|
||||||
|
`RULES_RUST.md` explicite désormais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
private parent dans unit_tests -> super::Item obligatoire
|
||||||
|
pub/pub(crate) -> crate::Item obligatoire, jamais super:: ni nom nu
|
||||||
|
visibilité -> jamais élargie uniquement pour tester
|
||||||
|
```
|
||||||
|
|
||||||
|
`audit_rust_export_completeness.py` ajoute des canaris mécaniques bidirectionnels pour les fichiers `unit_tests/` rattachés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
RUST-IMPORT-204 private parent appelé sans super::
|
||||||
|
RUST-IMPORT-205 visible parent appelé sans crate-root
|
||||||
|
RUST-IMPORT-202 visible parent appelé via super:: (déjà présent)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.8-pre.5.fix.1
|
||||||
|
commit attendu = v0.2.8-pre.005-fix.001
|
||||||
|
tag prerelease = aucun
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs
|
||||||
|
scripts/audit_rust_export_completeness.py
|
||||||
|
docs/rules/RULES_RUST.md
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
deltas/0.2.8/pre.005-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Le fix reste `PREPARED` jusqu'à ce gate.
|
||||||
58
deltas/0.2.8/pre.005-fix.002.md
Normal file
58
deltas/0.2.8/pre.005-fix.002.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.005-fix.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.005-fix.002` — suppression des helpers wire morts hors tests
|
||||||
|
|
||||||
|
## 1. Cause
|
||||||
|
|
||||||
|
Le checkpoint de `pre.005-fix.001` corrige les quatre erreurs de type et valide les tests Transport, mais `cargo check`, `cargo clippy --workspace --all-targets` et la compilation des tests émettent encore neuf warnings `dead_code` dans `ws_helius_transactions.rs`.
|
||||||
|
|
||||||
|
Ces warnings concernent uniquement des helpers strictement privés préparant le wire `transactionSubscribe`/`transactionUnsubscribe` avant son intégration actor de `pre.006`. Ils n'ont volontairement aucun consommateur de production en `pre.005`.
|
||||||
|
|
||||||
|
## 2. Correction
|
||||||
|
|
||||||
|
`pre.005-fix.002` ne réélargit aucune visibilité et n'ajoute aucun `#[allow(dead_code)]`. Les helpers concernés restent privés et sont compilés uniquement sous `#[cfg(test)]` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HeliusTransactionSubscribeFilter::to_json_value
|
||||||
|
HeliusTransactionSubscribeOptions::to_json_value
|
||||||
|
helius_transaction_subscribe_params
|
||||||
|
helius_transaction_subscribe_method
|
||||||
|
helius_transaction_unsubscribe_method
|
||||||
|
decode_helius_transaction_subscribe_result
|
||||||
|
helius_transaction_unsubscribe_params
|
||||||
|
decode_helius_transaction_unsubscribe_result
|
||||||
|
insert_account_list
|
||||||
|
```
|
||||||
|
|
||||||
|
Le contrat public typed (`HeliusTransactionSubscribe*`) et les validations déterministes restent compilés dans le build de production. Les tests unitaires continuent d'accéder aux helpers privés via `super::Item`; les éléments visibles continuent d'être consommés via `crate::Item`.
|
||||||
|
|
||||||
|
## 3. Règle durable
|
||||||
|
|
||||||
|
`RUST-API-008` formalise qu'un helper strictement privé uniquement utile à un `unit_tests/` de préparation reste sous `#[cfg(test)]` tant qu'aucun chemin de production ne le consomme réellement. La visibilité ne doit pas être élargie et `#[allow(dead_code)]` ne doit pas servir de compensation.
|
||||||
|
|
||||||
|
En `pre.006`, seuls les helpers réellement nécessaires à l'actor seront promus dans le build normal ; si un partage cross-module impose `pub(crate)`, il devra alors passer par le crate-root et être consommé via `crate::Item`.
|
||||||
|
|
||||||
|
## 4. Fichiers
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
|
||||||
|
docs/rules/RULES_RUST.md
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
deltas/0.2.8/pre.005-fix.002.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Critère supplémentaire de fermeture : `cargo check` et Clippy ne doivent plus émettre les neuf warnings `dead_code` observés après `fix.001`.
|
||||||
277
deltas/0.2.8/pre.005.md
Normal file
277
deltas/0.2.8/pre.005.md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.005.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.005` — contrat typed Helius `transactionSubscribe`
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Cette tranche matérialise le contrat de requête Helius LaserStream WebSocket `transactionSubscribe` : filtres, options, `tokenAccounts`, validations déterministes, acknowledgement numérique et wire `transactionUnsubscribe`.
|
||||||
|
|
||||||
|
Elle ne publie volontairement **pas encore** de handle live transaction : `transactionNotification`, le registry actor, les remaps de remote IDs et les races reconnect/unsubscribe doivent arriver atomiquement en `pre.006` afin de ne jamais exposer un abonnement public incapable de livrer correctement ses notifications.
|
||||||
|
|
||||||
|
Le checkpoint opérateur de `pre.004-fix.002` est intégralement vert.
|
||||||
|
|
||||||
|
Version workspace :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.5
|
||||||
|
```
|
||||||
|
|
||||||
|
Livraison / commit attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.005
|
||||||
|
v0.2.8-pre.005
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag prerelease.
|
||||||
|
|
||||||
|
## 2. Audit Helius courant verrouillé
|
||||||
|
|
||||||
|
La documentation Helius relue le 2026-08-23 confirme pour `transactionSubscribe` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
filter.vote bool optionnel
|
||||||
|
filter.failed bool optionnel
|
||||||
|
filter.signature signature exacte optionnelle
|
||||||
|
filter.accountInclude liste OR, <= 50_000 adresses
|
||||||
|
filter.accountExclude liste d'exclusion, <= 50_000 adresses
|
||||||
|
filter.accountRequired liste AND, <= 50_000 adresses
|
||||||
|
filter.tokenAccounts none | balanceChanged | all
|
||||||
|
|
||||||
|
options.commitment processed | confirmed | finalized
|
||||||
|
options.encoding base58 | base64 | jsonParsed
|
||||||
|
options.transactionDetails full | signatures | accounts | none
|
||||||
|
options.showRewards bool optionnel
|
||||||
|
options.maxSupportedTransactionVersion
|
||||||
|
requis pour transactionDetails = accounts | full
|
||||||
|
|
||||||
|
subscribe result integer subscription id
|
||||||
|
unsubscribe params [subscriptionId]
|
||||||
|
unsubscribe result bool
|
||||||
|
late notifications possibles brièvement après unsubscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
`tokenAccounts = none` est équivalent à l'omission du champ. `balanceChanged` et `all` étendent le matching d'un `accountInclude` wallet aux token accounts qu'il possède selon les règles Helius documentées.
|
||||||
|
|
||||||
|
## 3. Contrat public typed
|
||||||
|
|
||||||
|
Nouveau module ciblé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Il publie depuis le crate-root :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HeliusTokenAccountsFilter
|
||||||
|
HeliusTransactionSubscribeEncoding
|
||||||
|
HeliusTransactionSubscribeFilter
|
||||||
|
HeliusTransactionSubscribeOptions
|
||||||
|
HeliusTransactionSubscribeRequest
|
||||||
|
```
|
||||||
|
|
||||||
|
Le contrat réutilise les types KSP existants lorsqu'ils sont wire-identiques :
|
||||||
|
|
||||||
|
```text
|
||||||
|
commitment -> SolanaCommitment
|
||||||
|
transactionDetails -> SolanaTransactionDetails
|
||||||
|
account filters -> ksp_core_lib::Pubkey
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun DTO Solana commun n'est recopié sous un nom Helius sans nécessité wire.
|
||||||
|
|
||||||
|
## 4. Sémantique des filtres et options
|
||||||
|
|
||||||
|
`HeliusTransactionSubscribeFilter` conserve explicitement la différence entre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
champ omis
|
||||||
|
liste présente mais vide []
|
||||||
|
liste présente avec valeurs
|
||||||
|
```
|
||||||
|
|
||||||
|
pour `accountInclude`, `accountExclude` et `accountRequired`.
|
||||||
|
|
||||||
|
Chaque liste est validée indépendamment avec la limite Helius :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0 ..= 50_000 accepté
|
||||||
|
50_001 rejeté avant I/O
|
||||||
|
```
|
||||||
|
|
||||||
|
Les erreurs déterministes n'incluent aucune signature ni adresse du filtre ; elles transportent uniquement le nom du champ et les cardinalités sûres.
|
||||||
|
|
||||||
|
`HeliusTransactionSubscribeOptions` impose avant I/O :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transactionDetails = full -> maxSupportedTransactionVersion requis
|
||||||
|
transactionDetails = accounts -> maxSupportedTransactionVersion requis
|
||||||
|
transactionDetails = signatures -> version optionnelle
|
||||||
|
transactionDetails = none -> version optionnelle
|
||||||
|
```
|
||||||
|
|
||||||
|
La distinction suivante est préservée sur le wire :
|
||||||
|
|
||||||
|
```text
|
||||||
|
options = None -> params = [filter]
|
||||||
|
options = Some(default) -> params = [filter, {}]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Wire subscribe/unsubscribe préparé
|
||||||
|
|
||||||
|
Les helpers crate-private préparés pour l'intégration actor de `pre.006` verrouillent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transactionSubscribe
|
||||||
|
transactionUnsubscribe
|
||||||
|
subscribe result integer -> u64
|
||||||
|
unsubscribe params -> [remote_subscription_id]
|
||||||
|
unsubscribe result -> bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Les décodeurs refusent les formes de réponse d'un type différent au lieu de les coercer.
|
||||||
|
|
||||||
|
Ces helpers restent crate-private : aucun raw provider-extension API public n'est introduit.
|
||||||
|
|
||||||
|
## 6. Réutilisation du moteur physique
|
||||||
|
|
||||||
|
Une fixture locale passe réellement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HeliusLaserStreamWsSession
|
||||||
|
-> physical_session() crate-private
|
||||||
|
-> WsSession::execute_json_rpc
|
||||||
|
-> actor/socket unique existant
|
||||||
|
-> transactionSubscribe
|
||||||
|
-> transactionUnsubscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
Elle vérifie les méthodes, params, acknowledgements et résultat d'unsubscribe exacts contre un peer WebSocket local.
|
||||||
|
|
||||||
|
Aucun second :
|
||||||
|
|
||||||
|
```text
|
||||||
|
actor
|
||||||
|
socket
|
||||||
|
pending map
|
||||||
|
reconnect loop
|
||||||
|
subscription engine
|
||||||
|
```
|
||||||
|
|
||||||
|
n'est ajouté.
|
||||||
|
|
||||||
|
## 7. Sécurité et surface différée
|
||||||
|
|
||||||
|
`Debug` pour le filtre/requête expose seulement des indicateurs, modes et cardinalités ; il ne rend ni la signature exacte ni les valeurs des comptes filtrés.
|
||||||
|
|
||||||
|
`HeliusLaserStreamWsSession` n'expose toujours pas :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pub async fn transaction_subscribe(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette absence est verrouillée par release-completeness. Le handle live arrive en `pre.006` avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transactionNotification
|
||||||
|
registry local/remote
|
||||||
|
reconnect + resubscribe
|
||||||
|
unsubscribe races
|
||||||
|
late notifications
|
||||||
|
backpressure ciblée
|
||||||
|
```
|
||||||
|
|
||||||
|
Le heartbeat Helius reste réservé à `pre.007`.
|
||||||
|
|
||||||
|
## 8. Canaries et non-régressions
|
||||||
|
|
||||||
|
Les nouveaux tests couvrent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
strings wire exactes tokenAccounts/encoding
|
||||||
|
serialization complète filtre/options
|
||||||
|
omission vs [] explicite
|
||||||
|
borne 50_000 / rejet 50_001 pour les trois listes
|
||||||
|
règle conditionnelle maxSupportedTransactionVersion
|
||||||
|
ack subscribe numérique strict
|
||||||
|
wire/result unsubscribe strict
|
||||||
|
Debug sans signature/adresses
|
||||||
|
round-trip local via actor physique partagé
|
||||||
|
public API des nouveaux types
|
||||||
|
absence du live handle avant pre.006
|
||||||
|
```
|
||||||
|
|
||||||
|
Les surfaces acquises restent inchangées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
SolanaStandardWsSession 9 familles standard
|
||||||
|
HeliusLaserStreamWsSession 6 familles standard supportées
|
||||||
|
HTTP 52 current + 14 historiques
|
||||||
|
Config Helius mainnet + devnet, secret/redaction/provenance validés
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune nouvelle dépendance Rust n'est ajoutée.
|
||||||
|
|
||||||
|
## 9. Preuve opérateur héritée
|
||||||
|
|
||||||
|
Le checkpoint `pre.004-fix.002` fourni le 2026-08-23 est intégralement vert :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py clean
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test -p ksp-config-lib OK 110/110 + ownership/public API
|
||||||
|
cargo test -p ksp-onchain-transport-lib OK 314 unit + 38 public + 26 completeness + 4 doctests
|
||||||
|
cargo test --workspace OK
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.004`, `pre.004-fix.001` et `pre.004-fix.002` sont donc `DONE` avant cette tranche.
|
||||||
|
|
||||||
|
## 10. Fichiers de la livraison
|
||||||
|
|
||||||
|
Nouveaux :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs
|
||||||
|
deltas/0.2.8/pre.005.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Modifiés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun fichier Config, schema, `.env`, README/USAGE, ROADMAP ou CHANGELOG n'est modifié.
|
||||||
|
|
||||||
|
## 11. Validation de préparation et gate opérateur
|
||||||
|
|
||||||
|
Validation statique disponible dans le sandbox 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Le sandbox ne fournit pas Cargo/rustfmt ; la tranche reste donc `PREPARED` jusqu'au checkpoint opérateur suivant :
|
||||||
|
|
||||||
|
```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 --workspace
|
||||||
|
```
|
||||||
235
deltas/0.2.8/pre.006.md
Normal file
235
deltas/0.2.8/pre.006.md
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.006.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.006` — Helius transactionNotification + lifecycle actor
|
||||||
|
|
||||||
|
## 1. Base et objet
|
||||||
|
|
||||||
|
Base appliquée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.5.fix.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Le checkpoint opérateur de cette base est intégralement vert et sans warning : `cargo fmt`, audit Rust, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, tests Transport et `cargo test --workspace` passent. Transport compte alors `322` tests unitaires, `39` tests public API, `27` tests release-completeness et `4` doctests compile-fail.
|
||||||
|
|
||||||
|
Cette tranche transforme le contrat de requête Helius préparé en `pre.005` en une souscription live complète, sans créer de second moteur WebSocket :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transactionSubscribe
|
||||||
|
-> registry actor existant
|
||||||
|
-> WsSubscription<HeliusTransactionNotification>
|
||||||
|
-> transactionNotification
|
||||||
|
-> reconnect/resubscribe/remap remote ID
|
||||||
|
-> transactionUnsubscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.8-pre.6
|
||||||
|
commit attendu = v0.2.8-pre.006
|
||||||
|
Git tag = aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header root `Cargo.toml` passe en version `226`.
|
||||||
|
|
||||||
|
## 3. Subscription kind provider
|
||||||
|
|
||||||
|
`WsSubscriptionKind` gagne :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HeliusTransaction
|
||||||
|
```
|
||||||
|
|
||||||
|
avec le triplet exact :
|
||||||
|
|
||||||
|
```text
|
||||||
|
as_str helius_transaction
|
||||||
|
subscribe_method transactionSubscribe
|
||||||
|
unsubscribe_method transactionUnsubscribe
|
||||||
|
notification_method transactionNotification
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette extension ne modifie pas la partition standard Solana de neuf familles et reste hors des trois familles standard classées unstable (`Block`, `SlotsUpdates`, `Vote`).
|
||||||
|
|
||||||
|
Le generic actor existant reste l'unique propriétaire :
|
||||||
|
|
||||||
|
- du socket physique ;
|
||||||
|
- du pending map JSON-RPC ;
|
||||||
|
- des local IDs ;
|
||||||
|
- des remote IDs ;
|
||||||
|
- du registry de subscriptions ;
|
||||||
|
- du reconnect/resubscribe ;
|
||||||
|
- des queues de notifications ;
|
||||||
|
- du cleanup unsubscribe ;
|
||||||
|
- du shutdown.
|
||||||
|
|
||||||
|
## 4. Handle live Helius
|
||||||
|
|
||||||
|
`HeliusLaserStreamWsSession` expose maintenant :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
transaction_subscribe(
|
||||||
|
&self,
|
||||||
|
request: &HeliusTransactionSubscribeRequest,
|
||||||
|
) -> Result<WsSubscription<HeliusTransactionNotification>>
|
||||||
|
```
|
||||||
|
|
||||||
|
La validation déterministe et la sérialisation de `pre.005` restent exécutées avant l'enregistrement actor. Le helper de sérialisation et ses sous-helpers redeviennent du code de production uniquement parce qu'ils ont désormais un consommateur réel ; ils restent strictement privés au module.
|
||||||
|
|
||||||
|
Aucune visibilité n'est élargie pour les tests. Les canaris du sous-module accèdent aux helpers privés avec `super::Item`; les contrats publics sont consommés via `crate::Item`.
|
||||||
|
|
||||||
|
## 5. Notification typed
|
||||||
|
|
||||||
|
Trois formes publiques sont exposées au crate-root.
|
||||||
|
|
||||||
|
### 5.1 Full/accounts
|
||||||
|
|
||||||
|
`HeliusFullTransactionNotification` conserve :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transaction serde_json::Value
|
||||||
|
signature String
|
||||||
|
slot u64
|
||||||
|
transactionIndex u64
|
||||||
|
```
|
||||||
|
|
||||||
|
Le nested `transaction` reste lossless en JSON, car sa forme dépend de `encoding` et `transactionDetails`; Transport ne décode pas les Programs.
|
||||||
|
|
||||||
|
### 5.2 Signatures
|
||||||
|
|
||||||
|
`HeliusTransactionSignatureNotification` conserve :
|
||||||
|
|
||||||
|
```text
|
||||||
|
signature String
|
||||||
|
slot u64
|
||||||
|
transactionIndex u64
|
||||||
|
err Omitted | Null | Value(JSON)
|
||||||
|
memo Omitted | Null | Value(String)
|
||||||
|
blockTime Omitted | Null | Value(i64)
|
||||||
|
confirmationStatus Omitted | Null | Value(String)
|
||||||
|
```
|
||||||
|
|
||||||
|
Les champs optionnels réutilisent `SolanaWireField` afin de ne pas confondre omission et `null`.
|
||||||
|
|
||||||
|
### 5.3 Union publique
|
||||||
|
|
||||||
|
```text
|
||||||
|
HeliusTransactionNotification::Full(...)
|
||||||
|
HeliusTransactionNotification::Signature(...)
|
||||||
|
HeliusTransactionNotification::Unknown(JSON)
|
||||||
|
```
|
||||||
|
|
||||||
|
`Unknown` conserve uniquement le `params.result` provider. L'enveloppe JSON-RPC complète et `params.subscription` ne franchissent pas le boundary public. Cette forme couvre notamment un `transactionDetails=none` ou une évolution provider non encore typée sans tuer arbitrairement la logical subscription.
|
||||||
|
|
||||||
|
## 6. Reconnect, unsubscribe tardif et backpressure
|
||||||
|
|
||||||
|
Le support Helius s'appuie directement sur les garanties du moteur `0.2.7` :
|
||||||
|
|
||||||
|
- les params `transactionSubscribe` originaux sont conservés par le registry ;
|
||||||
|
- après reconnect, un nouvel ID remote remplace l'ancien ;
|
||||||
|
- le `WsSubscriptionId` local reste stable ;
|
||||||
|
- le remote ID n'est jamais public ;
|
||||||
|
- au début d'un unsubscribe, le mapping remote -> local est retiré avant l'émission de `transactionUnsubscribe` ;
|
||||||
|
- une notification provider déjà en vol après cancellation est donc ignorée ;
|
||||||
|
- un overflow de queue échoue seulement la logical subscription lente ;
|
||||||
|
- le cleanup best-effort utilise automatiquement `transactionUnsubscribe` grâce au nouveau `WsSubscriptionKind`.
|
||||||
|
|
||||||
|
Cette sémantique correspond au contrat Helius actuel qui précise que quelques messages en vol peuvent encore arriver brièvement après `transactionUnsubscribe`.
|
||||||
|
|
||||||
|
## 7. Canaris ajoutés/actualisés
|
||||||
|
|
||||||
|
Les tests Helius transaction couvrent maintenant :
|
||||||
|
|
||||||
|
```text
|
||||||
|
notification Full / Signature / Unknown
|
||||||
|
live transactionSubscribe exact via façade publique
|
||||||
|
transactionNotification routée vers WsSubscription
|
||||||
|
transactionUnsubscribe exact via handle public
|
||||||
|
reconnect : remote ID 41 -> 99
|
||||||
|
resubscribe : params identiques
|
||||||
|
stable local WsSubscriptionId
|
||||||
|
late transactionNotification après demande unsubscribe ignorée
|
||||||
|
overflow transaction : handle lent Failed + ERROR_CODE_WS_BACKPRESSURE_OVERFLOW
|
||||||
|
cleanup overflow : transactionUnsubscribe [remote_id]
|
||||||
|
Helius root sain reste Active et reçoit encore sa notification
|
||||||
|
```
|
||||||
|
|
||||||
|
Un canari lifecycle verrouille aussi le triplet exact du nouveau `WsSubscriptionKind::HeliusTransaction`.
|
||||||
|
|
||||||
|
Les public/release canaries gagnent :
|
||||||
|
|
||||||
|
- le symbole public `HeliusLaserStreamWsSession::transaction_subscribe` ;
|
||||||
|
- les trois types publics de notification ;
|
||||||
|
- la présence du kind provider ;
|
||||||
|
- l'absence de second `connect_async`/actor dans le module Helius ;
|
||||||
|
- la conservation des compile-fail Helius block/slotsUpdates/vote/escape-hatch.
|
||||||
|
|
||||||
|
Comptages attendus :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport unit 325
|
||||||
|
Transport public API 40
|
||||||
|
release completeness 28
|
||||||
|
doctests compile-fail 4
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Documentation
|
||||||
|
|
||||||
|
Le plan `015` :
|
||||||
|
|
||||||
|
- ferme `pre.005`, `fix.001` et `fix.002` après preuve opérateur sans warning ;
|
||||||
|
- marque `pre.006` PREPARED ;
|
||||||
|
- documente l'union notification, le remap remote/local et les nouveaux canaris lifecycle.
|
||||||
|
|
||||||
|
La validation `011` :
|
||||||
|
|
||||||
|
- enregistre le checkpoint final `pre.005` ;
|
||||||
|
- ouvre la gate `pre.006` ;
|
||||||
|
- conserve heartbeat, adversarial élargi et smoke live dans leurs tranches prévues.
|
||||||
|
|
||||||
|
## 9. Hors scope
|
||||||
|
|
||||||
|
Restent explicitement hors de `pre.006` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
heartbeat / idle timer pre.007
|
||||||
|
provider adversarial/security élargi pre.008
|
||||||
|
compliance finale pre.009
|
||||||
|
smoke Helius live opt-in pre.010
|
||||||
|
LaserStream gRPC future transport séparé
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune nouvelle dépendance n'est ajoutée.
|
||||||
|
|
||||||
|
## 10. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_lifecycle.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_subscription.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/ws_helius_transactions.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_lifecycle.rs
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
deltas/0.2.8/pre.006.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. 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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Critère de fermeture : aucune erreur, aucun warning nouveau, audit Rust clean et tous les nouveaux canaris lifecycle Helius verts.
|
||||||
111
deltas/0.2.8/pre.007-fix.001.md
Normal file
111
deltas/0.2.8/pre.007-fix.001.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.007-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.007-fix.001` — déterminisme canaris heartbeat Tokio
|
||||||
|
|
||||||
|
## 1. Base et défaut corrigé
|
||||||
|
|
||||||
|
Base appliquée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.7
|
||||||
|
```
|
||||||
|
|
||||||
|
Le checkpoint opérateur confirme que `fmt`, audit Rust et `cargo check --workspace` sont propres. Le gate reste toutefois bloqué par :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
1 erreur clippy::implicit_return dans le helper test yield_runtime_steps
|
||||||
|
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
329/331 unit passent
|
||||||
|
2 échecs heartbeat test-only
|
||||||
|
```
|
||||||
|
|
||||||
|
Les autres canaris heartbeat, notamment policy Helius-only, absence sur standard, write failure/reconnect et réarmement après reconnexion, passent déjà. Aucun défaut runtime heartbeat n'est démontré.
|
||||||
|
|
||||||
|
## 2. Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.8-pre.7.fix.1
|
||||||
|
commit attendu = v0.2.8-pre.007-fix.001
|
||||||
|
Git tag = aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header root `Cargo.toml` passe en version `228`.
|
||||||
|
|
||||||
|
## 3. Correction `implicit_return`
|
||||||
|
|
||||||
|
Le helper privé test-only :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn yield_runtime_steps()
|
||||||
|
```
|
||||||
|
|
||||||
|
termine désormais par un `return;` explicite après sa boucle de yields. Aucun `allow` Clippy n'est introduit.
|
||||||
|
|
||||||
|
## 4. Armement déterministe du timer Tokio
|
||||||
|
|
||||||
|
Dans le canari 60 s, `tokio::time::pause()` était suivi immédiatement de `advance(59 s)`. Rien ne garantissait alors que la tâche actor ait déjà été pollée et ait enregistré son `sleep_until(heartbeat_deadline)` sous l'horloge pausée.
|
||||||
|
|
||||||
|
Le fix insère un passage de stabilisation par `yield_runtime_steps().await` immédiatement après `pause()` et avant toute avance. Le canari vérifie ensuite toujours la vraie cadence runtime :
|
||||||
|
|
||||||
|
```text
|
||||||
|
t=59 s aucun Ping
|
||||||
|
t=60 s un Ping
|
||||||
|
t=119 s aucun second Ping
|
||||||
|
t=120 s second Ping
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune cadence spéciale de test n'est créée.
|
||||||
|
|
||||||
|
## 5. Sémantique du canari close
|
||||||
|
|
||||||
|
Le canari close est renforcé en deux étapes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
t=30 s avant close -> observation channel obligatoirement Empty
|
||||||
|
après close -> aucun Ping `Ok(())` accepté
|
||||||
|
```
|
||||||
|
|
||||||
|
Après réception de la frame Close, le serveur fixture termine et détruit naturellement son sender. Le receiver peut donc rendre `Disconnected`, ce qui prouve toujours qu'aucun heartbeat n'a été émis après fermeture. Exiger uniquement `Empty` après close était une hypothèse incorrecte du test.
|
||||||
|
|
||||||
|
## 6. Runtime explicitement inchangé
|
||||||
|
|
||||||
|
Ce fix ne modifie pas :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_session.rs
|
||||||
|
HELIUS_WS_HEARTBEAT_INTERVAL = 60 s
|
||||||
|
WebSocket Ping control frame
|
||||||
|
WsProtocolKind
|
||||||
|
WsSessionSettings
|
||||||
|
Config / schema / env
|
||||||
|
transactionSubscribe / transactionNotification lifecycle
|
||||||
|
reconnect / remap / backpressure
|
||||||
|
```
|
||||||
|
|
||||||
|
Il ne rajoute aucune dépendance ni feature.
|
||||||
|
|
||||||
|
## 7. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
deltas/0.2.8/pre.007-fix.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Critère de fermeture : zéro warning, audit clean, **331 unit / 40 public API / 29 completeness / 4 doctests** et workspace vert. `pre.008` reste bloqué jusque-là.
|
||||||
132
deltas/0.2.8/pre.007-fix.002.md
Normal file
132
deltas/0.2.8/pre.007-fix.002.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.007-fix.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.007-fix.002` — observation scheduler bornée du heartbeat
|
||||||
|
|
||||||
|
## 1. Base et défaut restant
|
||||||
|
|
||||||
|
Base appliquée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.7.fix.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Le replay opérateur du `fix.001` ferme le lint Clippy et le canari close : `fmt`, audit Rust, `cargo check` et `cargo clippy` sont verts. Transport atteint **330/331 unit** ; un seul canari reste en échec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
helius_heartbeat_sends_ping_at_sixty_seconds_and_rearms
|
||||||
|
```
|
||||||
|
|
||||||
|
Le reste des preuves heartbeat passe, notamment Helius-only, intervalle 60 s, absence standard, write failure/reconnect, close avant deadline et réarmement après reconnexion. Aucun défaut runtime supplémentaire n'est démontré.
|
||||||
|
|
||||||
|
## 2. Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.8-pre.7.fix.2
|
||||||
|
commit attendu = v0.2.8-pre.007-fix.002
|
||||||
|
Git tag = aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header root `Cargo.toml` passe en version `229`.
|
||||||
|
|
||||||
|
## 3. Cause exacte
|
||||||
|
|
||||||
|
Après :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
tokio::time::advance(std::time::Duration::from_secs(1)).await;
|
||||||
|
```
|
||||||
|
|
||||||
|
le deadline heartbeat est bien expiré, mais l'observation du Ping nécessite encore plusieurs étapes asynchrones :
|
||||||
|
|
||||||
|
```text
|
||||||
|
actor WsSession
|
||||||
|
-> websocket.send(Ping)
|
||||||
|
-> socket local
|
||||||
|
-> serveur fixture websocket.next()
|
||||||
|
-> ping_tx.send(())
|
||||||
|
-> ping_rx.try_recv()
|
||||||
|
```
|
||||||
|
|
||||||
|
Un `try_recv()` après un nombre fixe faible de `yield_now()` reste une course scheduler. Le défaut est donc dans le protocole d'observation du test, pas dans la cadence runtime.
|
||||||
|
|
||||||
|
## 4. Correction déterministe
|
||||||
|
|
||||||
|
Le fichier de tests ajoute un helper privé local :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn wait_for_observed_ping(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Il effectue au maximum 256 tours :
|
||||||
|
|
||||||
|
```text
|
||||||
|
try_recv() == Ok(()) -> succès immédiat
|
||||||
|
try_recv() == Empty -> yield_now().await puis nouvelle tentative
|
||||||
|
try_recv() == Disconnected -> échec immédiat
|
||||||
|
```
|
||||||
|
|
||||||
|
Après épuisement de la borne, le test échoue explicitement si aucun Ping n'est observé.
|
||||||
|
|
||||||
|
Ce helper n'appelle volontairement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ni tokio::time::advance()
|
||||||
|
ni tokio::time::sleep()
|
||||||
|
ni tokio::time::timeout()
|
||||||
|
```
|
||||||
|
|
||||||
|
L'horloge virtuelle reste donc exactement à `t=60 s` ou `t=120 s` pendant l'attente de propagation. Le canari ne peut pas réussir en observant accidentellement un heartbeat ultérieur.
|
||||||
|
|
||||||
|
## 5. Cadence vérifiée inchangée
|
||||||
|
|
||||||
|
Le scénario reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
t=59 s aucun Ping
|
||||||
|
t=60 s premier Ping observé après propagation scheduler bornée
|
||||||
|
t=119 s aucun second Ping
|
||||||
|
t=120 s second Ping observé après propagation scheduler bornée
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune cadence de test spéciale n'est introduite.
|
||||||
|
|
||||||
|
## 6. Runtime explicitement inchangé
|
||||||
|
|
||||||
|
Ce fix ne modifie pas :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_session.rs
|
||||||
|
HELIUS_WS_HEARTBEAT_INTERVAL = 60 s
|
||||||
|
WebSocket Ping control frame
|
||||||
|
WsProtocolKind
|
||||||
|
WsSessionSettings
|
||||||
|
Config / schema / env
|
||||||
|
transactionSubscribe / transactionNotification lifecycle
|
||||||
|
reconnect / remap / backpressure
|
||||||
|
```
|
||||||
|
|
||||||
|
Il n'ajoute aucune dépendance, feature ou visibilité.
|
||||||
|
|
||||||
|
## 7. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
deltas/0.2.8/pre.007-fix.002.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Critère de fermeture : zéro warning, audit clean, **331 unit / 40 public API / 29 completeness / 4 doctests** et workspace vert. `pre.008` reste bloqué jusque-là.
|
||||||
105
deltas/0.2.8/pre.007-fix.003.md
Normal file
105
deltas/0.2.8/pre.007-fix.003.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.007-fix.003.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.007-fix.003` — driver I/O sous horloge Tokio pausée
|
||||||
|
|
||||||
|
## 1. Base et défaut restant
|
||||||
|
|
||||||
|
Base appliquée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.7.fix.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Le replay opérateur du `fix.002` est propre pour fmt/audit/check/Clippy mais reste à **330/331 unit** sur le même canari :
|
||||||
|
|
||||||
|
```text
|
||||||
|
helius_heartbeat_sends_ping_at_sixty_seconds_and_rearms
|
||||||
|
```
|
||||||
|
|
||||||
|
Après 256 `yield_now()` bornés, le Ping n'est toujours pas visible par le canal de la fixture TCP locale.
|
||||||
|
|
||||||
|
## 2. Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.8-pre.7.fix.3
|
||||||
|
commit attendu = v0.2.8-pre.007-fix.003
|
||||||
|
Git tag = aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header root `Cargo.toml` passe en version `230`.
|
||||||
|
|
||||||
|
## 3. Diagnostic affiné
|
||||||
|
|
||||||
|
La revue du runtime confirme que `src/ws_session.rs` possède déjà le chemin attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
heartbeat_deadline = now + 60 s
|
||||||
|
tokio::select! -> sleep_until(heartbeat_deadline)
|
||||||
|
send_helius_heartbeat() -> websocket.send(Ping)
|
||||||
|
succès -> heartbeat_deadline = now + 60 s
|
||||||
|
```
|
||||||
|
|
||||||
|
Le canari de reconnexion actor réel reste vert et observe un Ping après réarmement. Aucun défaut runtime nouveau n'est donc démontré.
|
||||||
|
|
||||||
|
Le défaut est dans le harness : avec l'horloge Tokio pausée, `yield_now()` fait progresser les tâches runnable mais ne garantit pas à lui seul un tour du driver I/O OS. Une frame écrite vers le socket loopback peut donc ne pas encore être remontée jusqu'au `websocket.next()` de la fixture.
|
||||||
|
|
||||||
|
## 4. Correction du canari
|
||||||
|
|
||||||
|
Le helper yield-only est remplacé par une observation bornée qui :
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. reprend temporairement l'horloge Tokio ;
|
||||||
|
2. attend `ping_rx.recv()` avec un timeout réel de 1 s maximum ;
|
||||||
|
3. mesure le temps réel consommé ;
|
||||||
|
4. repause immédiatement l'horloge ;
|
||||||
|
5. exige qu'un Ping ait été reçu.
|
||||||
|
```
|
||||||
|
|
||||||
|
La fenêtre n'est ouverte **qu'après** que l'horloge virtuelle a atteint le deadline attendu. Une seconde réelle ne peut donc pas masquer l'absence d'un heartbeat dont la cadence nominale est 60 s.
|
||||||
|
|
||||||
|
Pour le second intervalle, le temps réel consommé lors de la première propagation est soustrait de la phase pré-deadline puis réinjecté lors du franchissement suivant. Cela garantit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
avant contrôle négatif : moins de 59 s depuis le réarmement réel
|
||||||
|
après avance positive : au moins 60 s depuis le réarmement réel
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Runtime inchangé
|
||||||
|
|
||||||
|
Ce fix ne modifie pas :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_session.rs
|
||||||
|
HELIUS_WS_HEARTBEAT_INTERVAL = 60 s
|
||||||
|
WebSocket Ping control frame
|
||||||
|
actor/reconnect/shutdown
|
||||||
|
WsSessionSettings
|
||||||
|
Config / schema / env
|
||||||
|
transactionSubscribe / transactionNotification lifecycle
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dépendance, feature, API ou visibilité n'est ajoutée.
|
||||||
|
|
||||||
|
## 6. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
deltas/0.2.8/pre.007-fix.003.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Critère de fermeture : zéro warning, audit clean, **331 unit / 40 public API / 29 completeness / 4 doctests** et workspace vert.
|
||||||
90
deltas/0.2.8/pre.007-fix.004.md
Normal file
90
deltas/0.2.8/pre.007-fix.004.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.007-fix.004.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.007-fix.004` — alignement du canari dependency-firewall Tokio dev
|
||||||
|
|
||||||
|
## 1. Base et défaut restant
|
||||||
|
|
||||||
|
Base appliquée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.7.fix.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Le replay opérateur du `fix.003` ferme entièrement Transport :
|
||||||
|
|
||||||
|
```text
|
||||||
|
331/331 unit
|
||||||
|
40/40 public API
|
||||||
|
29/29 release completeness
|
||||||
|
4/4 doctests
|
||||||
|
fmt/audit/check/Clippy verts
|
||||||
|
```
|
||||||
|
|
||||||
|
`cargo test --workspace` révèle toutefois un unique défaut dans le canari workspace `transport_manifest_preserves_ksp_dependency_firewall` : il attend encore l'ancienne déclaration Tokio dev `features = ["net", "rt"]`, alors que `pre.007` a légitimement ajouté `io-util` et `test-util`.
|
||||||
|
|
||||||
|
## 2. Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.8-pre.7.fix.4
|
||||||
|
commit attendu = v0.2.8-pre.007-fix.004
|
||||||
|
Git tag = aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header root `Cargo.toml` passe en version `231`.
|
||||||
|
|
||||||
|
## 3. Correction
|
||||||
|
|
||||||
|
Le manifest Transport n'est pas modifié. Le canari dans `crates/ksp-core-lib/tests/workspace_dependencies.rs` est mis en cohérence avec la déclaration réellement livrée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tokio = { workspace = true, features = ["io-util", "net", "rt", "test-util"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
La liste des noms de dev-dependencies reste exactement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
[tokio]
|
||||||
|
```
|
||||||
|
|
||||||
|
Le firewall KSP reste inchangé : aucune dépendance Config/Store/Program/tracing direct n'est autorisée dans Transport.
|
||||||
|
|
||||||
|
## 4. Portée explicitement inchangée
|
||||||
|
|
||||||
|
Ce fix ne modifie pas :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_session.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
|
||||||
|
heartbeat Helius 60 s
|
||||||
|
WebSocket Ping control frame
|
||||||
|
actor/reconnect/shutdown
|
||||||
|
Config / schema / env
|
||||||
|
transaction lifecycle
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dépendance ni feature supplémentaire n'est ajoutée.
|
||||||
|
|
||||||
|
## 5. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-core-lib/tests/workspace_dependencies.rs
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
deltas/0.2.8/pre.007-fix.004.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Critère de fermeture : zéro warning, audit clean, Transport **331 unit / 40 API / 29 completeness / 4 doctests** et workspace entièrement vert.
|
||||||
211
deltas/0.2.8/pre.007.md
Normal file
211
deltas/0.2.8/pre.007.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.007.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.007` — heartbeat Helius WebSocket + idle lifecycle
|
||||||
|
|
||||||
|
## 1. Base et objet
|
||||||
|
|
||||||
|
Base appliquée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.6
|
||||||
|
```
|
||||||
|
|
||||||
|
Le checkpoint opérateur de `pre.006` est intégralement vert et sans warning :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py clean
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
Transport unit 325/325
|
||||||
|
Transport public API 40/40
|
||||||
|
Transport release completeness 28/28
|
||||||
|
Transport doctests compile-fail 4/4
|
||||||
|
cargo test --workspace OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette tranche ajoute uniquement la policy de heartbeat Helius LaserStream WebSocket dans l'actor physique commun. Elle ne rouvre pas le lifecycle transaction validé par `pre.006`.
|
||||||
|
|
||||||
|
## 2. Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.8-pre.7
|
||||||
|
commit attendu = v0.2.8-pre.007
|
||||||
|
Git tag = aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header root `Cargo.toml` passe en version `227`.
|
||||||
|
|
||||||
|
## 3. Policy heartbeat provider-owned
|
||||||
|
|
||||||
|
La décision d'architecture de `pre.001` est matérialisée sans nouveau réglage public :
|
||||||
|
|
||||||
|
```text
|
||||||
|
protocol WsProtocolKind::HeliusLaserStream uniquement
|
||||||
|
intervalle nominal 60 s
|
||||||
|
frame WebSocket Ping control frame vide
|
||||||
|
état actor Active uniquement
|
||||||
|
succès réarmement à now + 60 s
|
||||||
|
reconnect réussi réarmement depuis la connexion de remplacement
|
||||||
|
write timeout/failure WsActorIoOutcome::Failed
|
||||||
|
close/shutdown timer abandonné ; shutdown prioritaire
|
||||||
|
SolanaStandard aucun heartbeat provider
|
||||||
|
```
|
||||||
|
|
||||||
|
Le heartbeat n'est pas un appel JSON-RPC `ping`. KSP utilise la frame de contrôle WebSocket `Ping`, déjà compatible avec le traitement `Pong` de l'actor.
|
||||||
|
|
||||||
|
Aucun champ `heartbeat_*` n'est ajouté à :
|
||||||
|
|
||||||
|
```text
|
||||||
|
WsSessionSettings
|
||||||
|
WsEndpointSettings
|
||||||
|
Config
|
||||||
|
.env.example
|
||||||
|
schema Transport
|
||||||
|
```
|
||||||
|
|
||||||
|
La cadence est donc une policy Helius WebSocket interne, pas une option de configuration générique.
|
||||||
|
|
||||||
|
## 4. Intégration dans l'actor unique
|
||||||
|
|
||||||
|
`run_ws_session_actor` possède le deadline heartbeat avec les mêmes priorités de shutdown que le reste du moteur :
|
||||||
|
|
||||||
|
```text
|
||||||
|
shutdown
|
||||||
|
command
|
||||||
|
socket input
|
||||||
|
Helius heartbeat deadline
|
||||||
|
pending JSON-RPC timeout
|
||||||
|
```
|
||||||
|
|
||||||
|
Le branch heartbeat est désactivé pour `SolanaStandard`.
|
||||||
|
|
||||||
|
Sur succès du Ping, le prochain deadline est recalculé à partir de `Instant::now()`. Pendant reconnect, aucun heartbeat n'est émis ; lorsque `recover_websocket_session` rend une connexion de remplacement active, le deadline est réarmé à 60 s.
|
||||||
|
|
||||||
|
Le helper d'écriture sélectionne de façon bornée entre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
shutdown signal
|
||||||
|
websocket.send(Ping)
|
||||||
|
command_timeout
|
||||||
|
```
|
||||||
|
|
||||||
|
Une erreur d'écriture ou un timeout retourne le même `WsActorIoOutcome::Failed` que les autres erreurs de connexion. Le chemin de reconnect, son budget, son backoff, les remaps et le shutdown restent donc uniques.
|
||||||
|
|
||||||
|
## 5. Timers déterministes de test
|
||||||
|
|
||||||
|
La cadence runtime ne doit pas être raccourcie pour rendre les tests rapides. La crate active donc uniquement côté dev/test la feature Tokio :
|
||||||
|
|
||||||
|
```text
|
||||||
|
test-util
|
||||||
|
```
|
||||||
|
|
||||||
|
avec `io-util` nécessaire au canari de socket cassé en mémoire.
|
||||||
|
|
||||||
|
Il ne s'agit pas d'une nouvelle dépendance ; aucune feature runtime de production n'est ajoutée au contrat KSP.
|
||||||
|
|
||||||
|
Les tests utilisent `tokio::time::pause()` / `advance()` pour vérifier la vraie constante de 60 s.
|
||||||
|
|
||||||
|
## 6. Canaris ajoutés
|
||||||
|
|
||||||
|
Six tests unitaires Transport sont ajoutés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Helius-only + constante 60 s
|
||||||
|
premier Ping à 60 s + second Ping après réarmement
|
||||||
|
absence totale de heartbeat provider sur SolanaStandard
|
||||||
|
close à 30 s sans Ping
|
||||||
|
échec d'écriture Ping -> WsActorIoOutcome::Failed / ERROR_CODE_WS_CONNECTION_FAILED
|
||||||
|
reconnect avant heartbeat -> nouveau deadline 60 s depuis la connexion de remplacement
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test hérité `websocket_shutdown_interrupts_reconnect_backoff_without_new_connection` continue de couvrir l'interruption du backoff par shutdown, tandis que le nouveau branch heartbeat place également le signal shutdown en première priorité.
|
||||||
|
|
||||||
|
Le canari release-completeness vérifie que :
|
||||||
|
|
||||||
|
- la policy reste dans `ws_session.rs` ;
|
||||||
|
- le Ping est une frame WebSocket ;
|
||||||
|
- `WsSessionSettings` ne gagne aucun champ de heartbeat ;
|
||||||
|
- la façade Helius ne possède aucun timer séparé.
|
||||||
|
|
||||||
|
Comptages attendus :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport unit 331
|
||||||
|
Transport public API 40
|
||||||
|
release completeness 29
|
||||||
|
doctests compile-fail 4
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Non-régressions
|
||||||
|
|
||||||
|
Cette tranche ne modifie pas :
|
||||||
|
|
||||||
|
```text
|
||||||
|
surface Helius standard 6 familles
|
||||||
|
transactionSubscribe request/filter/options
|
||||||
|
transactionNotification Full/Signature/Unknown
|
||||||
|
WsSubscriptionKind::HeliusTransaction
|
||||||
|
remote/local ID remapping
|
||||||
|
late notification handling
|
||||||
|
backpressure logical subscription
|
||||||
|
standard Solana WebSocket 18/18
|
||||||
|
HTTP 52 current / 14 historical
|
||||||
|
Config Helius mainnet/devnet
|
||||||
|
dependency firewall
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune promesse de replay WebSocket ou de livraison lossless n'est introduite.
|
||||||
|
|
||||||
|
## 8. Documentation
|
||||||
|
|
||||||
|
Le plan `015` :
|
||||||
|
|
||||||
|
- ferme `pre.006` sur preuve opérateur ;
|
||||||
|
- marque `pre.007` PREPARED ;
|
||||||
|
- matérialise la policy Helius-only 60 s et ses canaris.
|
||||||
|
|
||||||
|
La validation `011` :
|
||||||
|
|
||||||
|
- transforme les critères lifecycle `pre.006` en preuves acquises ;
|
||||||
|
- ouvre la gate `pre.007` ;
|
||||||
|
- conserve les tests adversariaux élargis pour `pre.008`.
|
||||||
|
|
||||||
|
## 9. Hors scope
|
||||||
|
|
||||||
|
Restent hors de `pre.007` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
provider adversarial lifecycle élargi pre.008
|
||||||
|
payload/security/redaction adversarial pre.008
|
||||||
|
compliance Helius + standard + HTTP pre.009
|
||||||
|
smoke Helius WebSocket live opt-in pre.010
|
||||||
|
LaserStream gRPC future backend distinct
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_session.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
deltas/0.2.8/pre.007.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. 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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Critère de fermeture : zéro warning nouveau, audit Rust clean, **331 unit / 40 public API / 29 completeness / 4 doctests** et workspace vert.
|
||||||
238
deltas/0.2.8/pre.008.md
Normal file
238
deltas/0.2.8/pre.008.md
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.008.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.008` — adversarial Helius provider + limites + diagnostics sûrs
|
||||||
|
|
||||||
|
## 1. Base et objet
|
||||||
|
|
||||||
|
Base appliquée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.7.fix.4
|
||||||
|
```
|
||||||
|
|
||||||
|
Le checkpoint opérateur final de `pre.007` est intégralement vert :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py clean
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
Transport unit 331/331
|
||||||
|
Transport public API 40/40
|
||||||
|
Transport release completeness 29/29
|
||||||
|
Transport doctests compile-fail 4/4
|
||||||
|
cargo test --workspace OK
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.008` durcit la surface Helius WebSocket contre des comportements provider adversariaux sans créer de nouveau runtime. La tranche réutilise l'actor physique, les limites frame/message, le reconnect, le registry et la backpressure existants.
|
||||||
|
|
||||||
|
## 2. Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.8-pre.8
|
||||||
|
commit attendu = v0.2.8-pre.008
|
||||||
|
Git tag = aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header root `Cargo.toml` passe en version `232`.
|
||||||
|
|
||||||
|
## 3. Ré-audit Helius courant
|
||||||
|
|
||||||
|
La documentation Helius réauditée le 2026-08-23 confirme :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transactionSubscribe supporté
|
||||||
|
transactionUnsubscribe supporté
|
||||||
|
accountInclude/accountExclude/accountRequired 50 000 adresses max par liste
|
||||||
|
blockSubscribe non supporté Helius
|
||||||
|
slotsUpdatesSubscribe non supporté Helius
|
||||||
|
voteSubscribe non supporté Helius
|
||||||
|
endpoints WSS unifiés mainnet/devnet
|
||||||
|
```
|
||||||
|
|
||||||
|
La tranche n'introduit donc aucune nouvelle famille provider.
|
||||||
|
|
||||||
|
## 4. `Debug` sûr pour les notifications Helius
|
||||||
|
|
||||||
|
Le prompt `0.2.8` interdit les payloads arbitraires/massifs dans les diagnostics génériques. Avant cette tranche, les types de notification Helius dérivaient `Debug`, ce qui pouvait rendre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transaction JSON brut
|
||||||
|
signature complète
|
||||||
|
memo/error provider
|
||||||
|
fallback Unknown(serde_json::Value) brut
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.008` remplace ces dérivations par des implémentations explicites :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HeliusFullTransactionNotification
|
||||||
|
transaction <omitted>
|
||||||
|
signature <omitted>
|
||||||
|
slot visible
|
||||||
|
transaction_index visible
|
||||||
|
|
||||||
|
HeliusTransactionSignatureNotification
|
||||||
|
signature <omitted>
|
||||||
|
slot/index visibles
|
||||||
|
err omitted/null/value seulement
|
||||||
|
memo omitted/null/value seulement
|
||||||
|
block_time omitted/null/value seulement
|
||||||
|
confirmation_status omitted/null/value seulement
|
||||||
|
|
||||||
|
HeliusTransactionNotification::Unknown
|
||||||
|
payload Value conservé fonctionnellement
|
||||||
|
Debug = Unknown("<omitted>")
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun accessor fonctionnel n'est supprimé. La donnée reste accessible explicitement au consommateur qui la demande ; seul le chemin diagnostic implicite est redacted.
|
||||||
|
|
||||||
|
## 5. Capability guard inverse
|
||||||
|
|
||||||
|
La façade Helius avait déjà quatre doctests compile-fail :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Helius -X-> blockSubscribe
|
||||||
|
Helius -X-> slotsUpdatesSubscribe
|
||||||
|
Helius -X-> voteSubscribe
|
||||||
|
Helius -X-> into_inner
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.008` ajoute le canari inverse :
|
||||||
|
|
||||||
|
```text
|
||||||
|
SolanaStandardWsSession -X-> transactionSubscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
La séparation standard/provider est donc prouvée dans les deux sens sans exposer le `WsSession` générique.
|
||||||
|
|
||||||
|
## 6. Provider RPC application error
|
||||||
|
|
||||||
|
Un serveur local adversarial retourne une erreur JSON-RPC `transactionSubscribe` avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
code provider
|
||||||
|
message canary sensible
|
||||||
|
payload data arbitraire de plusieurs KiB
|
||||||
|
```
|
||||||
|
|
||||||
|
Le canari exige :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ERROR_CODE_RPC_APPLICATION_ERROR
|
||||||
|
context sûr = rpc_code + method
|
||||||
|
message/data provider non copiés dans KspError
|
||||||
|
api-key URL non rendue
|
||||||
|
filter signature non rendue
|
||||||
|
session physique reste Active
|
||||||
|
subscription_count revient à 0
|
||||||
|
rootSubscribe fonctionne ensuite normalement
|
||||||
|
```
|
||||||
|
|
||||||
|
Une erreur d'application provider ne doit donc pas être promue en panne de transport.
|
||||||
|
|
||||||
|
## 7. Notification method mismatch
|
||||||
|
|
||||||
|
Un remote ID enregistré comme `HeliusTransaction` reçoit volontairement un `rootNotification`.
|
||||||
|
|
||||||
|
Résultat attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
logical transaction subscription -> Failed / WS_PROTOCOL_ERROR
|
||||||
|
remote binding transaction -> cleanup transactionUnsubscribe
|
||||||
|
session physique -> Active
|
||||||
|
subscription root saine -> Active et notification reçue
|
||||||
|
```
|
||||||
|
|
||||||
|
Le mismatch de famille ne doit pas contaminer les autres logical subscriptions.
|
||||||
|
|
||||||
|
## 8. Payload entrant oversized
|
||||||
|
|
||||||
|
Un endpoint Helius local envoie une frame texte supérieure aux limites configurées :
|
||||||
|
|
||||||
|
```text
|
||||||
|
max_message_size_bytes = 256
|
||||||
|
max_frame_size_bytes = 128
|
||||||
|
```
|
||||||
|
|
||||||
|
Le canari prouve que la limite tungstenite/actor s'applique avant tout décodage JSON provider, déclenche le reconnect borné existant et laisse la connexion Helius de remplacement utilisable pour un `rootSubscribe`/unsubscribe normal.
|
||||||
|
|
||||||
|
Aucun nouveau compteur ou chemin de reconnect n'est ajouté.
|
||||||
|
|
||||||
|
## 9. Backpressure et lifecycle conservés
|
||||||
|
|
||||||
|
La preuve provider-specific de `pre.006` reste autoritaire pour :
|
||||||
|
|
||||||
|
```text
|
||||||
|
queue capacity = 1
|
||||||
|
transaction notification overflow
|
||||||
|
échec de la seule transaction subscription
|
||||||
|
cleanup transactionUnsubscribe
|
||||||
|
root subscription saine non affectée
|
||||||
|
session physique Active
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.008` ne duplique pas ce scénario. Les canaris partagés hérités continuent aussi de couvrir unsubscribe pendant reconnect, decode failure isolé, pending bounds et shutdown pendant backoff.
|
||||||
|
|
||||||
|
## 10. Canaris ajoutés
|
||||||
|
|
||||||
|
Quatre unit tests Helius sont ajoutés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
notification Debug redaction Full/Signature/Unknown
|
||||||
|
provider RPC application error sûre + session réutilisable
|
||||||
|
notification method mismatch isolé + cleanup transactionUnsubscribe
|
||||||
|
oversized inbound provider payload + reconnect + session réutilisable
|
||||||
|
```
|
||||||
|
|
||||||
|
Un release-completeness canary supplémentaire vérifie les guards, les `Debug` explicites et la réutilisation des mécanismes de bornage de l'actor.
|
||||||
|
|
||||||
|
Un doctest compile-fail supplémentaire interdit `transactionSubscribe` sur `SolanaStandardWsSession`.
|
||||||
|
|
||||||
|
Comptages attendus :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport unit 335
|
||||||
|
Transport public API 40
|
||||||
|
release completeness 30
|
||||||
|
doctests compile-fail 5
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_helius_transactions.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_protocol_session.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_helius_transactions.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
deltas/0.2.8/pre.008.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. Hors scope
|
||||||
|
|
||||||
|
Restent hors de `pre.008` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
compliance finale Helius + standard/HTTP pre.009
|
||||||
|
smoke Helius live + cargo tree final pre.010
|
||||||
|
fermeture docs/indexes + prompt 0.2.9 pre.011
|
||||||
|
LaserStream gRPC futur backend séparé
|
||||||
|
Gatekeeper/preconf WebSocket hors scope 0.2.8
|
||||||
|
```
|
||||||
|
|
||||||
|
## 13. 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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Critère de fermeture : zéro warning, audit clean, **335 unit / 40 public API / 30 completeness / 5 doctests** et workspace vert.
|
||||||
262
deltas/0.2.8/pre.009.md
Normal file
262
deltas/0.2.8/pre.009.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.009.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.009` — compliance Helius courant + `slotsUpdates` + non-régressions finales
|
||||||
|
|
||||||
|
## 1. Base et objet
|
||||||
|
|
||||||
|
Base appliquée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.8
|
||||||
|
```
|
||||||
|
|
||||||
|
Le checkpoint opérateur `pre.008` est intégralement vert :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py clean
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
Transport unit 335/335
|
||||||
|
Transport public API 40/40
|
||||||
|
Transport release completeness 30/30
|
||||||
|
Transport doctests 5/5
|
||||||
|
cargo test --workspace OK
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.009` réalise la compliance finale Helius WebSocket prévue, mais le réaudit courant révèle une évolution de la documentation provider qui impose une petite extension fonctionnelle : `slotsUpdatesSubscribe`/`slotsUpdatesUnsubscribe` est désormais retenu comme disponible sur Helius.
|
||||||
|
|
||||||
|
## 2. Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.8-pre.9
|
||||||
|
commit attendu = v0.2.8-pre.009
|
||||||
|
Git tag = aucun tag prerelease
|
||||||
|
```
|
||||||
|
|
||||||
|
Le header root `Cargo.toml` passe en version `233`.
|
||||||
|
|
||||||
|
## 3. Réconciliation documentaire Helius du 2026-08-23
|
||||||
|
|
||||||
|
Sources officielles relues :
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://www.helius.dev/docs/rpc/websocket
|
||||||
|
https://www.helius.dev/docs/api-reference/rpc/websocket/slotsupdatessubscribe
|
||||||
|
https://www.helius.dev/docs/api-reference/rpc/websocket/slotsupdatesunsubscribe
|
||||||
|
https://www.helius.dev/docs/api-reference/rpc/websocket/blocksubscribe
|
||||||
|
https://www.helius.dev/docs/api-reference/rpc/websocket/votesubscribe
|
||||||
|
https://www.helius.dev/docs/api-reference/rpc/websocket/accountsubscribe
|
||||||
|
https://www.helius.dev/docs/api-reference/rpc/websocket/transactionsubscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
État courant :
|
||||||
|
|
||||||
|
```text
|
||||||
|
overview LaserStream WebSocket
|
||||||
|
-> annonce le support du jeu complet des méthodes standard
|
||||||
|
|
||||||
|
blockSubscribe
|
||||||
|
-> page spécifique : unstable + explicitement non supporté Helius
|
||||||
|
|
||||||
|
voteSubscribe
|
||||||
|
-> page spécifique : unstable + explicitement non supporté Helius
|
||||||
|
|
||||||
|
slotsUpdatesSubscribe
|
||||||
|
-> page spécifique : unstable
|
||||||
|
-> endpoints Helius mainnet/devnet
|
||||||
|
-> request, notification et subscription ID documentés
|
||||||
|
-> aucune mention de non-support Helius
|
||||||
|
|
||||||
|
slotsUpdatesUnsubscribe
|
||||||
|
-> subscriptionId entier
|
||||||
|
-> result bool
|
||||||
|
```
|
||||||
|
|
||||||
|
Arbitrage KSP : lorsqu'une formule globale et une page de méthode divergent, **la page spécifique prévaut**.
|
||||||
|
|
||||||
|
Surface Helius retenue après réaudit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
standard : account, logs, program, root, signature, slot, slotsUpdates
|
||||||
|
extension: transaction
|
||||||
|
absent : block, vote
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. `slotsUpdates` rejoint la façade Helius
|
||||||
|
|
||||||
|
`HeliusLaserStreamWsSession` expose désormais :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub async fn slots_updates_subscribe(
|
||||||
|
&self,
|
||||||
|
) -> ksp_core_lib::Result<crate::WsSubscription<crate::SolanaSlotUpdate>>
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun nouveau DTO ni nouveau wire n'est créé. La façade délègue au même chemin déjà stabilisé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HeliusLaserStreamWsSession
|
||||||
|
-> physical_session()
|
||||||
|
-> WsSession::slots_updates_subscribe()
|
||||||
|
-> WsSubscriptionKind::SlotsUpdates
|
||||||
|
-> slotsUpdatesSubscribe
|
||||||
|
-> slotsUpdatesNotification
|
||||||
|
-> slotsUpdatesUnsubscribe via handle
|
||||||
|
```
|
||||||
|
|
||||||
|
Le caractère unstable reste celui de la famille standard : le warning central `warn_if_unstable()` est donc conservé automatiquement.
|
||||||
|
|
||||||
|
## 5. Capability guards réconciliés
|
||||||
|
|
||||||
|
Le compile-fail historique :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Helius -X-> slotsUpdatesSubscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
est supprimé car il contredirait la surface courante.
|
||||||
|
|
||||||
|
Restent obligatoires :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Helius -X-> blockSubscribe
|
||||||
|
Helius -X-> voteSubscribe
|
||||||
|
Helius -X-> into_inner
|
||||||
|
SolanaStandard -X-> transactionSubscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun escape hatch vers le `WsSession` générique n'est ajouté.
|
||||||
|
|
||||||
|
## 6. `accountSubscribe` enhanced : pas de wire inventé
|
||||||
|
|
||||||
|
L'overview Helius mentionne désormais des « advanced filters » sur `accountSubscribe`, mais la page API spécifique ne publie actuellement que :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pubkey
|
||||||
|
encoding
|
||||||
|
commitment
|
||||||
|
notifyOn deprecated/no-op
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun DTO provider-specific supplémentaire n'est ajouté tant qu'un wire exact n'est pas publié. `notifyOn` reste volontairement absent.
|
||||||
|
|
||||||
|
## 7. Canaris de compliance transversaux
|
||||||
|
|
||||||
|
Trois canaris sont ajoutés dans `tests/release_completeness.rs`.
|
||||||
|
|
||||||
|
### 7.1 HTTP + Standard WebSocket
|
||||||
|
|
||||||
|
Ils verrouillent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP current 52
|
||||||
|
HTTP historical 14
|
||||||
|
Standard WS 9 familles
|
||||||
|
Standard operations 18 subscribe/unsubscribe
|
||||||
|
```
|
||||||
|
|
||||||
|
Les neuf méthodes typed de `SolanaStandardWsSession` restent adressables.
|
||||||
|
|
||||||
|
### 7.2 Helius courant
|
||||||
|
|
||||||
|
Ils verrouillent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
7 familles standard :
|
||||||
|
account
|
||||||
|
logs
|
||||||
|
program
|
||||||
|
root
|
||||||
|
signature
|
||||||
|
slot
|
||||||
|
slotsUpdates
|
||||||
|
|
||||||
|
1 extension :
|
||||||
|
transaction
|
||||||
|
|
||||||
|
absentes :
|
||||||
|
block
|
||||||
|
vote
|
||||||
|
```
|
||||||
|
|
||||||
|
Le source canary vérifie également que le guard `unsupported_slots_updates` a disparu tandis que `unsupported_block` et `unsupported_vote` restent présents.
|
||||||
|
|
||||||
|
### 7.3 Config / secret / dependency firewall
|
||||||
|
|
||||||
|
La compliance finale vérifie durablement :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport -X-> Config/Store/Program/tracing direct
|
||||||
|
Config -> Transport présent
|
||||||
|
Config mappe WsProtocolKind::HeliusLaserStream
|
||||||
|
example Transport contient kind = helius_laserstream
|
||||||
|
example Transport utilise ${KSP_SECRET_HELIUS_API_KEY...}
|
||||||
|
.env.example inventorie KSP_SECRET_HELIUS_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune nouvelle dépendance n'est introduite.
|
||||||
|
|
||||||
|
## 8. Public API et fixture Helius standard
|
||||||
|
|
||||||
|
Un public-API canary supplémentaire adresse `HeliusLaserStreamWsSession::slots_updates_subscribe` et le DTO partagé `SolanaSlotUpdate`.
|
||||||
|
|
||||||
|
Le fixture Helius standard existant passe de six à sept familles et vérifie la paire exacte :
|
||||||
|
|
||||||
|
```text
|
||||||
|
slotsUpdatesSubscribe params []
|
||||||
|
slotsUpdatesUnsubscribe params [remoteSubscriptionId]
|
||||||
|
```
|
||||||
|
|
||||||
|
Le lifecycle, le decoder, le remote-ID mapping et l'unsubscribe restent ceux du moteur commun.
|
||||||
|
|
||||||
|
## 9. Comptages attendus
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport unit 335
|
||||||
|
Transport public API 41
|
||||||
|
release completeness 33
|
||||||
|
doctests compile-fail 4
|
||||||
|
```
|
||||||
|
|
||||||
|
La diminution de 5 à 4 doctests est intentionnelle : le compile-fail Helius `slotsUpdatesSubscribe` est retiré parce que cette API devient supportée.
|
||||||
|
|
||||||
|
## 10. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_cluster.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_protocol_session.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/ws_helius_standard.rs
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
deltas/0.2.8/pre.009.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. Hors scope
|
||||||
|
|
||||||
|
Restent hors `pre.009` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
smoke Helius live opt-in + README/USAGE + cargo tree final pre.010
|
||||||
|
fermeture workspace/docs/indexes + prompt 0.2.9 pre.011
|
||||||
|
LaserStream gRPC futur backend distinct
|
||||||
|
Gatekeeper beta / preconfirmations hors 0.2.8
|
||||||
|
```
|
||||||
|
|
||||||
|
## 12. 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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Critère de fermeture : zéro warning, audit clean, **335 unit / 41 public API / 33 completeness / 4 doctests** et workspace vert.
|
||||||
73
deltas/0.2.8/pre.010.md
Normal file
73
deltas/0.2.8/pre.010.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.010.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.010` — documentation Helius finale + stratégie smoke live + cargo tree final
|
||||||
|
|
||||||
|
## Objet
|
||||||
|
|
||||||
|
Fermer la documentation d’usage de la surface Helius WebSocket acquise en `pre.009`, décider explicitement si un smoke Helius live peut être committé sans violer les frontières KSP, puis demander le dernier audit du graphe Cargo avant la tranche de clôture.
|
||||||
|
|
||||||
|
## Décision smoke live
|
||||||
|
|
||||||
|
Aucun nouveau smoke Helius live n’est ajouté dans cette tranche.
|
||||||
|
|
||||||
|
Motif :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Transport ne peut ni lire KSP_SECRET_HELIUS_API_KEY ni dépendre de Config.
|
||||||
|
Config ne doit pas devenir la destination générale des futurs smokes Config + autre crate.
|
||||||
|
Le smoke Config -> Transport historique reste une exception transitoire déjà documentée.
|
||||||
|
```
|
||||||
|
|
||||||
|
Le propriétaire durable sera la future surface KSP d’intégration/orchestration. Son scénario minimal devra utiliser `helius_devnet`, ouvrir `HeliusLaserStreamWsSession`, faire `slotSubscribe`, attendre une notification sous timeout, unsubscribe puis close. `transactionSubscribe` pourra rester un opt-in additionnel soumis aux droits provider.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
`README.md` et `USAGE.md` de Transport documentent désormais :
|
||||||
|
|
||||||
|
- Helius = `account/logs/program/root/signature/slot/slotsUpdates` + `transaction` ;
|
||||||
|
- `block` et `vote` absents ;
|
||||||
|
- `slotsUpdates` unstable ;
|
||||||
|
- heartbeat Helius control-frame Ping à 60 s ;
|
||||||
|
- secret Helius résolu par Config, jamais lu par Transport ;
|
||||||
|
- LaserStream gRPC distinct du namespace WebSocket ;
|
||||||
|
- stratégie de smoke live et future ownership d’intégration.
|
||||||
|
|
||||||
|
## Canari de release
|
||||||
|
|
||||||
|
Un canari `release_completeness` supplémentaire vérifie que :
|
||||||
|
|
||||||
|
- Transport ne dépend toujours pas de Config ;
|
||||||
|
- le smoke WebSocket Transport ne lit ni `KSP_SECRET_HELIUS_API_KEY` ni `ConfigEnvironment` ;
|
||||||
|
- aucun `helius_websocket_smoke.rs` n’est ajouté à Config ;
|
||||||
|
- README/USAGE portent explicitement la décision et les commandes `cargo tree` ;
|
||||||
|
- `.env.example` conserve l’inventaire `KSP_SECRET_HELIUS_API_KEY`.
|
||||||
|
|
||||||
|
## Version et comptages attendus
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.8-pre.10
|
||||||
|
commit attendu = v0.2.8-pre.010
|
||||||
|
Git tag = aucun
|
||||||
|
|
||||||
|
Transport unit 335
|
||||||
|
Transport public API 41
|
||||||
|
release completeness 34
|
||||||
|
Transport doctests 4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audit 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 --workspace
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
cargo tree --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
`pre.011` ne s’ouvre qu’après gate vert et inspection des graphes résolus.
|
||||||
207
deltas/0.2.8/pre.011.md
Normal file
207
deltas/0.2.8/pre.011.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<!-- file: deltas/0.2.8/pre.011.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-pre.011` — candidate finale + indexes + prompt `0.2.9`
|
||||||
|
|
||||||
|
## Objet
|
||||||
|
|
||||||
|
Fermer la candidate documentaire de `0.2.8` après le gate opérateur complet de `pre.010`, synchroniser les index qui décrivaient encore des états intermédiaires de la release et préparer un prompt de démarrage `0.2.9` autonome, conforme à `docs/rules/PROMPT_STRUCTURE.md`, avant la publication stable `rel.001`.
|
||||||
|
|
||||||
|
Cette tranche ne modifie aucune capacité runtime HTTP/WebSocket, aucun contrat Config et aucune dépendance Rust.
|
||||||
|
|
||||||
|
## Base et preuve opérateur héritée
|
||||||
|
|
||||||
|
Le gate `0.2.8-pre.10` fourni le 2026-08-23 est entièrement vert :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py clean
|
||||||
|
General Rust rule audit clean
|
||||||
|
Rust export completeness audit 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit clean
|
||||||
|
cargo check --workspace OK / sans warning
|
||||||
|
cargo clippy --workspace --all-targets OK / sans warning
|
||||||
|
Transport unit 335/335
|
||||||
|
Transport public API 41/41
|
||||||
|
release completeness 34/34
|
||||||
|
Transport doctests 4/4
|
||||||
|
cargo test --workspace OK
|
||||||
|
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 graphe Transport conserve uniquement ses dépendances acquises. Les doublons ciblés visibles dans ce sous-graphe (`syn` 2/3 et `webpki-roots` 0.26/1.0) sont transitifs. Les autres duplications du workspace proviennent principalement des stacks Tauri/GTK, JSON Schema et Wallet/Solana crypto ; aucune dérive nouvelle propre à `0.2.8` n'est identifiée.
|
||||||
|
|
||||||
|
Verdict hérité : **`pre.010` DONE.**
|
||||||
|
|
||||||
|
## Candidate fonctionnelle `0.2.8`
|
||||||
|
|
||||||
|
L'état que `pre.011` fige au niveau candidate reste :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP 52 méthodes current + 14 historical
|
||||||
|
Solana Standard WebSocket 9 familles / 18 subscribe-unsubscribe
|
||||||
|
Helius standard réutilisé account/logs/program/root/signature/slot/slotsUpdates
|
||||||
|
Helius extension transactionSubscribe / transactionUnsubscribe
|
||||||
|
Helius absent block / vote
|
||||||
|
slotsUpdates unstable
|
||||||
|
heartbeat Helius WebSocket Ping control frame / 60 s
|
||||||
|
Config V2 helius_laserstream + provenance/redaction secret
|
||||||
|
moteur physique un seul WsSession actor partagé
|
||||||
|
smoke Helius live reporté vers future intégration/orchestration
|
||||||
|
LaserStream gRPC backend distinct, hors namespace WebSocket
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun changement runtime n'est nécessaire dans `pre.011`.
|
||||||
|
|
||||||
|
## Prompt `0.2.9`
|
||||||
|
|
||||||
|
Création de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/014-V0_2_9_START_PROMPT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Titre :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9 — Yellowstone gRPC standard/provider-neutral
|
||||||
|
```
|
||||||
|
|
||||||
|
Le prompt a été construit après relecture de :
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/rules/PROMPT_STRUCTURE.md
|
||||||
|
prompts/010-V0_2_5_START_PROMPT.md
|
||||||
|
prompts/011-V0_2_6_START_PROMPT.md
|
||||||
|
prompts/012-V0_2_7_START_PROMPT.md
|
||||||
|
prompts/013-V0_2_8_START_PROMPT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Le squelette reprend la structure stricte récente de `0.2.7`/`0.2.8`, tout en conservant la profondeur métier/threat-model/sizing des prompts `0.2.5`/`0.2.6`.
|
||||||
|
|
||||||
|
Le prompt contient explicitement les rubriques requises par `PROMPT_STRUCTURE.md` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
identité/base exacte
|
||||||
|
mission/résultat
|
||||||
|
sources internes ordonnées
|
||||||
|
sources externes à réauditer
|
||||||
|
état stable à préserver
|
||||||
|
frontières architecturales/dependencies
|
||||||
|
décisions acquises/questions ouvertes
|
||||||
|
objectifs/livrables/hors périmètre
|
||||||
|
sécurité/API/lifecycle/resources
|
||||||
|
pre.001 détaillée + gate de sortie
|
||||||
|
prévision souple initiale détaillée
|
||||||
|
version/deltas/commits/archives/tags
|
||||||
|
validation opérateur
|
||||||
|
critères de clôture
|
||||||
|
release suivante
|
||||||
|
instruction d'ouverture
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gate `0.2.9-pre.001`
|
||||||
|
|
||||||
|
Le prompt interdit l'implémentation lourde Yellowstone avant :
|
||||||
|
|
||||||
|
- audit du service/proto upstream réellement courant ;
|
||||||
|
- classification standard/provider-extension de chaque RPC/message ;
|
||||||
|
- audit de `Subscribe`, unary RPCs, replay/from_slot et extensions deshred ;
|
||||||
|
- comparaison des stratégies `yellowstone-grpc-client`, `yellowstone-grpc-proto + tonic` ou wire KSP borné ;
|
||||||
|
- audit licence/MSRV/features/build-dependencies/transitifs ;
|
||||||
|
- architecture gRPC distincte de HTTP/WS ;
|
||||||
|
- threat model ;
|
||||||
|
- sizing et recalibrage du forecast.
|
||||||
|
|
||||||
|
Le snapshot upstream du 2026-08-23 est uniquement informatif. Le repository/master observé comporte un workspace AGPL-3.0 et des versions Yellowstone/Tonic/Prost qui ne sont pas nécessairement synchronisées avec les releases/crates stables. Le prompt exige donc l'audit de la licence exacte de chaque crate/proto/source réellement utilisé avant toute dépendance, copie ou génération vendored dans KSP MIT.
|
||||||
|
|
||||||
|
### Forecast initial souple `0.2.9`
|
||||||
|
|
||||||
|
Le prompt fournit un forecast de départ détaillé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
pre.001 audit/matrice/licences/dependencies/architecture/threat-model/sizing
|
||||||
|
pre.002 stratégie proto/client + settings/errors/façade provider-neutral
|
||||||
|
pre.003 connexion/TLS/metadata + lifecycle minimal + unary canaries
|
||||||
|
pre.004 SubscribeRequest foundation + validations/bounds communs
|
||||||
|
pre.005 Accounts + Slots
|
||||||
|
pre.006 Transactions + transaction_status
|
||||||
|
pre.007 Blocks + block_meta + entry / autres variantes retenues
|
||||||
|
pre.008 stream bidirectionnel + mutation/Ping/backpressure/shutdown
|
||||||
|
pre.009 reconnect/resubscribe/from_slot/replay/adversarial
|
||||||
|
pre.010 Config -> Transport gRPC si retenu par l'audit
|
||||||
|
pre.011 compliance Yellowstone + non-régressions HTTP/WS
|
||||||
|
pre.012 live opt-in si sûr + README/USAGE + cargo tree final
|
||||||
|
pre.013 workspace final + plan/matrice/indexes + prompt 0.2.10
|
||||||
|
rel.001 publication stable stricte
|
||||||
|
```
|
||||||
|
|
||||||
|
Ce forecast est explicitement recalibrable en `pre.001`, autorise fixes et prereleases supplémentaires, et impose un split avant client lourd si la surface réelle est trop grande pour une release concrète raisonnable.
|
||||||
|
|
||||||
|
## Synchronisation documentaire
|
||||||
|
|
||||||
|
Les index et la séquence fonctionnelle sont mis à jour pour refléter l'état candidate réel de `0.2.8` et l'existence du prompt `0.2.9` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/000-README.md
|
||||||
|
docs/plans/000-README.md
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/000-README.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
prompts/000-README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
`ROADMAP.md` et `CHANGELOG.md` restent volontairement inchangés : leur synchronisation stable appartient à `rel.001`, conformément au workflow KSP.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.8-pre.11
|
||||||
|
livraison = 0.2.8-pre.011
|
||||||
|
commit attendu = v0.2.8-pre.011
|
||||||
|
Git tag = aucun
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers de la livraison
|
||||||
|
|
||||||
|
Overlay attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
deltas/0.2.8/pre.011.md
|
||||||
|
docs/000-README.md
|
||||||
|
docs/plans/000-README.md
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/000-README.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
prompts/000-README.md
|
||||||
|
prompts/014-V0_2_9_START_PROMPT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Comptages Transport attendus, inchangés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
unit 335
|
||||||
|
public API 41
|
||||||
|
release completeness 34
|
||||||
|
doctests 4
|
||||||
|
```
|
||||||
|
|
||||||
|
Les `cargo tree` n'ont pas besoin d'être rejoués pour cette tranche documentaire/versionnée : aucun manifest de dépendances n'est modifié hors la version workspace, et les trois graphes finaux ont déjà été fournis au gate `pre.010`.
|
||||||
|
|
||||||
|
Si ce gate est vert, la prochaine tranche est `0.2.8-rel.001`. `pre.012+` reste possible uniquement si un défaut réel est découvert.
|
||||||
246
deltas/0.2.8/rel.001.md
Normal file
246
deltas/0.2.8/rel.001.md
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
<!-- file: deltas/0.2.8/rel.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.8-rel.001` — publication stable Helius LaserStream WebSocket
|
||||||
|
|
||||||
|
## 1. Base requise
|
||||||
|
|
||||||
|
Base directe attendue :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.8-pre.011
|
||||||
|
workspace.package.version = 0.2.8-pre.11
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit attendu pour cette livraison :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.8-rel.001
|
||||||
|
```
|
||||||
|
|
||||||
|
Tag stable attendu après validation :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.8
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Objectif
|
||||||
|
|
||||||
|
Publier `0.2.8 — Helius LaserStream WebSocket` sans ajouter de capacité fonctionnelle après la candidate.
|
||||||
|
|
||||||
|
`rel.001` :
|
||||||
|
|
||||||
|
- passe `workspace.package.version` de `0.2.8-pre.11` à `0.2.8` ;
|
||||||
|
- clôt `ROADMAP.md`, le plan `015` et la matrice `validation/011` ;
|
||||||
|
- ajoute l’entrée stable `0.2.8` au `CHANGELOG.md` ;
|
||||||
|
- passe `Helius WS` à `Stable` dans l’inventaire composants ;
|
||||||
|
- synchronise les index/documentation de séquence vers le statut publié ;
|
||||||
|
- conserve `prompts/014-V0_2_9_START_PROMPT.md` inchangé comme contrat actif pour `0.2.9`.
|
||||||
|
|
||||||
|
Aucun fichier Rust, aucune API publique, aucun schema/config runtime, aucune dépendance et aucune feature ne changent dans cette livraison.
|
||||||
|
|
||||||
|
## 3. Preuve candidate finale acquise
|
||||||
|
|
||||||
|
Le checkpoint opérateur fourni le **23 août 2026** après application de `0.2.8-pre.011` est intégralement vert :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all OK
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py OK
|
||||||
|
General Rust rule audit clean
|
||||||
|
Rust export completeness audit 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit clean
|
||||||
|
cargo check --workspace OK
|
||||||
|
cargo clippy --workspace --all-targets OK
|
||||||
|
cargo test -p ksp-onchain-transport-lib OK
|
||||||
|
unit 335 passed
|
||||||
|
public API 41 passed
|
||||||
|
release completeness 34 passed
|
||||||
|
doctests compile-fail 4 passed
|
||||||
|
cargo test --workspace OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Les suites Config/Core/Logging/Wallet/Desks passent également. Les smokes réseau, benchmarks et probes operator-only restent ignorés par défaut conformément à leur contrat.
|
||||||
|
|
||||||
|
Les trois audits Cargo ont été exécutés en `pre.010` puis conservés valides par `pre.011`, qui ne modifie aucune dépendance :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo tree -p ksp-onchain-transport-lib
|
||||||
|
cargo tree -p ksp-onchain-transport-lib --duplicates
|
||||||
|
cargo tree --duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
Transport ne présente aucune nouvelle dépendance Helius/gRPC/Config ; les doublons ciblés `syn 2/3` et `webpki-roots 0.26/1.0` sont transitifs et non bloquants.
|
||||||
|
|
||||||
|
## 4. Surface stable publiée
|
||||||
|
|
||||||
|
### HTTP hérité
|
||||||
|
|
||||||
|
```text
|
||||||
|
52/52 méthodes HTTP courantes typées
|
||||||
|
14/14 méthodes historiques Deprecated/Removed conservées
|
||||||
|
KSP-TRANSPORT-007 appliqué à la surface typed
|
||||||
|
write submissions : aucun resend après dispatch ambigu
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket Solana standard hérité
|
||||||
|
|
||||||
|
```text
|
||||||
|
9 familles standard
|
||||||
|
18/18 opérations subscribe/unsubscribe
|
||||||
|
stable : account, logs, program, root, signature, slot
|
||||||
|
unstable : block, slotsUpdates, vote
|
||||||
|
```
|
||||||
|
|
||||||
|
### Helius LaserStream WebSocket
|
||||||
|
|
||||||
|
Surface standard réutilisée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
account
|
||||||
|
logs
|
||||||
|
program
|
||||||
|
root
|
||||||
|
signature
|
||||||
|
slot
|
||||||
|
slotsUpdates # unstable
|
||||||
|
```
|
||||||
|
|
||||||
|
Extension Helius typée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
transactionSubscribe
|
||||||
|
transactionUnsubscribe
|
||||||
|
transactionNotification
|
||||||
|
```
|
||||||
|
|
||||||
|
Absents de la façade Helius :
|
||||||
|
|
||||||
|
```text
|
||||||
|
block
|
||||||
|
vote
|
||||||
|
```
|
||||||
|
|
||||||
|
Contrats stabilisés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
HeliusLaserStreamWsSession façade publique dédiée
|
||||||
|
WsSession actor physique unique partagé avec le standard
|
||||||
|
aucun second client/socket/registry/scheduler
|
||||||
|
capability guards avant I/O
|
||||||
|
heartbeat Helius-only = WebSocket Ping control frame / 60 s
|
||||||
|
reconnect/resubscribe/remap/unsubscribe races bornés
|
||||||
|
backpressure isolé par subscription
|
||||||
|
payload oversized borné avant decode métier
|
||||||
|
erreurs RPC provider n’abattent pas la session physique
|
||||||
|
Debug/snapshots Helius sans payload provider brut
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config et secrets
|
||||||
|
|
||||||
|
```text
|
||||||
|
std.transport V2 comprend helius_laserstream
|
||||||
|
profiles Helius mainnet/devnet
|
||||||
|
KSP_SECRET_HELIUS_API_KEY appartient à Config
|
||||||
|
redaction segmentaire des URLs résolues
|
||||||
|
Config -> Transport autorisé
|
||||||
|
Transport -X-> Config / environnement
|
||||||
|
```
|
||||||
|
|
||||||
|
Le smoke Helius live cross-crates est reporté vers une future surface d’intégration/orchestration dédiée afin de ne pas faire de Config une destination générale de smoke et de ne pas faire lire le secret par Transport.
|
||||||
|
|
||||||
|
LaserStream gRPC / Yellowstone reste un backend distinct : aucune promesse gRPC/replay n’est attachée au contrat WebSocket `0.2.8`.
|
||||||
|
|
||||||
|
## 5. Fichiers ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
deltas/0.2.8/rel.001.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Fichiers modifiés
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
ROADMAP.md
|
||||||
|
CHANGELOG.md
|
||||||
|
docs/000-README.md
|
||||||
|
docs/architecture/004-COMPONENT_INVENTORY.md
|
||||||
|
docs/plans/000-README.md
|
||||||
|
docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md
|
||||||
|
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/000-README.md
|
||||||
|
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
|
||||||
|
prompts/000-README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Volontairement inchangé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/014-V0_2_9_START_PROMPT.md
|
||||||
|
crates/**
|
||||||
|
config/**
|
||||||
|
.env.example
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Décisions de clôture
|
||||||
|
|
||||||
|
Aucune nouvelle décision de protocole n’est introduite par `rel.001`. La publication confirme :
|
||||||
|
|
||||||
|
1. Helius LaserStream WebSocket reste une façade provider dédiée sur le moteur/actor WebSocket partagé ;
|
||||||
|
2. la surface publiée est `7 familles standard + transaction`, avec `block/vote` absents ;
|
||||||
|
3. `slotsUpdates` reste exposé comme unstable ;
|
||||||
|
4. le heartbeat Ping 60 s est provider-owned et Helius-only ;
|
||||||
|
5. Config reste l’unique propriétaire du secret Helius et Transport ne lit pas l’environnement ;
|
||||||
|
6. le smoke live cross-crates Helius attend une surface d’intégration/orchestration dédiée ;
|
||||||
|
7. HTTP 52+14 et Standard WS 18/18 restent des non-régressions obligatoires ;
|
||||||
|
8. `0.2.9` ouvre Yellowstone gRPC standard/provider-neutral par audit/sizing avant dépendances ou implémentation lourde.
|
||||||
|
|
||||||
|
## 8. Validation finale après application
|
||||||
|
|
||||||
|
Le passage du signal Cargo à la version stable doit être revalidé avant commit/tag :
|
||||||
|
|
||||||
|
```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 --workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
Les graphes Cargo n’ont pas besoin d’être rejoués si l’overlay appliqué est exactement `0.2.8-rel.001`, car cette livraison ne change aucune dépendance. Les smokes live n’ont pas besoin d’être rejoués non plus : aucune source runtime n’est modifiée.
|
||||||
|
|
||||||
|
Comptages Transport attendus :
|
||||||
|
|
||||||
|
```text
|
||||||
|
unit = 335
|
||||||
|
public API = 41
|
||||||
|
release completeness = 34
|
||||||
|
doctests compile-fail = 4
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Commit et tag stable
|
||||||
|
|
||||||
|
Après succès du gate :
|
||||||
|
|
||||||
|
```text
|
||||||
|
commit : v0.2.8-rel.001
|
||||||
|
tag : v0.2.8
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag intermédiaire de prerelease/fix/rel n’est requis.
|
||||||
|
|
||||||
|
## 10. Suite
|
||||||
|
|
||||||
|
Après création du tag stable `v0.2.8`, ouvrir :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.9-pre.001
|
||||||
|
```
|
||||||
|
|
||||||
|
avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
prompts/014-V0_2_9_START_PROMPT.md
|
||||||
|
```
|
||||||
|
|
||||||
|
La première tranche est un gate strict de relecture, audit Yellowstone courant, architecture, threat model, dépendances/licences/MSRV/features et sizing. La prévision `pre.001 -> pre.013` du prompt est explicitement souple et doit être recalibrée avant toute implémentation gRPC lourde.
|
||||||
139
deltas/0.2.9/pre.001-fix.001.md
Normal file
139
deltas/0.2.9/pre.001-fix.001.md
Normal 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 `15–20 minutes`, mais elle ne rendait pas assez visible le contrat normatif complet de `PROMPT_STRUCTURE.md` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
une prerelease estimée > 15–20 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 = 15–20 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 `165–220 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
|
||||||
|
15–20 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
|
||||||
|
```
|
||||||
154
deltas/0.2.9/pre.001-fix.002.md
Normal file
154
deltas/0.2.9/pre.001-fix.002.md
Normal 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
377
deltas/0.2.9/pre.001.md
Normal 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 ~15–20 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`.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user