v0.2.7-pre.014

This commit is contained in:
2026-08-23 11:15:57 +02:00
parent 3c5786f273
commit 5aa7b45840
23 changed files with 1605 additions and 103 deletions

View File

@@ -1,9 +1,9 @@
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
<!-- version: 18 -->
<!-- version: 19 -->
# `ksp-onchain-transport-lib`
`ksp-onchain-transport-lib` est la bibliothèque KSP propriétaire du transport on-chain Solana. Sa première surface est le transport HTTP JSON-RPC ; les extensions WebSocket et gRPC sont introduites séparément lorsque leur release les cible.
`ksp-onchain-transport-lib` est la bibliothèque KSP propriétaire du transport on-chain Solana. Elle fournit le transport HTTP JSON-RPC complet et le moteur WebSocket Solana standard ; les extensions provider-specific et gRPC sont ajoutées séparément lorsquune release les cible.
## Responsabilités
@@ -19,7 +19,8 @@ La crate possède :
- les enveloppes JSON-RPC 2.0 et leur validation ;
- le registre audité des méthodes Solana HTTP ;
- l'exécution générique des méthodes standard supportées ;
- les wrappers typés explicitement livrés par KSP ;
- les wrappers typés HTTP et WebSocket explicitement livrés par KSP ;
- les sessions physiques WebSocket, subscriptions logiques, reconnect/resubscribe et backpressure bornés ;
- les snapshots runtime sûrs ;
- l'observabilité Transport via `ksp-logging-lib`.
@@ -70,23 +71,23 @@ Le registre porte notamment :
- remplacement historique éventuel ;
- release de couverture typée KSP.
La release stable `0.2.4` complète la surface typée des **52 méthodes courantes** :
La surface HTTP typée couvre les **52 méthodes courantes** :
```text
0.2.1 foundation : 4
0.2.2 Accounts/Tokens/Cluster : 22
0.2.3 Transactions : 11
0.2.4 Blocks/Economics : 15
total : 52
foundation : 4
Accounts/Tokens/Cluster : 22
Transactions : 11
Blocks/Economics : 15
total : 52
```
`0.2.4` ajoute les dix wrappers Blocks et les cinq wrappers Economics. Les points sensibles restent notamment `getBlock` moderne + bare encoding legacy deprecated, les quatre `transactionDetails`, les versions transaction numériques génériques, `numRewardPartitions`, `commissionBps`, les overloads de `getBlocks`, les ranges de production, les valeurs d'inflation/minimum de délégation fournies par le runtime et les `null` positionnels de `getInflationReward`.
Les dix wrappers Blocks et les cinq wrappers Economics couvrent notamment `getBlock` moderne + bare encoding legacy deprecated, les quatre `transactionDetails`, les versions transaction numériques génériques, `numRewardPartitions`, `commissionBps`, les overloads de `getBlocks`, les ranges de production, les valeurs d'inflation/minimum de délégation fournies par le runtime et les `null` positionnels de `getInflationReward`.
`KSP-TRANSPORT-007` impose qu'un wrapper typé couvre toutes les possibilités RPC supportées retenues par l'audit : paramètres/options, overloads et formes legacy encore supportées, contraintes déterministes utiles et variantes de réponse pertinentes sans perte. Le réaudit final `0.2.4` agrège le réaudit 37/37 de `0.2.3` avec les 15 nouveaux wrappers et porte la preuve stable à **52/52**.
`KSP-TRANSPORT-007` impose qu'un wrapper typé couvre toutes les possibilités RPC supportées retenues par l'audit : paramètres/options, overloads et formes legacy encore supportées, contraintes déterministes utiles et variantes de réponse pertinentes sans perte. Les canaris de release portent la preuve globale à **52/52** méthodes courantes typées.
Les 14 méthodes historiques restent découvrables pour la compliance mais sont `Removed` et ne sont pas simulées comme appelables.
## Foundation WebSocket `0.2.7-pre.004`
## Moteur WebSocket standard
La première session physique WebSocket est matérialisée sans introduire de pool/scheduler automatique ni de registry de subscriptions anticipé.
@@ -99,25 +100,25 @@ La première session physique WebSocket est matérialisée sans introduire de po
- publie `WsSessionSnapshot` via un état compact `watch` ;
- applique aux sockets les plafonds KSP de message, frame et write buffer ;
- ne projette jamais l'URL dans `Debug`, snapshot, erreurs KSP ou logs ;
- répond aux `Ping` reçus et tolère les `Pong`; le lifecycle complet `Close`/shutdown a été durci ensuite en `pre.005`.
- répond aux `Ping` reçus et tolère les `Pong`; le lifecycle `Close`/shutdown est borné et explicite.
Le chemin JSON-RPC générique reste `pub(crate)`. Il sert de primitive au moteur typed de subscriptions et **ne constitue pas une API publique raw provider-extension**. Le registry subscriptions et le mapping remote/local sont matérialisés en `pre.006`, reconnect/resubscribe est actif depuis `pre.007` et le backpressure borné par subscription est effectif depuis `pre.008`.
Le chemin JSON-RPC générique reste `pub(crate)`. Il sert de primitive au moteur typed de subscriptions et **ne constitue pas une API publique raw provider-extension**. Le registry de subscriptions et le mapping remote/local, le reconnect/resubscribe et le backpressure borné par subscription font partie du moteur standard.
Les tests déterministes utilisent un serveur WebSocket local et prouvent le handshake, le round-trip JSON-RPC, le dispatch de réponses hors ordre, l'isolation des erreurs RPC applicatives, deux sessions physiques distinctes sur la même URL et la redaction des erreurs de connexion.
### Durcissement `0.2.7-pre.005`
### Limites, control frames et shutdown
La session physique dispose désormais de `WsSession::close().await`. Le signal de shutdown est distinct de la command queue, passe l'état en `Closing`, annule les requests JSON-RPC en attente, envoie un Close WebSocket best-effort sous `close_timeout`, puis publie `Closed`. Un peer qui ne répond pas au Close ne peut donc pas bloquer indéfiniment le shutdown.
Les limites `max_message_size`, `max_frame_size`, `max_write_buffer_size` et `max_pending_requests` sont couvertes par des fixtures adversariales locales. Les requests outbound qui dépassent les bornes message/frame sont rejetées avant écriture ; les frames/messages inbound surdimensionnés sont rejetés par Tungstenite avant parse JSON. Les timeouts pending libèrent leur capacité sans faire tomber une session encore saine.
Ping/Pong/Close sont traités comme control frames : le Pong automatique Tungstenite est flushé et aucun heartbeat applicatif périodique n'est ajouté. Depuis `pre.007`, un Close distant inattendu, EOF, erreur I/O/TLS/WebSocket ou violation protocolaire structurelle entre dans le reconnect borné ; `Closed` reste réservé au shutdown local explicite ou à la disparition des handles.
Ping/Pong/Close sont traités comme control frames : le Pong automatique Tungstenite est flushé et aucun heartbeat applicatif périodique n'est ajouté. Un Close distant inattendu, EOF, erreur I/O/TLS/WebSocket ou violation protocolaire structurelle entre dans le reconnect borné ; `Closed` reste réservé au shutdown local explicite ou à la disparition des handles.
### Registry de subscriptions `0.2.7-pre.006`
### Registry de subscriptions
Le même actor possède maintenant le registre des subscriptions logiques, sans exposer les IDs numériques distants. Chaque subscription reçoit un `WsSubscriptionId` local stable, et le mapping `remote_subscription_id -> WsSubscriptionId` reste strictement runtime/interne.
La création générique typed reste `pub(crate)` et n'est jamais exposée comme API raw provider-extension. À partir de `pre.009`, les wrappers standard publics l'utilisent derrière leurs DTOs et paramètres typés. Le handle public `WsSubscription<T>` expose uniquement :
La création générique typed reste `pub(crate)` et n'est jamais exposée comme API raw provider-extension. Les wrappers standard publics lutilisent derrière leurs DTOs et paramètres typés. Le handle public `WsSubscription<T>` expose uniquement :
- `id()` et `kind()` ;
- `state()` ;
@@ -126,7 +127,7 @@ La création générique typed reste `pub(crate)` et n'est jamais exposée comme
L'ACK de subscribe est traité atomiquement dans l'actor : le remote ID est lié au local ID avant que la notification suivante puisse être dispatchée. Les notifications inconnues/stale sont ignorées avec un diagnostic sûr. Un mismatch de méthode de notification ou un échec de décodage typed termine uniquement la subscription concernée ; la session physique reste `Active`.
### Reconnect et resubscribe `0.2.7-pre.007`
### Reconnect et resubscribe
Une perte de connexion physique invalide immédiatement les remote subscription IDs et incrémente `continuity_gap_count`. Le runtime utilise `WsReconnectSettings` pour appliquer un nombre fini de tentatives avec backoff exponentiel borné et sans jitter. Le shutdown surveille les phases de backoff et de handshake et interrompt la reprise sans reconnecter uniquement pour nettoyer des subscriptions.
@@ -136,7 +137,7 @@ Avec `WsResubscribePolicy::Never`, la session physique peut se reconnecter mais
Le compteur de continuity gaps est un signal d'observabilité, pas une garantie de livraison. Transport n'ajoute aucun backfill HTTP et ne promet aucune continuité lossless pendant l'intervalle de déconnexion.
### Backpressure et libération de capacité `0.2.7-pre.008`
### Backpressure et libération de capacité
Chaque subscription dispose de sa propre queue typed bornée par `notification_queue_capacity`. Le runtime ne droppe jamais silencieusement une notification lorsque cette queue est pleine : il incrémente `WsSessionSnapshot::overflow_count()`, fait passer uniquement le handle lent à `Failed`, publie `ERROR_CODE_WS_BACKPRESSURE_OVERFLOW` via `WsSubscription::terminal_error_code()` et programme un `*Unsubscribe` distant best-effort. Les autres subscriptions et la session physique restent utilisables.
@@ -146,7 +147,7 @@ Les autres terminaisons en échec publient également un code KSP sûr sur le ha
Les fixtures adversariales prouvent l'isolation d'un consumer lent, la survie d'une subscription saine, le cleanup distant best-effort, la réutilisation de capacité après unsubscribe ou abandon du receiver et la conservation du compteur d'overflow à travers les snapshots. Aucune promesse de livraison lossless n'est ajoutée.
### Wrappers stables lot A `0.2.7-pre.009`
### Wrappers stables : account, program et logs
Les trois premiers wrappers WebSocket standards sont publics sur `WsSession` :
@@ -162,9 +163,9 @@ logs_subscribe -> WsSubscription<SolanaRpcResponse<SolanaLogsNotification>>
`SolanaLogsSubscribeFilter` rend les trois filtres upstream explicites : `All`, `AllWithVotes` et `Mentions(Pubkey)`. La variante `Mentions` encode par construction exactement une adresse. `SolanaLogsNotification` conserve la signature opaque, le `err` nullable et l'ordre des messages `logs`, enveloppés dans `SolanaRpcResponse`.
L'unsubscribe de ces trois familles passe toujours par `WsSubscription::unsubscribe()`: le caller ne voit ni ne fournit l'ID serveur. Les paramètres initiaux restent conservés par l'actor pour le resubscribe déterministe acquis en `pre.007`, et toutes les règles de backpressure/terminal error acquises en `pre.008` s'appliquent sans branche spéciale aux nouveaux DTOs publics.
L'unsubscribe de ces trois familles passe toujours par `WsSubscription::unsubscribe()`: le caller ne voit ni ne fournit l'ID serveur. Les paramètres initiaux restent conservés par lactor pour le resubscribe déterministe, et toutes les règles de backpressure/terminal error sappliquent sans branche spéciale aux DTOs publics.
### Wrappers stables lot B `0.2.7-pre.010`
### Wrappers stables : signature, slot et root
Le second lot stable complète les subscriptions standard non instables :
@@ -178,9 +179,9 @@ root_subscribe -> WsSubscription<u64>
Une cancellation effectuée avant cette terminaison continue d'utiliser `WsSubscription::unsubscribe()` et émet `signatureUnsubscribe` avec le remote ID détenu uniquement par l'actor. Après la notification terminale, `unsubscribe()` devient local-only et retourne `false`, puisque la subscription est déjà fermée côté serveur et côté KSP.
`slot_subscribe()` et `root_subscribe()` n'acceptent aucun paramètre. `SolanaSlotNotification` conserve exactement `slot`, `parent` et `root`; `root_subscribe()` délivre directement le root `u64`. Ces deux subscriptions restent continues et utilisent donc le reconnect/resubscribe standard de `pre.007`.
`slot_subscribe()` et `root_subscribe()` n'acceptent aucun paramètre. `SolanaSlotNotification` conserve exactement `slot`, `parent` et `root`; `root_subscribe()` délivre directement le root `u64`. Ces deux subscriptions restent continues et utilisent donc le reconnect/resubscribe standard.
### Wrappers unstable `0.2.7-pre.011`
### Wrappers unstable : block, slotsUpdates et vote
Les trois familles standard restantes complètent désormais l'inventaire **9/9 subscribe + 9/9 unsubscribe via handles** :
@@ -264,9 +265,9 @@ Les trois tests sont `ignored` par défaut. Les deux smokes Transport appartienn
- [`USAGE.md`](USAGE.md) — consommation directe, Config -> Transport, API typed/raw, smokes et inspection runtime ;
- [`../../docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md`](../../docs/plans/008-V0_2_1_ONCHAIN_HTTP_PLAN.md) — foundation HTTP stable ;
- [`../../docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md`](../../docs/plans/009-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER_PLAN.md) — extension typed Accounts/Tokens/Cluster ;
- [`../../docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md`](../../docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md) — matrice finale validée `0.2.2` ;
- [`../../docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](../../docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) — plan historique clôturé de `0.2.3` ;
- [`../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md) — matrice finale validée `0.2.3` ;
- [`../../docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md`](../../docs/validation/004-V0_2_2_HTTP_ACCOUNTS_TOKENS_CLUSTER.md) — matrice finale validée Accounts/Tokens/Cluster ;
- [`../../docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md`](../../docs/plans/010-V0_2_3_HTTP_TRANSACTIONS_PLAN.md) — plan historique clôturé Transactions ;
- [`../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md`](../../docs/validation/006-V0_2_3_HTTP_TRANSACTIONS.md) — matrice finale validée Transactions ;
- [`../../docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md`](../../docs/plans/011-V0_2_4_HTTP_BLOCKS_ECONOMICS_PLAN.md) — plan Blocks/Economics et compliance HTTP finale ;
- [`../../docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md`](../../docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md) — matrice finale validée `52/52 + 14/14` et audit `KSP-TRANSPORT-007` global ;
- [`../../config/std.transport.json`](../../config/std.transport.json) — configuration standard HTTP + WebSocket V2, avec lecture backward V1 HTTP-only.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-onchain-transport-lib/USAGE.md -->
<!-- version: 18 -->
<!-- version: 19 -->
# Utilisation de `ksp-onchain-transport-lib`
@@ -67,7 +67,7 @@ Le document standard peut contenir une URL provenant d'un `KSP_SECRET_*`. La val
## 3. Session physique WebSocket
À partir de `0.2.7-pre.004`, un consumer peut créer explicitement une session physique :
Un consumer peut créer explicitement une session physique WebSocket :
```rust
let ws_url = match ksp_onchain_transport_lib::WsEndpointUrl::parse("wss://api.devnet.solana.com") {
@@ -92,7 +92,7 @@ let snapshot = session.snapshot();
Deux appels `WsSession::connect` avec le même endpoint créent volontairement deux connexions physiques distinctes. Il n'existe encore aucun pool de sessions automatique.
Le socket brut et la primitive JSON-RPC générique ne sont pas publics. Le moteur générique de subscription typed existe depuis `pre.006` mais sa création reste `pub(crate)` même après l'ouverture des premiers wrappers standards en `pre.009`; il ne constitue donc pas une escape hatch provider-specific.
Le socket brut et la primitive JSON-RPC générique ne sont pas publics. Le moteur générique de subscription typed reste `pub(crate)` ; il ne constitue donc pas une escape hatch provider-specific.
`WsSubscription<T>` est le handle public commun retourné par les wrappers standards. Il porte un `WsSubscriptionId` local stable, jamais le remote ID numérique du serveur. Les notifications arrivent via un receiver typed borné et `unsubscribe().await` exécute le `*Unsubscribe` correspondant en préservant son résultat booléen.
@@ -100,7 +100,7 @@ Le snapshot de session expose les subscriptions actuellement enregistrées via `
### Premiers wrappers standards publics
Depuis `0.2.7-pre.009`, trois familles stables peuvent être créées directement sur la session :
Trois familles stables peuvent être créées directement sur la session :
```rust
let account = match "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>() {
@@ -128,7 +128,7 @@ Les trois wrappers retournent le même handle `WsSubscription<T>` : reconnect, r
### Lot stable B : signature, slot et root
Depuis `0.2.7-pre.010`, la session expose également :
La session expose également les familles stables suivantes :
```rust
let signature_config = ksp_onchain_transport_lib::SolanaSignatureSubscribeConfig::new(
@@ -156,7 +156,7 @@ Avec `enableReceivedNotification = true`, `ReceivedSignature` peut arriver avant
### Familles unstable : block, slotsUpdates et vote
Depuis `0.2.7-pre.011`, les trois familles unstable standard sont également typées. Leur utilisation déclenche un warning KSP centralisé :
Les trois familles unstable standard sont également typées. Leur utilisation déclenche un warning KSP centralisé :
```rust
let block_config = ksp_onchain_transport_lib::SolanaBlockSubscribeConfig::new(
@@ -185,7 +185,7 @@ Les trois familles utilisent le même `WsSubscription::unsubscribe().await`; auc
### Reconnect automatique borné
Depuis `0.2.7-pre.007`, les settings de session contrôlent réellement 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`.
`WsResubscribePolicy::Never` reconnecte uniquement la session physique : les subscriptions existantes deviennent terminales et doivent être recréées explicitement par le consumer. Dans les deux modes, les requests applicatives qui étaient en vol lors de la coupure échouent et ne sont pas rejouées implicitement.
@@ -193,7 +193,7 @@ Depuis `0.2.7-pre.007`, les settings de session contrôlent réellement le recon
### Backpressure par subscription
Depuis `0.2.7-pre.008`, `WsSessionSettings::notification_queue_capacity()` borne réellement la queue de chaque `WsSubscription<T>`. Le consumer doit donc drainer `recv()` selon son débit métier. Une queue pleine ne bloque pas l'actor et n'affecte pas les autres subscriptions : la subscription lente devient terminale avec `state() == Failed` et `terminal_error_code() == Some(ERROR_CODE_WS_BACKPRESSURE_OVERFLOW)`, tandis que `WsSessionSnapshot::overflow_count()` est incrémenté.
`WsSessionSettings::notification_queue_capacity()` borne la queue de chaque `WsSubscription<T>`. Le consumer doit donc drainer `recv()` selon son débit métier. Une queue pleine ne bloque pas l'actor et n'affecte pas les autres subscriptions : la subscription lente devient terminale avec `state() == Failed` et `terminal_error_code() == Some(ERROR_CODE_WS_BACKPRESSURE_OVERFLOW)`, tandis que `WsSessionSnapshot::overflow_count()` est incrémenté.
Un échec terminal non lié à l'overflow expose lui aussi un `ErrorCode` KSP sûr via `terminal_error_code()`. Une fermeture normale conserve `None`. Cette projection ne contient ni payload de notification, ni remote subscription ID, ni URL d'endpoint.
@@ -203,7 +203,7 @@ Le consumer doit traiter `overflow_count` et `continuity_gap_count` comme deux s
### Fermeture explicite
À partir de `0.2.7-pre.005`, fermer explicitement la session est la voie normale de shutdown :
Fermer explicitement la session est la voie normale de shutdown :
```rust
let session = ksp_onchain_transport_lib::WsSession::connect(endpoint).await?;
@@ -238,7 +238,7 @@ let balance = pool
.await;
```
Les quatre canaris `0.2.1` restent disponibles. `0.2.2` ajoute les wrappers typés Accounts, Tokens et Cluster. Exemples représentatifs :
Les wrappers foundation, Accounts, Tokens et Cluster sont disponibles directement sur le pool. Exemples représentatifs :
```rust
let account = pool
@@ -248,7 +248,7 @@ let epoch = pool.get_epoch_info(&role, None).await;
let vote_accounts = pool.get_vote_accounts(&role, None).await;
```
La release stable `0.2.4` contient les **52 wrappers typés courants** : 4 foundation + 22 Accounts/Tokens/Cluster + 11 Transactions + 10 Blocks + 5 Economics. Les DTOs Transport conservent les `null`, options, overloads et formes wire sans décodage Program/SPL métier.
La surface HTTP contient **52 wrappers typés courants** : 4 foundation + 22 Accounts/Tokens/Cluster + 11 Transactions + 10 Blocks + 5 Economics. Les DTOs Transport conservent les `null`, options, overloads et formes wire sans décodage Program/SPL métier.
Exemples Transaction représentatifs :
@@ -334,7 +334,7 @@ La configuration standard route les événements `info` de Transport vers un fic
## 10. Smokes Devnet opt-in
Le smoke **Transport HTTP pur** construit ses settings programmatiquement et exerce un sous-ensemble représentatif d'Accounts/Tokens/Cluster, trois reads Transactions, puis des reads Blocks/Economics de la release stable `0.2.4` :
Le smoke **Transport HTTP pur** construit ses settings programmatiquement et exerce un sous-ensemble représentatif d'Accounts/Tokens/Cluster, trois reads Transactions, puis des reads Blocks/Economics :
```bash
cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture
@@ -350,7 +350,7 @@ cargo test -p ksp-onchain-transport-lib --test websocket_devnet_smoke -- --ignor
Il n'utilise ni `blockSubscribe`, ni `slotsUpdatesSubscribe`, ni `voteSubscribe` : ces familles restent unstable et leur disponibilité dépend des capabilities du validator. Le smoke live n'est donc pas un gate de disponibilité de ces extensions.
Le smoke historique de **composition Config -> Transport** reste également disponible :
Le smoke de **composition Config -> Transport** reste également disponible :
```bash
cargo test -p ksp-config-lib --test transport_devnet_smoke -- --ignored --nocapture
@@ -360,7 +360,7 @@ Il valide le profil committé `devnet_public` et les quatre canaris foundation.
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.
Pour l'audit final de dépendances de `0.2.7`, 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 :
```bash
cargo tree -p ksp-onchain-transport-lib