🔌 Livraison gRPC
Un Decoded Shred Stream peut être consommé en gRPC à la place de l'UDP : un stream sortant, orienté connexion, qui ajoute un filtrage par comptes côté serveur et une livraison ordonnée sur HTTP/2 par-dessus les mêmes transactions avec une latence sub-milliseconde — sans point d'entrée UDP joignable publiquement. Votre dashboard affiche l'endpoint gRPC et le token d'accès de chaque stream.
- Endpoint —
<host>:50051(un seul port pour tous les flux). - Transport — gRPC sur HTTP/2, en clair h2c (pas de TLS). C'est un choix de latence assumé : aucune poignée de main TLS à payer. Isolez le lien au niveau réseau — réseau privé, VPN ou peering — ne l'exposez jamais à l'internet public.
- Routes —
shredstream.com.DecodedShredStreamService/SubscribeDecodedTransactions, ou la route compatible drop-inshreder_binary.ShrederBinaryService/SubscribeBinaryTransactions. Les deux sont strictement équivalentes (mêmes messages protobuf, contenu octet pour octet identique) — un client venant d'un endpoint Shreder / Raiden Pulse n'a qu'à changer d'adresse.
🔑 Authentification
Chaque appel doit porter votre token dans les métadonnées gRPC, sous l'une de ces deux formes (les deux sont acceptées) :
authorization: Bearer <TOKEN> x-token: <TOKEN>
Un token absent ou invalide est rejeté par un statut uniforme UNAUTHENTICATED avec le message authentication refused. Vérifiez votre token.
Rappel : une connexion par token. Ouvrir un second stream avec le même token évince le plus ancien.
🧠Modèle d'abonnement
La route est bidirectionnelle (stream requête → stream réponse) :
- Ouvrez le stream et envoyez au moins une requête contenant une map de filtres nommés :
{ "<nom>": <filtre>, … }. - Le serveur ne livre que les transactions qui matchent au moins un filtre. Chaque réponse est taguée du/des nom(s) de filtre(s) satisfait(s) (champ
filters) — vous routez ainsi plusieurs stratégies sur un seul stream. - Vous pouvez renvoyer une nouvelle map à tout moment : elle remplace la précédente à chaud, sans reconnexion et sans trou de flux.
Cas limites :
- Map vide (
{}) ⇒ rien n'est livré (on s'abonne à des filtres, pas au firehose). - Filtre nommé vide (
{ "all": {} }) ⇒ toutes les transactions passent, taguées"all". - Une fois vos filtres envoyés, vous n'avez plus rien à émettre : le stream continue de livrer avec ces filtres jusqu'à ce que vous en envoyiez de nouveaux ou fermiez la connexion.
🎯 Filtres par comptes
Chaque filtre nommé est composé de trois listes de clés publiques base58, combinées en ET logique :
| Champ | Sémantique |
|---|---|
account_include | la transaction doit toucher au moins un de ces comptes (liste vide = pas de contrainte) |
account_exclude | la transaction ne doit toucher aucun de ces comptes |
account_required | la transaction doit toucher tous ces comptes |
Pour les transactions, « toucher » porte sur les clés de compte statiques de la transaction (signataires inclus). Les adresses résolues via les Address Lookup Tables (ALT) ne sont pas présentes dans l'enveloppe de la transaction et ne sont donc pas filtrables — filtrez uniquement sur les clés statiques.
📦 Le payload
Chaque réponse porte la transaction sous forme d'octets bruts, avec son slot :
message SubscribeUpdateBinaryTransaction {BinaryTransaction transaction = 1;uint64 slot = 2; // slot Solana}message BinaryTransaction {repeated bytes signatures = 1; // signatures (64 octets chacune)bytes binary_transaction = 3; // VersionedTransaction, bincode, VERBATIM}
binary_transaction est le wire Solana standard — une VersionedTransaction sérialisée en bincode, non retouchée. Désérialisez-la avec n'importe quel SDK Solana et passez-la à votre parser existant, exactement comme en mode UDP. Les transactions de vote sont exclues par défaut.
đź’» Client officiel
Les clients officiels decoded-shredstream prennent cette route en charge pour vous — connexion, token dans les métadonnées, map de filtres, reconnexion avec renvoi des filtres courants, et la transaction avec son slot et ses signatures. Un seul et même package pour l'UDP et le gRPC.
// npm install decoded-shredstreamimport { DecodedShredStream, FilterAll } from "decoded-shredstream";const client = await DecodedShredStream.grpc({endpoint: "<host>:50051",token: process.env.DECODED_SHREDSTREAM_TOKEN!,filters: { all: FilterAll }, // or: { "watched-wallet": { include: [wallet] } }});for await (const tx of client.transactions()) {console.log(`slot=${tx.slot} sig=${tx.signature.toBase58()} matched=${JSON.stringify(tx.filters)}`);}
# pip install decoded-shredstreamfrom decoded_shredstream import Client, Filter, GrpcConfigwith Client.grpc(GrpcConfig(endpoint="<host>:50051", token="<TOKEN>",filters={"all": Filter()})) as client:for update in client:print(update.slot, len(update.data), list(update.filters))
// cargo add decoded-shredstream tokio --features tokio/macros,tokio/rt-multi-threaduse decoded_shredstream::{Filter, GrpcClient, GrpcConfig};let mut client = GrpcClient::connect(GrpcConfig::new("<host>:50051", "<TOKEN>").filter("all", Filter::all()),).await?;while let Some(update) = client.next_update().await {let update = update?;println!("slot={} sig={} matched={:?}", update.slot(), update.signature(), update.filters());}
// go get github.com/shredstream/decoded-shredstream-goclient, err := decodedshredstream.NewGRPC(decodedshredstream.GRPCConfig{Endpoint: "<host>:50051",Token: "<TOKEN>",Filters: decodedshredstream.Filters{"all": decodedshredstream.FilterAll()},})if err != nil { log.Fatal(err) }defer client.Close()err = client.Run(ctx, func(u *decodedshredstream.TransactionUpdate) {sig, _ := u.Signature()fmt.Println(u.Slot, sig, u.Filters)})
Les filtres se remplacent à chaud sur un stream ouvert (updateFilters / update_filters), sans reconnexion. Les interruptions récupérables ne remontent jamais jusqu'à vous — le client se reconnecte et renvoie la map de filtres courante ; seuls un token refusé, une session fermée par le serveur ou une map de filtres rejetée mettent fin au stream, via une erreur que vous traitez une seule fois.
💻 Clients gRPC standard (stubs générés)
Pour les équipes qui préfèrent leur propre stack gRPC : le contrat est decoded.proto (notre route, shredstream.com.DecodedShredStreamService), qui importe shreder_binary.proto pour ses messages — les deux fichiers sont livrés avec chaque client officiel. Générer les stubs à partir du seul shreder_binary.proto vous donne la route compatible drop-in utilisée ci-dessous.
Rust (tonic)
use std::collections::HashMap;use shreder_binary::shreder_binary_service_client::ShrederBinaryServiceClient;use shreder_binary::{SubscribeBinaryTransactionsRequest, SubscribeRequestFilterBinaryTransactions};use solana_transaction::versioned::VersionedTransaction;use tonic::Request;let mut client = ShrederBinaryServiceClient::connect("http://<host>:50051").await?;let filters = HashMap::from([("pumpfun".to_string(),SubscribeRequestFilterBinaryTransactions {account_include: vec!["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P".into()],account_exclude: vec![],account_required: vec![],},)]);// Filtres statiques : on émet une requête puis on half-close (l'itérateur se termine).let outbound = tokio_stream::iter(vec![SubscribeBinaryTransactionsRequest { transactions: filters }]);let mut req = Request::new(outbound);req.metadata_mut().insert("authorization", "Bearer <TOKEN>".parse()?);let mut stream = client.subscribe_binary_transactions(req).await?.into_inner();while let Some(resp) = stream.message().await? {if let Some(tx) = resp.transaction.and_then(|u| u.transaction) {let vtx: VersionedTransaction = bincode::deserialize(&tx.binary_transaction)?;// → routez selon resp.filters, traitez vtx …}}
Notre route équivalente est
DecodedShredStreamServiceClient::subscribe_decoded_transactions, avec exactement les mêmes types de requête/réponse. Pour mettre à jour les filtres en cours de route, remplaceztokio_stream::iterpar un canal que vous gardez ouvert et sur lequel voussendune nouvelle requête — sans reconnexion.
TypeScript (@grpc/grpc-js)
import { Metadata } from "@grpc/grpc-js";// … client généré depuis shreder_binary.proto …const meta = new Metadata();meta.set("x-token", "<TOKEN>");const call = client.subscribeBinaryTransactions(meta);call.write({transactions: { pumpfun: { accountInclude: ["6EF8rrec…"], accountExclude: [], accountRequired: [] } },});// call.end(); // half-close si les filtres ne changent pascall.on("data", (resp) => {const raw = resp.transaction?.transaction?.binaryTransaction; // Buffer// désérialisez une VersionedTransaction côté client …});call.on("error", (e) => { /* DATA_LOSS / UNAUTHENTICATED → reconnexion / log */ });
Python (grpcio)
import grpcimport shreder_binary_pb2 as pb, shreder_binary_pb2_grpc as rpcchan = grpc.insecure_channel("<host>:50051")stub = rpc.ShrederBinaryServiceStub(chan)def requests():yield pb.SubscribeBinaryTransactionsRequest(transactions={"pumpfun": pb.SubscribeRequestFilterBinaryTransactions(account_include=["6EF8rrec…"])})md = (("x-token", "<TOKEN>"),)for resp in stub.SubscribeBinaryTransactions(requests(), metadata=md):raw = resp.transaction.transaction.binary_transaction # octets bincode# désérialisez une VersionedTransaction …
Test rapide avec grpcurl
grpcurl -plaintext -proto shreder_binary.proto \-H 'x-token: <TOKEN>' \-d '{"transactions":{"all":{}}}' \<host>:50051 shreder_binary.ShrederBinaryService/SubscribeBinaryTransactions
đź§Ż Gestion des erreurs & reconnexion
| Statut gRPC | Signification | Que faire |
|---|---|---|
UNAUTHENTICATED | token absent / invalide / mauvais flux | corriger le token ou le flux — ne pas retenter à l'aveugle |
INVALID_ARGUMENT | base58 invalide, trop de pubkeys, trop de filtres nommés | corriger le filtre fautif |
DATA_LOSS | votre consommateur est trop lent — la file de sortie a débordé, le stream est fermé | se reconnecter et renvoyer votre map de filtres |
PERMISSION_DENIED | déconnexion/révocation par l'opérateur | ne pas boucler ; contacter l'opérateur |
Le service est temps réel uniquement — pas de rejeu ni de backfill. Sur DATA_LOSS ou coupure transport, reconnectez-vous avec un backoff exponentiel (et un peu de jitter), puis renvoyez votre map de filtres ; un court trou de données pendant la reconnexion est normal. À cause de la règle une-connexion-par-token, assurez-vous qu'une seule boucle de reconnexion tourne par token.
Pour éviter DATA_LOSS en amont, consommez le stream sans bloquer : déportez tout traitement lourd (y compris la désérialisation bincode) vers une file ou un pool de workers pour que votre boucle de réception ne cale jamais. gRPC sur HTTP/2 garantit l'ordre et l'intégrité de tout ce qui est émis — la seule perte possible est le drop « client trop lent », toujours signalé explicitement par DATA_LOSS.
⚖️ gRPC ou UDP ?
| gRPC | UDP | |
|---|---|---|
| Latence | Basse — h2c, pas de poignée de main TLS | La plus basse — datagrammes binaires, pas de connexion |
| Livraison | Ordonnée, fiable ; « client trop lent » signalé par DATA_LOSS | Push best-effort ; perte possible, détectée via les trous de seq |
| Filtrage | Filtres par comptes côté serveur (clés statiques) | Aucun — filtrez côté client après réception |
| Joignabilité | Connexion sortante — fonctionne derrière NAT/pare-feu | Nécessite un point d'entrée UDP joignable publiquement |
| Payload | VersionedTransaction (bincode) | VersionedTransaction (bincode), dans une enveloppe de 16 octets |
Choisissez gRPC pour le filtrage côté serveur, la livraison ordonnée et la compatibilité NAT. Choisissez UDP pour la latence absolument la plus basse si vous disposez d'un point d'entrée joignable et surveillez vous-même les pertes. Le payload est identique dans les deux cas.
➡️ Étapes suivantes
- Recevoir les transactions décodées — l'enveloppe UDP, les offsets, la détection de pertes et un décodeur minimal.
- Decoded Shred Stream — positionnement, latence et modes de livraison.