Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d70c3a1672 | |||
| 7cdf5e80c9 | |||
| d8bfd7cd2e | |||
| 9c0d4fc197 | |||
| 7c12ec886b | |||
| 7eb6dec809 | |||
| 68f4384c5b | |||
| 9cc140fb84 | |||
| 3b64d1e0ec | |||
| 56b9ce6abc | |||
| 1c8d69778b | |||
| 8e739b9e55 | |||
| 53dbb5bccd | |||
| 92224e5ac6 | |||
| cbb4e7b0de | |||
| 38fd62c256 | |||
| f2a3ec62aa | |||
| b3363073c4 | |||
| c94a54f3e3 | |||
| 0871b85df9 | |||
| 315e7e67e5 | |||
| df95f2f558 | |||
| f0865d5137 |
@@ -1,5 +1,5 @@
|
|||||||
# file: .env.example
|
# file: .env.example
|
||||||
# version: 5
|
# version: 6
|
||||||
|
|
||||||
# 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,9 @@ 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.
|
# Helius API key used by the LaserStream WebSocket endpoint in config/examples/std.transport.example.json.
|
||||||
# Keep provider credentials in a KSP_SECRET_* variable; do not copy a real credential-bearing URL into committed JSON.
|
# Keep the real credential only in the process environment or local .env; never commit it.
|
||||||
# KSP_SECRET_SOLANA_WS_URL=wss://provider.example/?api-key=replace-me
|
# 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
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
<!-- file: CHANGELOG.md -->
|
<!-- file: CHANGELOG.md -->
|
||||||
<!-- version: 11 -->
|
<!-- version: 12 -->
|
||||||
|
|
||||||
# Changelog KSP
|
# Changelog KSP
|
||||||
|
|
||||||
Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`.
|
Ce changelog résume uniquement les releases KSP considérées comme stables, dans l'ordre chronologique décroissant. Les détails de chaque livraison restent dans `deltas/`.
|
||||||
|
|
||||||
|
## 0.2.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é.
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 216
|
# version: 236
|
||||||
|
|
||||||
[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.8"
|
||||||
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"
|
||||||
@@ -24,7 +24,7 @@ fs2 = { version = "^0.4" }
|
|||||||
futures-util = { version = "^0.3", default-features = false }
|
futures-util = { version = "^0.3", 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 }
|
jsonschema = { version = "^0.51", default-features = false }
|
||||||
reqwest = { version = "^0.13", 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 }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 82 -->
|
<!-- version: 83 -->
|
||||||
|
|
||||||
# Roadmap KSP
|
# Roadmap KSP
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ 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.
|
- [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.
|
||||||
- [ ] `0.2.10` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR.
|
- [ ] `0.2.10` — Introduire `ksp-offchain-transport-lib` avec un premier lecteur de prix, au minimum SOL/USD et SOL/EUR.
|
||||||
- [ ] `0.2.11` — Introduire une petite application desk de visualisation/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 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é.
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,8 @@
|
|||||||
},
|
},
|
||||||
"kind": {
|
"kind": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"solana_standard"
|
"solana_standard",
|
||||||
|
"helius_laserstream"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
<!-- file: crates/ksp-config-lib/USAGE.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 11 -->
|
||||||
|
|
||||||
# Utilisation de ksp-config-lib
|
# Utilisation de ksp-config-lib
|
||||||
|
|
||||||
@@ -140,7 +140,9 @@ let ws_settings = transport.ws_settings();
|
|||||||
let _ = (http_settings, ws_settings);
|
let _ = (http_settings, ws_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` V2 conserve `retry` et `profiles[].endpoints[]` pour HTTP, ajoute `ws_defaults` et `profiles[].ws_endpoints[]`, puis accepte les protocoles WebSocket `kind = "solana_standard"` et `kind = "helius_laserstream"`. Ce second discriminateur appartient exclusivement au namespace WebSocket et mappe vers `WsProtocolKind::HeliusLaserStream`; il ne préfigure aucun contrat LaserStream gRPC. Un `ws_endpoints[].session` optionnel surcharge seulement les paramètres génériques de `WsSessionSettings`.
|
||||||
|
|
||||||
|
Pour Helius LaserStream WebSocket, l'exemple versionné couvre explicitement les deux réseaux supportés par ce contrat : mainnet via `wss://mainnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}` et devnet via `wss://devnet.helius-rpc.com/?api-key=${KSP_SECRET_HELIUS_API_KEY:-replace-me}`. Les deux réseaux vivent dans des profils Config distincts afin de ne pas mélanger des clusters dans un même profil logique. Config reste l'unique propriétaire de `KSP_SECRET_HELIUS_API_KEY` : il résout la clé dans l'URL effective et transmet au Transport un `WsEndpointUrl` utilisable au runtime. Dans `safe_value`, Config conserve les segments littéraux non sensibles d'une chaîne composée et remplace uniquement chaque segment secret par `********` ; les projections deviennent donc respectivement `wss://mainnet.helius-rpc.com/?api-key=********` et `wss://devnet.helius-rpc.com/?api-key=********`. Les représentations `Debug` restent sûres et n'exposent jamais la clé réelle. Transport ne lit jamais directement l'environnement.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-config-lib/src/transport.rs
|
// file: crates/ksp-config-lib/src/transport.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
/// 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 and optional WebSocket runtime contracts.
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
@@ -443,6 +443,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)
|
||||||
|
|||||||
@@ -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}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: 6
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fixture_transport_profile_maps_complete_runtime_contract() {
|
fn fixture_transport_profile_maps_complete_runtime_contract() {
|
||||||
@@ -44,7 +44,7 @@ fn fixture_transport_profile_maps_complete_runtime_contract() {
|
|||||||
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 +64,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,6 +266,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();
|
||||||
|
|||||||
@@ -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: 5
|
||||||
|
|
||||||
//! Workspace-level dependency policy canaries owned by the foundational KSP test surface.
|
//! Workspace-level dependency policy canaries owned by the foundational KSP test surface.
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ fn transport_manifest_preserves_ksp_dependency_firewall() {
|
|||||||
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("[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\"] }"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-onchain-transport-lib/Cargo.toml
|
# file: crates/ksp-onchain-transport-lib/Cargo.toml
|
||||||
# version: 5
|
# version: 6
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-onchain-transport-lib"
|
name = "ksp-onchain-transport-lib"
|
||||||
@@ -18,7 +18,7 @@ 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"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["net", "rt"] }
|
tokio = { workspace = true, features = ["io-util", "net", "rt", "test-util"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
||||||
<!-- version: 19 -->
|
<!-- version: 20 -->
|
||||||
|
|
||||||
# `ksp-onchain-transport-lib`
|
# `ksp-onchain-transport-lib`
|
||||||
|
|
||||||
@@ -199,6 +199,24 @@ 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`.
|
||||||
|
|
||||||
## 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 :
|
||||||
@@ -260,6 +278,8 @@ Le smoke WebSocket Transport cible uniquement la famille stable `slotSubscribe`
|
|||||||
|
|
||||||
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 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
- [`USAGE.md`](USAGE.md) — consommation directe, Config -> Transport, API typed/raw, smokes et inspection runtime ;
|
- [`USAGE.md`](USAGE.md) — consommation directe, Config -> Transport, API typed/raw, smokes et inspection runtime ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
|
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
|
||||||
<!-- version: 19 -->
|
<!-- version: 20 -->
|
||||||
|
|
||||||
# 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`.
|
||||||
@@ -358,7 +398,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 quatrième smoke 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 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,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
// version: 29
|
// version: 34
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -24,7 +24,13 @@
|
|||||||
//! 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.
|
||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
mod constants;
|
mod constants;
|
||||||
@@ -46,7 +52,9 @@ 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;
|
||||||
@@ -336,6 +344,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 +368,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 +398,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;
|
||||||
|
|||||||
@@ -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: 38
|
||||||
|
|
||||||
//! 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,87 @@ 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");
|
||||||
|
}
|
||||||
|
|||||||
@@ -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: 32
|
||||||
|
|
||||||
//! 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,230 @@ 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"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
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.
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/architecture/004-COMPONENT_INVENTORY.md -->
|
<!-- file: docs/architecture/004-COMPONENT_INVENTORY.md -->
|
||||||
<!-- version: 20 -->
|
<!-- version: 21 -->
|
||||||
|
|
||||||
# Inventaire initial des composants KSP
|
# Inventaire initial des composants KSP
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ Ce document maintient l'inventaire synthétique des composants retenus ou presse
|
|||||||
| Wallet Desk | `ksp-app-wallet-desk` | app | Stable | `0.2.6` | Wallet + Config composite + HTTP/balance |
|
| Wallet Desk | `ksp-app-wallet-desk` | app | Stable | `0.2.6` | Wallet + Config composite + HTTP/balance |
|
||||||
| Wallet V2 | `ksp-wallet-lib` | lib | Stable | `0.2.6` | wire/runtime V2 + API default/versionnée + migration explicite |
|
| Wallet V2 | `ksp-wallet-lib` | lib | Stable | `0.2.6` | wire/runtime V2 + API default/versionnée + migration explicite |
|
||||||
| Standard WS | `ksp-onchain-transport-lib` | lib | Stable | `0.2.7` | WebSocket Solana 18/18, sessions/subscriptions bornées |
|
| Standard WS | `ksp-onchain-transport-lib` | lib | Stable | `0.2.7` | WebSocket Solana 18/18, sessions/subscriptions bornées |
|
||||||
| Helius WS | `ksp-onchain-transport-lib` | lib | Retenu | `0.2.8` | LaserStream WebSocket comme extension du moteur standard |
|
| Helius WS | `ksp-onchain-transport-lib` | lib | Stable | `0.2.8` | LaserStream WS : 7 standard + transaction, actor partagé |
|
||||||
| Yellowstone | `ksp-onchain-transport-lib` | lib | Pressenti | `0.2.9` | client gRPC standard/provider-neutral |
|
| Yellowstone | `ksp-onchain-transport-lib` | lib | Pressenti | `0.2.9` | client gRPC standard/provider-neutral |
|
||||||
| Off-chain price | `ksp-offchain-transport-lib` | lib | Retenu | `0.2.10` | première abstraction/provider de prix SOL/USD, SOL/EUR |
|
| Off-chain price | `ksp-offchain-transport-lib` | lib | Retenu | `0.2.10` | première abstraction/provider de prix SOL/USD, SOL/EUR |
|
||||||
| Price Desk | nom à fixer | app | Retenu | `0.2.11` | visualisation/validation des prix + intégration Wallet Desk |
|
| Price Desk | nom à fixer | app | Retenu | `0.2.11` | visualisation/validation des prix + intégration Wallet Desk |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/000-README.md -->
|
<!-- file: docs/plans/000-README.md -->
|
||||||
<!-- version: 56 -->
|
<!-- version: 59 -->
|
||||||
|
|
||||||
# Plans KSP
|
# Plans KSP
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ Un plan décrit le périmètre, les décisions déjà acquises, les questions ou
|
|||||||
- [`012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](012-V0_2_5_WALLET_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.2.5 — Wallet foundation`, ouvert par `pre.001`, livré jusqu’à `pre.010`, renforcé par `pre.010-fix.001`–`fix.003` pour Dalek 3 et la normalisation Rust/audit structurel, puis publié par `rel.001`; il couvre `.kspwallet` V1, VIEW/OWNER, crypto, persistence, administration, transfer et compliance.
|
- [`012-V0_2_5_WALLET_FOUNDATION_PLAN.md`](012-V0_2_5_WALLET_FOUNDATION_PLAN.md) — plan historique clôturé de la release stable `0.2.5 — Wallet foundation`, ouvert par `pre.001`, livré jusqu’à `pre.010`, renforcé par `pre.010-fix.001`–`fix.003` pour Dalek 3 et la normalisation Rust/audit structurel, puis publié par `rel.001`; il couvre `.kspwallet` V1, VIEW/OWNER, crypto, persistence, administration, transfer et compliance.
|
||||||
- [`013-V0_2_6_WALLET_DESK_PLAN.md`](013-V0_2_6_WALLET_DESK_PLAN.md) — plan historique clôturé de la release stable `0.2.6 — Wallet Desk`, ouvert par `pre.001`, étendu en `pre.015`–`pre.017` au wire binaire `.kspwallet` V2, aux APIs multi-version et à la migration V1 -> V2, puis fermé par `pre.018`/`fix.001` avec le runtime Tauri packagé et le build final vert avant publication `rel.001`.
|
- [`013-V0_2_6_WALLET_DESK_PLAN.md`](013-V0_2_6_WALLET_DESK_PLAN.md) — plan historique clôturé de la release stable `0.2.6 — Wallet Desk`, ouvert par `pre.001`, étendu en `pre.015`–`pre.017` au wire binaire `.kspwallet` V2, aux APIs multi-version et à la migration V1 -> V2, puis fermé par `pre.018`/`fix.001` avec le runtime Tauri packagé et le build final vert avant publication `rel.001`.
|
||||||
- [`014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md`](014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md) — plan historique clôturé de la release stable `0.2.7 — WebSocket Solana standard`, ouvert par `pre.001`, exécuté jusqu’à `pre.014`, corrigé documentairement par `pre.014-fix.001` puis publié par `rel.001`; il conserve l’inventaire officiel 18 méthodes, le modèle session/subscription, le threat model, les preuves de compliance/smoke/dépendances et la préparation de `0.2.8`.
|
- [`014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md`](014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md) — plan historique clôturé de la release stable `0.2.7 — WebSocket Solana standard`, ouvert par `pre.001`, exécuté jusqu’à `pre.014`, corrigé documentairement par `pre.014-fix.001` puis publié par `rel.001`; il conserve l’inventaire officiel 18 méthodes, le modèle session/subscription, le threat model, les preuves de compliance/smoke/dépendances et la préparation de `0.2.8`.
|
||||||
|
- [`015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md`](015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md) — plan historique clôturé de la release stable `0.2.8 — Helius LaserStream WebSocket`, ouvert par `pre.001`, fermé techniquement/documentairement par `pre.011` puis publié par `rel.001`; il conserve la surface finale Helius `7 standard + transaction`, heartbeat/Config/secrets, l’historique des fixes heartbeat, la stratégie live architecture-safe, les graphes Cargo finaux et la préparation du prompt `0.2.9`.
|
||||||
|
|
||||||
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.
|
Le `pre.001` de chaque release fonctionnelle peut introduire son propre plan détaillé lorsque la release s'ouvre.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
<!-- file: docs/plans/002-FUNCTIONAL_RELEASE_SEQUENCE.md -->
|
||||||
<!-- version: 82 -->
|
<!-- version: 85 -->
|
||||||
|
|
||||||
# Séquence des releases fonctionnelles KSP
|
# Séquence des releases fonctionnelles KSP
|
||||||
|
|
||||||
@@ -465,7 +465,11 @@ La candidate atteint `pre.014` après matérialisation des 9 familles standard,
|
|||||||
|
|
||||||
### `0.2.8` — Helius LaserStream WebSocket
|
### `0.2.8` — Helius LaserStream WebSocket
|
||||||
|
|
||||||
Mission : étendre le moteur WebSocket standard avec les opérations/filtres/capabilities Helius ciblés sans copier le client.
|
Mission accomplie : étendre le moteur WebSocket standard avec la surface Helius LaserStream WebSocket actuelle sans copier le client/session actor. Le gate et l'historique complet sont conservés dans [`015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md`](015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md) et la matrice finale dans [`../validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md`](../validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md).
|
||||||
|
|
||||||
|
Le réaudit final Helius du 23 août 2026 retient `helius_laserstream` comme protocol kind WebSocket : les sept familles standard `account/logs/program/root/signature/slot/slotsUpdates` réutilisent les wrappers publiés, `block/vote` restent absents et `transactionSubscribe`/`transactionUnsubscribe` constitue l'extension provider-specific. `slotsUpdates` conserve son statut unstable, `notifyOn` deprecated/no-op n'est pas exposé et le heartbeat Helius-only envoie un WebSocket Ping control frame toutes les 60 secondes depuis l'actor partagé.
|
||||||
|
|
||||||
|
`pre.010` ferme README/USAGE, la stratégie smoke live architecture-safe et les graphes Cargo finaux ; `pre.011` ferme la candidate documentaire et prépare [`../../prompts/014-V0_2_9_START_PROMPT.md`](../../prompts/014-V0_2_9_START_PROMPT.md). `0.2.8-rel.001` publie ensuite cette surface stable sans nouvelle capacité runtime. Aucun SDK Helius, aucune dépendance gRPC et aucun replay historique WebSocket ne sont introduits.
|
||||||
|
|
||||||
### `0.2.9` — Yellowstone gRPC standard
|
### `0.2.9` — Yellowstone gRPC standard
|
||||||
|
|
||||||
|
|||||||
1660
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
Normal file
1660
docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/RULES_RUST.md -->
|
<!-- file: docs/rules/RULES_RUST.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 10 -->
|
||||||
|
|
||||||
# Règles Rust générales
|
# Règles Rust générales
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ Les règles `RUST-*` s'appliquent à tous les fichiers Rust de KSP : crates, sou
|
|||||||
- **RUST-IMPORT-009** — Dans sa propre crate, un élément `pub` ou `pub(crate)` partagé est appelé via `crate::Item`, y compris depuis son module de déclaration lorsque le contrat est crate-wide.
|
- **RUST-IMPORT-009** — Dans sa propre crate, un élément `pub` ou `pub(crate)` partagé est appelé via `crate::Item`, y compris depuis son module de déclaration lorsque le contrat est crate-wide.
|
||||||
- **RUST-IMPORT-010** — Un chemin `crate::module::Item` est interdit pour un élément partagé `pub`/`pub(crate)` qui peut être consommé via le crate-root. Le chemin du module interne n'est pas une façade.
|
- **RUST-IMPORT-010** — Un chemin `crate::module::Item` est interdit pour un élément partagé `pub`/`pub(crate)` qui peut être consommé via le crate-root. Le chemin du module interne n'est pas une façade.
|
||||||
- **RUST-IMPORT-011** — Un élément strictement privé à un module n'est pas réexporté et est appelé par son nom local dans ce module.
|
- **RUST-IMPORT-011** — Un élément strictement privé à un module n'est pas réexporté et est appelé par son nom local dans ce module.
|
||||||
- **RUST-IMPORT-012** — Dans un sous-module de tests, `super::Item` est réservé à un élément strictement privé du module parent. Un élément `pub` ou `pub(crate)` continue d'être appelé via `crate::Item`.
|
- **RUST-IMPORT-012** — Dans un fichier `unit_tests/...` rattaché au module parent, tout élément strictement privé du parent est appelé explicitement via `super::Item`. Inversement, un élément `pub` ou `pub(crate)` n'est jamais appelé via `super::` ni par un nom nu : il continue d'être appelé via le crate-root `crate::Item`, y compris lorsque le test est rattaché à son module de déclaration.
|
||||||
- **RUST-IMPORT-013** — Les réexports internes commencent par `self::`. Un réexport d'une crate externe peut utiliser directement le chemin externe canonique.
|
- **RUST-IMPORT-013** — Les réexports internes commencent par `self::`. Un réexport d'une crate externe peut utiliser directement le chemin externe canonique.
|
||||||
- **RUST-IMPORT-014** — Un export correspond à une ligne de réexport distincte ; les accolades ne servent jamais à regrouper une façade.
|
- **RUST-IMPORT-014** — Un export correspond à une ligne de réexport distincte ; les accolades ne servent jamais à regrouper une façade.
|
||||||
- **RUST-IMPORT-015** — Une crate externe rendue publique uniquement pour l'hygiène d'une macro exportée conserve son nom canonique, porte `#[doc(hidden)]` et ne devient pas une API de consommation. `ksp-logging-lib::tracing` est ce bridge technique pour les macros Logging ; les autres crates KSP n'y accèdent jamais directement.
|
- **RUST-IMPORT-015** — Une crate externe rendue publique uniquement pour l'hygiène d'une macro exportée conserve son nom canonique, porte `#[doc(hidden)]` et ne devient pas une API de consommation. `ksp-logging-lib::tracing` est ce bridge technique pour les macros Logging ; les autres crates KSP n'y accèdent jamais directement.
|
||||||
@@ -48,6 +48,8 @@ Les règles `RUST-*` s'appliquent à tous les fichiers Rust de KSP : crates, sou
|
|||||||
- **RUST-API-004** — Un élément `pub(crate)` consommé hors de son module est réexporté au crate-root via `pub(crate) use` puis appelé via `crate::Item`.
|
- **RUST-API-004** — Un élément `pub(crate)` consommé hors de son module est réexporté au crate-root via `pub(crate) use` puis appelé via `crate::Item`.
|
||||||
- **RUST-API-005** — Les chemins internes de modules ne constituent jamais une API stable.
|
- **RUST-API-005** — Les chemins internes de modules ne constituent jamais une API stable.
|
||||||
- **RUST-API-006** — Si deux éléments crate-wide auraient le même nom au crate-root, ils sont renommés dans leurs modules propriétaires avec des noms canoniques non ambigus ; un alias de réexport n'est pas utilisé pour masquer la collision.
|
- **RUST-API-006** — Si deux éléments crate-wide auraient le même nom au crate-root, ils sont renommés dans leurs modules propriétaires avec des noms canoniques non ambigus ; un alias de réexport n'est pas utilisé pour masquer la collision.
|
||||||
|
- **RUST-API-007** — La visibilité d'un item n'est jamais élargie uniquement pour permettre son test. Un helper utilisé seulement par son module et ses `unit_tests/` reste privé et les tests y accèdent via `super::Item`; il ne devient `pub(crate)` que lorsqu'un autre module de production le consomme réellement, auquel cas `RUST-API-004` et `RUST-IMPORT-009` s'appliquent.
|
||||||
|
- **RUST-API-008** — Un helper strictement privé dont aucun chemin de production ne dépend encore et qui n'existe que pour un `unit_tests/` de préparation est compilé sous `#[cfg(test)]` jusqu'à sa première consommation de production réelle. Il ne reste pas mort dans le build normal et n'est pas conservé au moyen d'un `#[allow(dead_code)]` compensatoire.
|
||||||
|
|
||||||
## Formatage, blocs et ordre
|
## Formatage, blocs et ordre
|
||||||
|
|
||||||
@@ -102,7 +104,7 @@ Les règles `RUST-*` s'appliquent à tous les fichiers Rust de KSP : crates, sou
|
|||||||
|
|
||||||
- **RUST-AUDIT-001** — `scripts/audit_rust_workspace_rules.py` est le point d'entrée obligatoire de l'audit Rust KSP. Il exécute les audits généraux, la complétude des réexports/chemins et les frontières KSP sans fusionner leurs responsabilités.
|
- **RUST-AUDIT-001** — `scripts/audit_rust_workspace_rules.py` est le point d'entrée obligatoire de l'audit Rust KSP. Il exécute les audits généraux, la complétude des réexports/chemins et les frontières KSP sans fusionner leurs responsabilités.
|
||||||
- **RUST-AUDIT-002** — L'audit est dependency-free côté Python standard et échoue avec un code non nul dès qu'une violation mécanique est détectée.
|
- **RUST-AUDIT-002** — L'audit est dependency-free côté Python standard et échoue avec un code non nul dès qu'une violation mécanique est détectée.
|
||||||
- **RUST-AUDIT-003** — L'audit contrôle au minimum : headers/version/newline, lints de crate-root, visibilité interdite, `use`/aliases/groupes/globs/scope, rustdocs visibles, structure des blocs de réexports, ordre des imports de traits et constantes lorsque KSP le possède, lignes vides dans fonctions/structs/enums, complétude des réexports crate-root, chemins `crate::module::Item`, usage de `super::` dans les tests séparés et frontières KSP directement vérifiables. L'ordre intra-bloc des `use`/réexports reste la responsabilité canonique de `cargo fmt --all` et n'est pas réimplémenté par le script.
|
- **RUST-AUDIT-003** — L'audit contrôle au minimum : headers/version/newline, lints de crate-root, visibilité interdite, `use`/aliases/groupes/globs/scope, rustdocs visibles, structure des blocs de réexports, ordre des imports de traits et constantes lorsque KSP le possède, lignes vides dans fonctions/structs/enums, complétude des réexports crate-root, chemins `crate::module::Item`, accès `super::PrivateItem` et `crate::VisibleItem` dans les tests séparés, ainsi que les frontières KSP directement vérifiables. L'ordre intra-bloc des `use`/réexports reste la responsabilité canonique de `cargo fmt --all` et n'est pas réimplémenté par le script.
|
||||||
- **RUST-AUDIT-004** — Les règles contextuelles qui ne peuvent pas être prouvées sans interpréter la sémantique restent des critères de revue humaine ; le script ne doit pas produire de faux sentiment de complétude.
|
- **RUST-AUDIT-004** — Les règles contextuelles qui ne peuvent pas être prouvées sans interpréter la sémantique restent des critères de revue humaine ; le script ne doit pas produire de faux sentiment de complétude.
|
||||||
|
|
||||||
## Contrôle avant livraison
|
## Contrôle avant livraison
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/000-README.md -->
|
<!-- file: docs/validation/000-README.md -->
|
||||||
<!-- version: 20 -->
|
<!-- version: 23 -->
|
||||||
|
|
||||||
# Validations KSP
|
# Validations KSP
|
||||||
|
|
||||||
@@ -19,3 +19,4 @@ Documents :
|
|||||||
- [`008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) — matrice finale validée de la release stable `0.2.5`, threat model V1, canaris adversariaux, reproduction externe des vecteurs, audit de frontières, normalisation Rust/audit structurel, graphes Cargo et checkpoint final `pre.010-fix.003` vert.
|
- [`008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md`](008-V0_2_5_WALLET_SECURITY_COMPLIANCE.md) — matrice finale validée de la release stable `0.2.5`, threat model V1, canaris adversariaux, reproduction externe des vecteurs, audit de frontières, normalisation Rust/audit structurel, graphes Cargo et checkpoint final `pre.010-fix.003` vert.
|
||||||
- [`009-V0_2_6_WALLET_DESK_COMPLIANCE.md`](009-V0_2_6_WALLET_DESK_COMPLIANCE.md) — matrice finale validée de la release stable `0.2.6`, couvrant Wallet Desk, les wires V1/V2, la migration explicite, le runtime Tauri packagé, les frontières sécurité/ownership et le gate opérateur `pre.018-fix.001` avec build final Linux vert.
|
- [`009-V0_2_6_WALLET_DESK_COMPLIANCE.md`](009-V0_2_6_WALLET_DESK_COMPLIANCE.md) — matrice finale validée de la release stable `0.2.6`, couvrant Wallet Desk, les wires V1/V2, la migration explicite, le runtime Tauri packagé, les frontières sécurité/ownership et le gate opérateur `pre.018-fix.001` avec build final Linux vert.
|
||||||
- [`010-V0_2_7_ONCHAIN_WEBSOCKET.md`](010-V0_2_7_ONCHAIN_WEBSOCKET.md) — matrice finale validée de la release stable `0.2.7`, ouverte par `pre.001`, fermée techniquement par `pre.014` puis publiée par `rel.001` : inventaire 9 subscribe + 9 unsubscribe, lifecycle borné, statuts unstable, compliance 18/18, non-régression HTTP 52+14, composition Config V2, smoke WebSocket Devnet et audit de dépendances.
|
- [`010-V0_2_7_ONCHAIN_WEBSOCKET.md`](010-V0_2_7_ONCHAIN_WEBSOCKET.md) — matrice finale validée de la release stable `0.2.7`, ouverte par `pre.001`, fermée techniquement par `pre.014` puis publiée par `rel.001` : inventaire 9 subscribe + 9 unsubscribe, lifecycle borné, statuts unstable, compliance 18/18, non-régression HTTP 52+14, composition Config V2, smoke WebSocket Devnet et audit de dépendances.
|
||||||
|
- [`011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md`](011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md) — matrice finale validée de la release stable `0.2.8`, ouverte par `pre.001`, consolidée jusqu’à `pre.011` puis publiée par `rel.001` : Helius `account/logs/program/root/signature/slot/slotsUpdates + transaction`, `block/vote` absents, heartbeat provider, sécurité/redaction, lifecycle adversarial, non-régressions HTTP/WS, stratégie smoke live et graphes Cargo finaux.
|
||||||
|
|||||||
1160
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
Normal file
1160
docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: prompts/000-README.md -->
|
<!-- file: prompts/000-README.md -->
|
||||||
<!-- version: 23 -->
|
<!-- version: 25 -->
|
||||||
|
|
||||||
# Prompts KSP
|
# Prompts KSP
|
||||||
|
|
||||||
@@ -33,4 +33,5 @@ Le prompt générique `0.1.x` a été affiné pendant `0.0.3` puis remplacé par
|
|||||||
- [`010-V0_2_5_START_PROMPT.md`](010-V0_2_5_START_PROMPT.md) — prompt préparé par `0.2.4-pre.009` puis finalisé en version 2 par `pre.009-fix.001`, destiné à ouvrir `0.2.5 — Wallet foundation` sur la base stable `v0.2.4`; il impose audit/threat-model/sizing avant choix cryptographiques et cadre `.kspwallet` interopérable, capacités indépendantes VIEW/OWNER, metadata protégées, key slots/rotations, signature, persistence atomique et import/export extensible sans `WalletPolicy`.
|
- [`010-V0_2_5_START_PROMPT.md`](010-V0_2_5_START_PROMPT.md) — prompt préparé par `0.2.4-pre.009` puis finalisé en version 2 par `pre.009-fix.001`, destiné à ouvrir `0.2.5 — Wallet foundation` sur la base stable `v0.2.4`; il impose audit/threat-model/sizing avant choix cryptographiques et cadre `.kspwallet` interopérable, capacités indépendantes VIEW/OWNER, metadata protégées, key slots/rotations, signature, persistence atomique et import/export extensible sans `WalletPolicy`.
|
||||||
- [`011-V0_2_6_START_PROMPT.md`](011-V0_2_6_START_PROMPT.md) — prompt préparé par `0.2.5-pre.010` puis renforcé pendant `pre.010-fix.001`–`fix.003`, prompt historique consommé pour ouvrir `0.2.6 — Wallet Desk` après le tag stable `v0.2.5`; il impose une première tranche audit/sizing, rappelle les règles Rust/audit structurel, cadre Config composite + Wallet + HTTP `getBalance`, lifecycle VIEW/OWNER, sécurité password/export, validation frontend/Tauri et conserve le TODO `0.2.11` d’intégration des prix offchain dans Wallet Desk après validation de la Price Desk spécialisée.
|
- [`011-V0_2_6_START_PROMPT.md`](011-V0_2_6_START_PROMPT.md) — prompt préparé par `0.2.5-pre.010` puis renforcé pendant `pre.010-fix.001`–`fix.003`, prompt historique consommé pour ouvrir `0.2.6 — Wallet Desk` après le tag stable `v0.2.5`; il impose une première tranche audit/sizing, rappelle les règles Rust/audit structurel, cadre Config composite + Wallet + HTTP `getBalance`, lifecycle VIEW/OWNER, sécurité password/export, validation frontend/Tauri et conserve le TODO `0.2.11` d’intégration des prix offchain dans Wallet Desk après validation de la Price Desk spécialisée.
|
||||||
- [`012-V0_2_7_START_PROMPT.md`](012-V0_2_7_START_PROMPT.md) — prompt réaligné par `0.2.6-pre.015` puis renforcé en contrat de reprise autonome par `0.2.6-pre.018-fix.002`; prompt historique consommé pour ouvrir `0.2.7 — WebSocket Solana standard` depuis `v0.2.6`, avec lectures/règles ordonnées, audit officiel et historique, threat-model session/subscription/reconnect/backpressure, matrice de compliance, gate `pre.001` strict et prévision souple de prereleases avant toute implémentation lourde.
|
- [`012-V0_2_7_START_PROMPT.md`](012-V0_2_7_START_PROMPT.md) — prompt réaligné par `0.2.6-pre.015` puis renforcé en contrat de reprise autonome par `0.2.6-pre.018-fix.002`; prompt historique consommé pour ouvrir `0.2.7 — WebSocket Solana standard` depuis `v0.2.6`, avec lectures/règles ordonnées, audit officiel et historique, threat-model session/subscription/reconnect/backpressure, matrice de compliance, gate `pre.001` strict et prévision souple de prereleases avant toute implémentation lourde.
|
||||||
- [`013-V0_2_8_START_PROMPT.md`](013-V0_2_8_START_PROMPT.md) — prompt actif après publication `0.2.7-rel.001` et tag stable `v0.2.7`, préparé par `0.2.7-pre.014` puis renforcé en version 2 par `pre.014-fix.001`; il ouvre `0.2.8 — Helius LaserStream WebSocket` avec relecture des règles et de la surface WebSocket finale, réaudit Helius actuel, gate `pre.001` strict audit/brainstorming/sizing, réutilisation du moteur actor standard sans second client, frontières provider/Config/secrets explicites et forecast souple recalibrable avant toute implémentation lourde.
|
- [`013-V0_2_8_START_PROMPT.md`](013-V0_2_8_START_PROMPT.md) — prompt historique consommé pour ouvrir puis conduire la release stable `0.2.8 — Helius LaserStream WebSocket` depuis `v0.2.7`; préparé par `0.2.7-pre.014` puis renforcé en version 2 par `pre.014-fix.001`, il imposait relecture des règles, réaudit Helius, gate `pre.001` strict audit/brainstorming/sizing, actor standard partagé, frontières provider/Config/secrets et forecast souple avant implémentation lourde.
|
||||||
|
- [`014-V0_2_9_START_PROMPT.md`](014-V0_2_9_START_PROMPT.md) — prompt actif après publication `0.2.8-rel.001` et tag stable `v0.2.8`, préparé par `0.2.8-pre.011`; il ouvre `0.2.9 — Yellowstone gRPC standard/provider-neutral` et impose la base stable autoritaire, la relecture ordonnée des règles/architectures/compliances Transport, un réaudit Yellowstone actuel (service/proto/releases/crates), un audit dépendances/licences/MSRV avant choix du client/proto, la séparation standard/provider/deshred, un gate `pre.001` strict audit/brainstorming/sizing et une prévision souple détaillée avant toute implémentation gRPC lourde.
|
||||||
|
|||||||
1278
prompts/014-V0_2_9_START_PROMPT.md
Normal file
1278
prompts/014-V0_2_9_START_PROMPT.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# file: scripts/audit_rust_export_completeness.py
|
# file: scripts/audit_rust_export_completeness.py
|
||||||
# version: 3
|
# version: 4
|
||||||
|
|
||||||
"""Audit crate-root export completeness and canonical same-crate paths."""
|
"""Audit crate-root export completeness and canonical same-crate paths."""
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ class Declaration:
|
|||||||
module: str
|
module: str
|
||||||
name: str
|
name: str
|
||||||
visibility: str
|
visibility: str
|
||||||
|
kind: str
|
||||||
path: pathlib.Path
|
path: pathlib.Path
|
||||||
line: int
|
line: int
|
||||||
|
|
||||||
@@ -62,17 +63,47 @@ def declaration_candidates(crate: pathlib.Path, path: pathlib.Path) -> list[Decl
|
|||||||
depths = line_depths(mask_rust_source(text))
|
depths = line_depths(mask_rust_source(text))
|
||||||
found: list[Declaration] = []
|
found: list[Declaration] = []
|
||||||
pattern = re.compile(
|
pattern = re.compile(
|
||||||
r"^\s*(pub(?:\(crate\))?)\s+(?:(?:async|unsafe|const)\s+)*(?:const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)"
|
r"^\s*(pub(?:\(crate\))?)\s+(?:(?:async|unsafe|const)\s+)*(const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)"
|
||||||
)
|
)
|
||||||
for idx, line in enumerate(lines, 1):
|
for idx, line in enumerate(lines, 1):
|
||||||
if depths[idx - 1] != 0:
|
if depths[idx - 1] != 0:
|
||||||
continue
|
continue
|
||||||
match = pattern.match(line)
|
match = pattern.match(line)
|
||||||
if match is not None:
|
if match is not None:
|
||||||
found.append(Declaration(module_path(crate, path), match.group(2), match.group(1), path, idx))
|
found.append(Declaration(module_path(crate, path), match.group(3), match.group(1), match.group(2), path, idx))
|
||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def private_declaration_candidates(crate: pathlib.Path, path: pathlib.Path) -> list[Declaration]:
|
||||||
|
"""Return module-level strictly private declarations from one source module."""
|
||||||
|
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
lines = text.splitlines()
|
||||||
|
depths = line_depths(mask_rust_source(text))
|
||||||
|
found: list[Declaration] = []
|
||||||
|
pattern = re.compile(r"^\s*(?:(?:async|unsafe|const)\s+)*(const|static|type|struct|enum|trait|union|fn)\s+([A-Za-z_][A-Za-z0-9_]*)")
|
||||||
|
for idx, line in enumerate(lines, 1):
|
||||||
|
if depths[idx - 1] != 0:
|
||||||
|
continue
|
||||||
|
if line.lstrip().startswith(("pub ", "pub(crate) ")):
|
||||||
|
continue
|
||||||
|
match = pattern.match(line)
|
||||||
|
if match is not None:
|
||||||
|
found.append(Declaration(module_path(crate, path), match.group(2), "private", match.group(1), path, idx))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def unqualified_test_reference_pattern(declaration: Declaration) -> re.Pattern[str]:
|
||||||
|
"""Return a conservative pattern for one unqualified parent-item reference in a separated unit test."""
|
||||||
|
|
||||||
|
name = re.escape(declaration.name)
|
||||||
|
if declaration.kind == "fn":
|
||||||
|
return re.compile(rf"(?<![\w:.]){name}\s*\(")
|
||||||
|
if declaration.kind in {"const", "static", "type", "struct", "enum", "trait", "union"}:
|
||||||
|
return re.compile(rf"(?<![\w:]){name}\b")
|
||||||
|
return re.compile(rf"(?<![\w:]){name}\b")
|
||||||
|
|
||||||
|
|
||||||
def root_exports(crate: pathlib.Path) -> dict[tuple[str, str], str]:
|
def root_exports(crate: pathlib.Path) -> dict[tuple[str, str], str]:
|
||||||
"""Return explicit crate-root re-exports keyed by source module and symbol."""
|
"""Return explicit crate-root re-exports keyed by source module and symbol."""
|
||||||
|
|
||||||
@@ -160,10 +191,12 @@ def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]
|
|||||||
exports = root_exports(crate)
|
exports = root_exports(crate)
|
||||||
tests = unit_test_parents(crate)
|
tests = unit_test_parents(crate)
|
||||||
declarations: dict[tuple[str, str], Declaration] = {}
|
declarations: dict[tuple[str, str], Declaration] = {}
|
||||||
|
private_declarations_by_source: dict[pathlib.Path, dict[str, Declaration]] = {}
|
||||||
candidates: list[Candidate] = []
|
candidates: list[Candidate] = []
|
||||||
for path in sorted((crate / "src").rglob("*.rs")):
|
for path in sorted((crate / "src").rglob("*.rs")):
|
||||||
if path == crate_root:
|
if path == crate_root:
|
||||||
continue
|
continue
|
||||||
|
private_declarations_by_source[path.resolve()] = {item.name: item for item in private_declaration_candidates(crate, path)}
|
||||||
for declaration in declaration_candidates(crate, path):
|
for declaration in declaration_candidates(crate, path):
|
||||||
key = (declaration.module, declaration.name)
|
key = (declaration.module, declaration.name)
|
||||||
declarations[key] = declaration
|
declarations[key] = declaration
|
||||||
@@ -198,19 +231,28 @@ def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]
|
|||||||
if symbol not in root_symbols:
|
if symbol not in root_symbols:
|
||||||
candidates.append(Candidate("RUST-IMPORT-203", relative, idx, f"`crate::{symbol}` does not resolve to a declared/re-exported crate-root symbol"))
|
candidates.append(Candidate("RUST-IMPORT-203", relative, idx, f"`crate::{symbol}` does not resolve to a declared/re-exported crate-root symbol"))
|
||||||
|
|
||||||
# `super::Item` is reserved to strictly private parent items in separated unit tests.
|
# Separated unit tests use `super::Item` only for strictly private parent items; visible items use the crate-root façade.
|
||||||
declarations_by_source: dict[pathlib.Path, dict[str, Declaration]] = {}
|
declarations_by_source: dict[pathlib.Path, dict[str, Declaration]] = {}
|
||||||
for declaration in declarations.values():
|
for declaration in declarations.values():
|
||||||
declarations_by_source.setdefault(declaration.path.resolve(), {})[declaration.name] = declaration
|
declarations_by_source.setdefault(declaration.path.resolve(), {})[declaration.name] = declaration
|
||||||
super_pattern = re.compile(r"\bsuper::([A-Za-z_][A-Za-z0-9_]*)")
|
super_pattern = re.compile(r"\bsuper::([A-Za-z_][A-Za-z0-9_]*)")
|
||||||
for test, parent in tests.items():
|
for test, parent in tests.items():
|
||||||
parent_declarations = declarations_by_source.get(parent, {})
|
parent_declarations = declarations_by_source.get(parent, {})
|
||||||
|
parent_private_declarations = private_declarations_by_source.get(parent, {})
|
||||||
relative = test.relative_to(workspace).as_posix()
|
relative = test.relative_to(workspace).as_posix()
|
||||||
for idx, line in enumerate(test.read_text(encoding="utf-8").splitlines(), 1):
|
text = test.read_text(encoding="utf-8")
|
||||||
|
masked_lines = mask_rust_source(text).splitlines()
|
||||||
|
for idx, line in enumerate(masked_lines, 1):
|
||||||
for match in super_pattern.finditer(line):
|
for match in super_pattern.finditer(line):
|
||||||
declaration = parent_declarations.get(match.group(1))
|
declaration = parent_declarations.get(match.group(1))
|
||||||
if declaration is not None and declaration.visibility in {"pub", "pub(crate)"}:
|
if declaration is not None and declaration.visibility in {"pub", "pub(crate)"}:
|
||||||
candidates.append(Candidate("RUST-IMPORT-202", relative, idx, f"`super::{declaration.name}` targets {declaration.visibility}; use crate-root `crate::{declaration.name}`"))
|
candidates.append(Candidate("RUST-IMPORT-202", relative, idx, f"`super::{declaration.name}` targets {declaration.visibility}; use crate-root `crate::{declaration.name}`"))
|
||||||
|
for declaration in parent_private_declarations.values():
|
||||||
|
if unqualified_test_reference_pattern(declaration).search(line) is not None:
|
||||||
|
candidates.append(Candidate("RUST-IMPORT-204", relative, idx, f"strictly private parent item `{declaration.name}` must be accessed as `super::{declaration.name}` in separated unit tests"))
|
||||||
|
for declaration in parent_declarations.values():
|
||||||
|
if unqualified_test_reference_pattern(declaration).search(line) is not None:
|
||||||
|
candidates.append(Candidate("RUST-IMPORT-205", relative, idx, f"{declaration.visibility} parent item `{declaration.name}` must be accessed through crate-root `crate::{declaration.name}` in separated unit tests"))
|
||||||
return candidates
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user