33 KiB
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 :
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 :
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
Un consumer peut créer explicitement une session physique WebSocket :
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 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.
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
Trois familles stables peuvent être créées directement sur la session :
let account = match "11111111111111111111111111111111".parse::<ksp_core_lib::Pubkey>() {
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<T> : 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
La session expose également les familles stables suivantes :
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("<base58-signature>", 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<SolanaSlotNotification> dont les getters exposent slot, parent et root. root_subscribe().await retourne un WsSubscription<u64>. Ces deux méthodes n'acceptent aucune configuration ni aucun paramètre RPC.
Familles unstable : block, slotsUpdates et vote
Les trois familles unstable standard sont également typées. Leur utilisation déclenche un warning KSP centralisé :
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<i64> 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.
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 :
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é
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.
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
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.
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
Fermer explicitement la session est la voie normale de shutdown :
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. Yellowstone gRPC standard
Construction programmatique et unary
Transport peut ouvrir directement un endpoint Yellowstone sans Config :
let grpc_url = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointUrl::parse(
"https://solana-yellowstone-grpc.publicnode.com:443",
) {
Ok(value) => value,
Err(error) => return Err(error),
};
let x_token_metadata = match ksp_onchain_transport_lib::YellowstoneGrpcMetadataEntry::secret(
"x-token",
x_token,
) {
Ok(value) => value,
Err(error) => return Err(error),
};
let grpc_endpoint = match ksp_onchain_transport_lib::YellowstoneGrpcEndpointSettings::new(
"publicnode_mainnet_yellowstone",
true,
ksp_onchain_transport_lib::YellowstoneGrpcProviderName::new("publicnode"),
ksp_onchain_transport_lib::YellowstoneGrpcClusterName::new("mainnet"),
grpc_url,
ksp_onchain_transport_lib::YellowstoneGrpcSessionSettings::default(),
)
.with_metadata(vec![x_token_metadata])
{
Ok(value) => value,
Err(error) => return Err(error),
};
let grpc_channel = match ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(&grpc_endpoint).await {
Ok(value) => value,
Err(error) => return Err(error),
};
let grpc = grpc_channel.standard_unary_client();
let version = grpc.get_version().await;
let slot = grpc
.get_slot(Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed))
.await;
L’URL reste sensible : Debug, erreurs KSP et snapshots n’en exposent pas la valeur. Les metadata publiques/secrètes se construisent avec YellowstoneGrpcMetadataEntry; Transport ne lit jamais l’environnement. PublicNode requiert actuellement une metadata secrète x-token pour les endpoints Yellowstone validés ; ici x_token représente une valeur déjà obtenue par un caller sécurisé. En usage normal, Config construit cette metadata depuis les variables KSP_SECRET_PUBLICNODE_MAINNET_GRPC_X_TOKEN ou KSP_SECRET_PUBLICNODE_TESTNET_GRPC_X_TOKEN. Les appels unary montrés ci-dessus illustrent la surface KSP standard ; le smoke PublicNode de release ne prétend pas valider leur entitlement provider et gate uniquement Subscribe slots.
Config Transport V3
Avec ksp-config-lib, un profil V3 peut exposer les trois transports sans casser l’accesseur historique HTTP + WS :
let resolved = match engine.load_resolved_transport_config(Some("publicnode_mainnet"), &environment) {
Ok(value) => value,
Err(error) => return Err(error),
};
let grpc_settings = match resolved.grpc_settings() {
Some(value) => value,
None => return Err(ksp_core_lib::Error::new(
ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS,
"selected profile has no Yellowstone gRPC endpoint",
)),
};
let endpoint = match grpc_settings.endpoints().iter().find(|candidate| candidate.enabled()) {
Some(value) => value,
None => return Err(ksp_core_lib::Error::new(
ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS,
"selected profile has no enabled Yellowstone gRPC endpoint",
)),
};
let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(endpoint).await;
protocol = solana_yellowstone est validé par Config et reste distinct du descripteur provider. Une valeur provider n’autorise pas Transport à introduire une API provider-specific sans divergence réelle.
Profil OrbitFlare Devnet
Le profil committé orbitflare_devnet réutilise exactement le même accès Config -> Transport :
let resolved = match engine.load_resolved_transport_config(Some("orbitflare_devnet"), &environment) {
Ok(value) => value,
Err(error) => return Err(error),
};
let grpc_settings = match resolved.grpc_settings() {
Some(value) => value,
None => return Err(ksp_core_lib::Error::new(
ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS,
"selected OrbitFlare profile has no Yellowstone gRPC endpoint",
)),
};
let endpoint = match grpc_settings.endpoints().iter().find(|candidate| candidate.enabled()) {
Some(value) => value,
None => return Err(ksp_core_lib::Error::new(
ksp_onchain_transport_lib::ERROR_CODE_INVALID_SETTINGS,
"selected OrbitFlare profile has no enabled Yellowstone gRPC endpoint",
)),
};
let channel = ksp_onchain_transport_lib::YellowstoneGrpcChannel::connect(endpoint).await;
Config résout KSP_SECRET_ORBITFLARE_DEVNET_GRPC_X_TOKEN vers la metadata secrète x-token. Sa valeur effective est la License Key ORBIT-* du produit Solana ; X-ORBIT-KEY et le Bearer du Customer API ne doivent pas être utilisés pour Yellowstone. Transport ne lit jamais cette variable lui-même.
L’endpoint validé par 0.2.10 est http://devnet.rpc.orbitflare.com:10000. Il reste volontairement en http : KSP ne remplace pas le transport provider par https sans endpoint TLS explicitement fourni.
Subscribe bidirectionnel
Une session standard part d’une requête typed complète :
let mut request = ksp_onchain_transport_lib::YellowstoneSubscribeRequest::new();
let name = match ksp_onchain_transport_lib::YellowstoneSubscribeFilterName::new("slots") {
Ok(value) => value,
Err(error) => return Err(error),
};
if let Err(error) = request.insert_slot_filter(
name,
ksp_onchain_transport_lib::YellowstoneSubscribeSlotFilter::new(),
) {
return Err(error);
}
request.set_commitment(Some(ksp_onchain_transport_lib::SolanaCommitment::Confirmed));
let mut stream = match grpc_channel.open_standard_subscribe(request).await {
Ok(value) => value,
Err(error) => return Err(error),
};
let update = stream.next_update().await;
let snapshot = stream.snapshot();
let closed = stream.close().await;
try_update() remplace dynamiquement la requête complète tant que la session est Active. Une mutation pendant Reconnecting est refusée pour éviter une application ambiguë. Le snapshot expose reconnects, replay attempts, gaps, duplicates, dernier from_slot demandé et dernier slot observé, sans endpoint ni payload arbitraire.
Le reconnect réutilise la dernière requête acceptée et peut avancer from_slot, mais le consumer doit traiter cette reprise comme best-effort. KSP ne promet ni exactly-once, ni replay historique complet, ni absence de fork/equivocation entre nœuds.
5. Appels typés
Les wrappers typés se trouvent directement sur HttpTransportPool.
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 wrappers foundation, Accounts, Tokens et Cluster sont disponibles directement sur le pool. Exemples représentatifs :
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 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 :
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 :
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.
6. Exécution JSON-RPC standard générique
Une méthode courante auditée peut être appelée via son descriptor :
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.
7. Sélection et admission sans exécuter la requête
Pour inspecter le routing :
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.
8. Snapshots runtime
HttpTransportPool::snapshot() fournit une vue sûre des endpoints/rôles : disponibilité, limites, requêtes en vol, cooldown restant et compteurs runtime.
Les URLs d'endpoint n'y apparaissent jamais.
9. 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.
10. Logging
Les événements Transport utilisent le target :
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.
11. Smokes réseau opt-in
Le smoke Transport HTTP pur construit ses settings programmatiquement et exerce un sous-ensemble représentatif d'Accounts/Tokens/Cluster, trois reads Transactions, puis des reads Blocks/Economics :
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 :
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 Transport Yellowstone gRPC PublicNode reste indépendant de Config mais nécessite un personal token opérateur. Il teste Mainnet et Testnet en ouvrant Subscribe, en demandant les updates slots, en attendant un YellowstoneSubscribeUpdate::Slot non nul puis en fermant la session de manière bornée.
Pour éviter de placer les secrets dans les arguments ou l'URL, le harness lit deux lignes sur stdin : Mainnet puis Testnet. Elles peuvent contenir la même valeur ; l'opérateur a validé un même personal token sur les deux réseaux.
read -rsp 'PublicNode Mainnet Yellowstone x-token: ' PUBLICNODE_MAINNET_TOKEN
echo
read -rsp 'PublicNode Testnet Yellowstone x-token: ' PUBLICNODE_TESTNET_TOKEN
echo
printf '%s\n%s\n' "$PUBLICNODE_MAINNET_TOKEN" "$PUBLICNODE_TESTNET_TOKEN" \
| cargo test -p ksp-onchain-transport-lib \
--test yellowstone_publicnode_smoke \
-- --ignored --nocapture
unset PUBLICNODE_MAINNET_TOKEN PUBLICNODE_TESTNET_TOKEN
Endpoints validés :
Mainnet https://solana-yellowstone-grpc.publicnode.com:443
Testnet https://solana-testnet-yellowstone-grpc.publicnode.com:443
Les profils Config conservent deux variables secrètes distinctes afin d'autoriser des credentials différents si nécessaire ; cette séparation ne signifie pas que PublicNode impose actuellement un token différent par réseau. Un timeout KSP de half-close après réception du slot est accepté par le smoke comme fermeture bornée du provider ; aucune absence de slot ni autre erreur n'est masquée.
Le smoke Transport Yellowstone gRPC OrbitFlare est lui aussi indépendant de Config. Il lit une seule License Key sur stdin, la classe comme metadata secrète x-token, ouvre le standard Subscribe, demande slots à commitment confirmed, attend un Slot non nul et un SubscribeUpdate::Ping, puis ferme la session de manière bornée :
read -rsp 'OrbitFlare License Key: ' ORBITFLARE_LICENSE_KEY
echo
printf '%s\n' "$ORBITFLARE_LICENSE_KEY" \
| cargo test -p ksp-onchain-transport-lib \
--test yellowstone_orbitflare_smoke \
-- --ignored --nocapture
unset ORBITFLARE_LICENSE_KEY
Endpoint validé :
Devnet http://devnet.rpc.orbitflare.com:10000
Le gate 0.2.10-pre.003 a passé ce scénario en live avec Slot + Ping. Le Ping reçu est le message Yellowstone standard auquel N1 sait déjà répondre sans remplacer la dernière requête complète mémorisée ; aucun heartbeat OrbitFlare supplémentaire n’est donc requis.
Le smoke de composition Config -> Transport reste également disponible :
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.
Smoke Helius live
Aucun nouveau test Helius live n’est committé en 0.2.8-pre.010. La raison est architecturale : Transport ne peut pas lire KSP_SECRET_HELIUS_API_KEY ni dépendre de Config, et Config ne doit pas devenir la destination générale des futurs smokes Config + autre crate. Créer un smoke Helius supplémentaire dans l’une de ces deux crates contournerait donc une frontière déjà documentée.
Lorsque la surface KSP d’intégration/orchestration dédiée existera, le smoke live minimal recommandé sera :
Config helius_devnet
-> endpoint helius_laserstream résolu avec KSP_SECRET_HELIUS_API_KEY
-> HeliusLaserStreamWsSession::connect
-> slotSubscribe
-> une slotNotification sous timeout
-> slotUnsubscribe
-> close
Ce scénario utilise une méthode standard stable sur l’endpoint Helius et teste donc auth + façade provider + actor + unsubscribe sans dépendre d’une entitlement particulière de transactionSubscribe. Un smoke transactionSubscribe pourra être ajouté séparément comme opt-in provider-specific si l’environnement opérateur possède les droits nécessaires ; il ne doit pas devenir un gate réseau obligatoire de la release.
Les endpoints publics/provider sont des dépendances externes. Un rate-limit, refus d’auth, entitlement absente ou incident réseau n’est pas assimilé automatiquement à une régression locale ; les fixtures HTTP/WebSocket/gRPC locales et les gates déterministes restent autoritaires.
Pour auditer les dépendances, inspecter également le graphe effectif après résolution Cargo :
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.