# Utilisation de `ksp-onchain-transport-lib` Ce guide présente les surfaces publiques destinées aux consumers. Les notes de release restent dans `CHANGELOG.md` et les deltas. ## 1. Construction directe du runtime Transport peut être utilisé sans Config. Le consumer construit les settings publics puis le pool : ```rust let url = match ksp_onchain_transport_lib::HttpEndpointUrl::parse("https://api.devnet.solana.com") { Ok(value) => value, Err(error) => return Err(error), }; let role = ksp_onchain_transport_lib::HttpEndpointRoleSettings::new( ksp_onchain_transport_lib::HttpRoleName::new("default"), true, vec![ksp_onchain_transport_lib::HttpRequestKind::wildcard()], 100, ksp_onchain_transport_lib::HttpRoleLimits::new(None, None, None, None), ); let endpoint = ksp_onchain_transport_lib::HttpEndpointSettings::new( "solana_devnet_public", true, ksp_onchain_transport_lib::HttpProviderName::new("solana-public"), ksp_onchain_transport_lib::HttpClusterName::new("devnet"), url, std::time::Duration::from_secs(5), std::time::Duration::from_secs(15), Some(8), vec![role], ); let settings = ksp_onchain_transport_lib::HttpTransportSettings::new( vec![endpoint], ksp_onchain_transport_lib::HttpRetrySettings::new( 2, std::time::Duration::from_millis(100), std::time::Duration::from_secs(2), ), ); let pool = match ksp_onchain_transport_lib::HttpTransportPool::new(settings) { Ok(value) => value, Err(error) => return Err(error), }; ``` `HttpTransportSettings::validate()` peut être appelé explicitement avant la construction du pool lorsque le consumer veut séparer validation et initialisation. ## 2. Construction via `ksp-config-lib` Lorsque le consumer utilise Config, la direction reste Config -> Transport : ```rust let resolved = match engine.load_resolved_transport_config(Some("devnet_public"), &environment) { Ok(value) => value, Err(error) => return Err(error), }; let pool = match ksp_onchain_transport_lib::HttpTransportPool::new(resolved.into_settings()) { Ok(value) => value, Err(error) => return Err(error), }; ``` Le document standard peut contenir une URL provenant d'un `KSP_SECRET_*`. La valeur réelle est transmise au runtime, mais les projections sûres et `Debug` restent redacted. ## 3. Session physique WebSocket À partir de `0.2.7-pre.004`, un consumer peut créer explicitement une session physique : ```rust let ws_url = match ksp_onchain_transport_lib::WsEndpointUrl::parse("wss://api.devnet.solana.com") { Ok(value) => value, Err(error) => return Err(error), }; let endpoint = ksp_onchain_transport_lib::WsEndpointSettings::new( "devnet_public", true, ksp_onchain_transport_lib::WsProviderName::new("solana-public"), ksp_onchain_transport_lib::WsClusterName::new("devnet"), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard, ws_url, ksp_onchain_transport_lib::WsSessionSettings::default(), ); let session = match ksp_onchain_transport_lib::WsSession::connect(endpoint).await { Ok(value) => value, Err(error) => return Err(error), }; 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. `WsSubscription` 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. Le snapshot de session expose les subscriptions actuellement enregistrées via `WsSubscriptionSnapshot`, avec `remote_bound: bool` seulement. Le remote ID réel n'est jamais projeté. ### Premiers wrappers standards publics Depuis `0.2.7-pre.009`, trois familles stables peuvent être créées directement sur la session : ```rust let account = match "11111111111111111111111111111111".parse::() { Ok(value) => value, Err(error) => return Err(error.into()), }; let account_config = ksp_onchain_transport_lib::SolanaAccountSubscribeConfig::new( Some(ksp_onchain_transport_lib::SolanaAccountEncoding::Base64), None, Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed), ); let mut account_subscription = match session.account_subscribe(&account, Some(&account_config)).await { Ok(value) => value, Err(error) => return Err(error), }; let next = account_subscription.recv().await; let removed = account_subscription.unsubscribe().await; ``` La même session expose `program_subscribe()` avec `SolanaProgramSubscribeConfig` et `logs_subscribe()` avec `SolanaLogsSubscribeFilter` plus `SolanaCommitmentConfig`. Pour `logsSubscribe`, `Mentions(pubkey)` représente exactement une adresse, conformément à la contrainte upstream retenue par l'audit. `accountSubscribe` ne propose pas `minContextSlot`: le champ existe dans un config partagé upstream mais est ignoré par le handler PubSub audité. `programSubscribe` conserve en revanche `withContext`; `SolanaProgramNotification` permet au consumer de traiter explicitement une notification contextée ou non contextée. Les trois wrappers retournent le même handle `WsSubscription` : reconnect, resubscribe, overflow, cause terminale et unsubscribe restent donc uniformes. Aucun wrapper public n'accepte un nom de méthode JSON-RPC arbitraire ni un remote subscription ID. ### Lot stable B : signature, slot et root Depuis `0.2.7-pre.010`, la session expose également : ```rust let signature_config = ksp_onchain_transport_lib::SolanaSignatureSubscribeConfig::new( Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized), Some(true), ); let mut signature_subscription = match session.signature_subscribe("", Some(&signature_config)).await { Ok(value) => value, Err(error) => return Err(error), }; while let Some(notification) = signature_subscription.recv().await { let notification = match notification { Ok(value) => value, Err(error) => return Err(error), }; if notification.value().is_terminal() { break; } } ``` Avec `enableReceivedNotification = true`, `ReceivedSignature` peut arriver avant la variante terminale `Processed { err }`. La variante terminale ferme automatiquement le handle KSP, sans `signatureUnsubscribe` supplémentaire et sans resubscribe lors d'une reconnexion ultérieure. Une cancellation explicite avant cette notification terminale reste possible via `unsubscribe().await`. `slot_subscribe().await` retourne un `WsSubscription` dont les getters exposent `slot`, `parent` et `root`. `root_subscribe().await` retourne un `WsSubscription`. Ces deux méthodes n'acceptent aucune configuration ni aucun paramètre RPC. ### 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é : ```rust let block_config = ksp_onchain_transport_lib::SolanaBlockSubscribeConfig::new( Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed), Some(ksp_onchain_transport_lib::SolanaTransactionEncoding::Base64), Some(ksp_onchain_transport_lib::SolanaTransactionDetails::Signatures), Some(0), Some(false), ); let mut blocks = match session .block_subscribe(&ksp_onchain_transport_lib::SolanaBlockSubscribeFilter::All, Some(&block_config)) .await { Ok(value) => value, Err(error) => return Err(error), }; ``` `blockSubscribe` requiert un validator qui active la capability upstream correspondante. Une erreur applicative RPC liée à cette capability est renvoyée au caller sans reconnect de la session. `processed` est refusé localement ; `confirmed` et `finalized` sont admis. `maxSupportedTransactionVersion` n'est pas limité artificiellement à `0`. `slots_updates_subscribe().await` délivre `SolanaSlotUpdate`. Les sept variantes courantes sont structurées ; une variante inconnue reste consommable via `Unknown` et `unknown_raw()`, sous la borne de taille WebSocket déjà appliquée avant décodage. `vote_subscribe().await` délivre `SolanaVoteNotification`. `timestamp()` retourne `Option` pour conserver omission/null/value. Ce flux reste gossip et pre-consensus : le consumer ne doit pas l'assimiler à une confirmation ledger. Les trois familles utilisent le même `WsSubscription::unsubscribe().await`; aucun remote subscription ID n'entre dans l'API publique. ### 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`. `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. `unsubscribe().await` peut être appelé pendant `Reconnecting` ou `Resubscribing`. La cancellation locale gagne et le handle ne redevient jamais `Active`. Un ACK distant tardif est nettoyé best-effort par l'actor. Aucun backfill HTTP n'est déclenché automatiquement ; le consumer doit traiter `continuity_gap_count` comme un signal de réconciliation éventuelle. ### Backpressure par subscription Depuis `0.2.7-pre.008`, `WsSessionSettings::notification_queue_capacity()` borne réellement la queue de chaque `WsSubscription`. 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. `max_active_subscriptions` borne séparément le nombre d'entrées logiques enregistrées. Son rejet utilise le même domaine d'erreur de capacité mais n'incrémente pas `overflow_count`, réservé aux queues de notifications saturées. Une subscription fermée ou nettoyée après abandon de son receiver libère sa capacité locale ; l'actor tente aussi de supprimer son binding distant sans rendre ce cleanup bloquant. Le consumer doit traiter `overflow_count` et `continuity_gap_count` comme deux signaux distincts : le premier indique une perte locale par saturation d'un consumer, le second une interruption de continuité liée à une reconnexion. Aucun des deux n'implique un replay ou un backfill automatique. ### Fermeture explicite À partir de `0.2.7-pre.005`, fermer explicitement la session est la voie normale de shutdown : ```rust let session = ksp_onchain_transport_lib::WsSession::connect(endpoint).await?; // ... wrappers standard puis recv()/unsubscribe() ... session.close().await?; ``` `close()` agit sur toute la session physique, y compris les clones du handle. Il annule les requests en attente, publie `Closing`, tente le Close WebSocket dans le budget configuré, puis publie `Closed`. Une session `Closed` refuse les nouvelles requests internes. Les limites de taille et de capacité sont des policies KSP configurables par `WsSessionSettings`; elles ne doivent pas être interprétées comme des limites protocolaires Solana officielles. Le snapshot expose seulement l'identité locale, les metadata logiques de l'endpoint, l'état, les compteurs sûrs et les projections locales de subscriptions. L'URL et les remote subscription IDs ne sont jamais projetés. La disparition de tous les handles de session déclenche le cleanup actor best-effort ; `close().await` reste la voie normale de shutdown. ## 4. Appels typés Les wrappers typés se trouvent directement sur `HttpTransportPool`. ```rust let role = ksp_onchain_transport_lib::HttpRoleName::new("default"); let health = pool.get_health(&role).await; let genesis_hash = pool.get_genesis_hash(&role).await; let version = pool.get_version(&role).await; let balance = pool .get_balance( &role, &ksp_core_lib::PRGIDPK_SOLANA_SYSTEM, Some(&ksp_onchain_transport_lib::GetBalanceConfig::new( Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed), None, )), ) .await; ``` Les quatre canaris `0.2.1` restent disponibles. `0.2.2` ajoute les wrappers typés Accounts, Tokens et Cluster. Exemples représentatifs : ```rust let account = pool .get_account_info(&role, &ksp_core_lib::PRGIDPK_SOLANA_SYSTEM, None) .await; let epoch = pool.get_epoch_info(&role, None).await; let vote_accounts = pool.get_vote_accounts(&role, None).await; ``` La release stable `0.2.4` contient les **52 wrappers typés courants** : 4 foundation + 22 Accounts/Tokens/Cluster + 11 Transactions + 10 Blocks + 5 Economics. Les DTOs Transport conservent les `null`, options, overloads et formes wire sans décodage Program/SPL métier. Exemples Transaction représentatifs : ```rust let context = ksp_onchain_transport_lib::SolanaContextConfig::new( std::option::Option::Some(ksp_onchain_transport_lib::SolanaCommitment::Finalized), std::option::Option::None, ); let latest = pool .get_latest_blockhash(&role, std::option::Option::Some(&context)) .await; let transaction_count = pool .get_transaction_count(&role, std::option::Option::Some(&context)) .await; ``` `getTransaction` expose la config moderne complète et une forme bare-encoding legacy séparée et deprecated. `requestAirdrop` et `sendTransaction` sont des write submissions : elles utilisent la protection centrale `NeverAfterDispatch`. `simulateTransaction` reste une simulation retry-safe et conserve son résultat riche sans introduire de décodage Program. Exemples Blocks/Economics représentatifs : ```rust let block_height = pool.get_block_height(&role, Some(&context)).await; let inflation_rate = pool.get_inflation_rate(&role).await; let stake_minimum = pool.get_stake_minimum_delegation(&role, Some(&context)).await; ``` `getBlock` possède également une forme bare-encoding legacy séparée et deprecated. Les valeurs Economics restent celles du runtime : le consumer ne doit pas supposer localement un taux d'inflation ou un minimum de délégation constant. ## 5. Exécution JSON-RPC standard générique Une méthode courante auditée peut être appelée via son descriptor : ```rust if let Some(descriptor) = ksp_onchain_transport_lib::find_http_rpc_method("getSlot") { let _result = pool.execute_standard_rpc(&role, descriptor, vec![]).await; } ``` Cette API retourne un `serde_json::Value`. Elle reste utile pour les extensions provider, les diagnostics et les méthodes hors registre standard, mais elle ne remplace jamais le wrapper typé d'une méthode HTTP standard désormais couverte par KSP. Avant exécution, `ensure_runtime_supported()` est appliqué. Une méthode historique `Removed` retourne `ERROR_CODE_METHOD_REMOVED` au lieu d'émettre un appel réseau fictif. ## 6. Sélection et admission sans exécuter la requête Pour inspecter le routing : ```rust if let Some(descriptor) = ksp_onchain_transport_lib::find_http_rpc_method("getBalance") { let _selection = pool.select_for_method(&role, descriptor); let _permit = pool.acquire_for_method(&role, descriptor).await; } ``` Dans le même bloc, `acquire_for_method()` réserve réellement la capacité RPS/concurrence sous deadline. `HttpRequestPermit` détient la capacité de concurrence jusqu'à sa destruction. Aucun verrou synchrone n'est conservé pendant l'attente réseau. ## 7. Snapshots runtime `HttpTransportPool::snapshot()` fournit une vue sûre des endpoints/rôles : disponibilité, limites, requêtes en vol, cooldown restant et compteurs runtime. Les URLs d'endpoint n'y apparaissent jamais. ## 8. Retry et write submissions La policy de retry est portée par la metadata des méthodes et `evaluate_transport_retry()`. Les reads/simulations classés `RetrySafe` peuvent être réessayés dans le budget configuré lorsqu'une cause transport est explicitement retryable. Pour une opération `WriteSubmission / NeverAfterDispatch`, un timeout ou autre résultat ambigu après dispatch arrête la resoumission automatique. Le consumer métier ne doit pas contourner cette protection avec une boucle de retry externe aveugle. ## 9. Logging Les événements Transport utilisent le target : ```text ksp-onchain-transport-lib ``` Ne jamais journaliser l'URL complète, un token provider, un body massif, une transaction complète ou une réponse complète. La configuration standard route les événements `info` de Transport vers un fichier dédié. Pour une investigation temporaire, élever uniquement ce target/sink à `debug` ou `trace`, puis revenir à `info` avant clôture du développement. ## 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` : ```bash cargo test -p ksp-onchain-transport-lib --test transport_devnet_smoke -- --ignored --nocapture ``` Il appelle `getAccountInfo`, `getTokenAccountsByOwner`, `getEpochInfo`, `getVoteAccounts`, puis `getLatestBlockhash`, `isBlockhashValid`, `getTransactionCount`, `getBlockHeight`, `getInflationRate` et `getStakeMinimumDelegation`. La branche Token suit la forme Devnet documentée : owner Pubkey ordinaire de l'exemple officiel, selector `programId` avec l'ID canonique du programme SPL Token, puis config explicite `commitment: finalized` + `encoding: jsonParsed`. Une réponse vide reste acceptable. La branche Transaction reste read-only : elle ne déclenche ni airdrop ni soumission de transaction et ne remplace pas les fixtures déterministes couvrant les 11 wrappers. Le smoke **Transport WebSocket pur** utilise l'endpoint public Devnet standard avec des settings programmatiques, ouvre une session physique, crée une subscription stable `slotSubscribe`, attend une notification bornée, vérifie une valeur de slot non nulle, annule la subscription avec son handle puis ferme explicitement la session : ```bash cargo test -p ksp-onchain-transport-lib --test websocket_devnet_smoke -- --ignored --nocapture ``` 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 : ```bash cargo test -p ksp-config-lib --test transport_devnet_smoke -- --ignored --nocapture ``` Il valide le profil committé `devnet_public` et les quatre canaris foundation. Il reste transitoirement hébergé dans Config : les futurs smokes cross-crates ne doivent pas faire de Config leur destination générale et devront migrer vers une surface d'intégration/orchestration dédiée lorsqu'elle existera. Les endpoints publics Solana sont rate-limités et non destinés à la production. Un échec réseau externe n'est pas assimilé automatiquement à une régression locale ; les fixtures HTTP 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 : ```bash cargo tree -p ksp-onchain-transport-lib cargo tree -p ksp-onchain-transport-lib --duplicates cargo tree --duplicates ``` Le premier graphe doit conserver la frontière `Transport -> Core + Logging + crates techniques`; il ne doit introduire aucune dépendance Config, Wallet, Store, Program ou `tracing` directe. Les sorties `--duplicates` sont un diagnostic de résolution transitive : une duplication n'est pas supprimée aveuglément si elle est imposée par des dépendances upstream incompatibles.