🔌 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-in shreder_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) :

  1. Ouvrez le stream et envoyez au moins une requête contenant une map de filtres nommés : { "<nom>": <filtre>, … }.
  2. 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.
  3. 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 :

ChampSémantique
account_includela transaction doit toucher au moins un de ces comptes (liste vide = pas de contrainte)
account_excludela transaction ne doit toucher aucun de ces comptes
account_requiredla 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 :

proto
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.

ts
// npm install decoded-shredstream
import { 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)}`);
}
python
# pip install decoded-shredstream
from decoded_shredstream import Client, Filter, GrpcConfig
with 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))
rust
// cargo add decoded-shredstream tokio --features tokio/macros,tokio/rt-multi-thread
use 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());
}
Gogo
// go get github.com/shredstream/decoded-shredstream-go
client, 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)

rust
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, remplacez tokio_stream::iter par un canal que vous gardez ouvert et sur lequel vous send une nouvelle requête — sans reconnexion.

TypeScript (@grpc/grpc-js)

ts
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 pas
call.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)

python
import grpc
import shreder_binary_pb2 as pb, shreder_binary_pb2_grpc as rpc
chan = 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

bash
grpcurl -plaintext -proto shreder_binary.proto \
-H 'x-token: <TOKEN>' \
-d '{"transactions":{"all":{}}}' \
<host>:50051 shreder_binary.ShrederBinaryService/SubscribeBinaryTransactions

đź§Ż Gestion des erreurs & reconnexion

Statut gRPCSignificationQue faire
UNAUTHENTICATEDtoken absent / invalide / mauvais fluxcorriger le token ou le flux — ne pas retenter à l'aveugle
INVALID_ARGUMENTbase58 invalide, trop de pubkeys, trop de filtres nomméscorriger le filtre fautif
DATA_LOSSvotre consommateur est trop lent — la file de sortie a débordé, le stream est fermése reconnecter et renvoyer votre map de filtres
PERMISSION_DENIEDdéconnexion/révocation par l'opérateurne 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 ?

gRPCUDP
LatenceBasse — h2c, pas de poignée de main TLSLa plus basse — datagrammes binaires, pas de connexion
LivraisonOrdonnée, fiable ; « client trop lent » signalé par DATA_LOSSPush best-effort ; perte possible, détectée via les trous de seq
FiltrageFiltres 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-feuNécessite un point d'entrée UDP joignable publiquement
PayloadVersionedTransaction (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

Livraison gRPC — Docs | ShredStream.com