Zum Hauptinhalt springen
Wenn du Transaktionsdaten von Laserstream empfängst, solltest du auf zwei wichtige Dinge achten:
  • Message → Was der Benutzer tun wollte (sein signierter Vorschlag)
  • Meta → Was tatsächlich passiert ist (das Ausführungsergebnis)
Die Herausforderung: Transaktionsrohdaten liegen als binäre Byte-Arrays wie <Buffer 00 bf a0 e8...> vor und nicht als lesbare Adressen und Signaturen. Dieser Leitfaden zeigt dir, wie du: Die Binärdaten in ein menschenlesbares Format decodierst, relevante Informationen extrahierst und den gesamten Ablauf einer Transaktion vom Vorschlag bis zur Ausführung verstehst.

Ein Live-stream ohne Decodierung

Führe den folgenden minimalen Client aus. Die Filter-Flags verwerfen Abstimmungs- und fehlgeschlagene Transaktionen. Das Array accountInclude beschränkt die Ergebnisse auf Aktivitäten, die die Jupiter-Programm-ID betreffen.
Deine Konsole zeigt jetzt einen Wrapper: filters, createdAt und einen transaction-Zweig, der zwei untergeordnete Elemente enthält:
  • transaction.transaction.transaction → die signierte Message
  • transaction.transaction.meta → die Meta der Ausführung
Alles, was wie Uint8Array aussieht, bleibt vorerst undurchsichtig. Wenn du das Skript mit der Decodierungsfunktion ausführst, siehst du die tatsächliche verschachtelte Struktur mit lesbaren Adressen:

Binärdaten decodieren

Warum decodieren? Laserstream-Rohdaten enthalten Signaturen, Kontoschlüssel und Hashes als binäre Uint8Array-Objekte, die nicht lesbar sind. Du musst sie in base58-Strings umwandeln, um die Transaktion nachvollziehen zu können. Die Lösung: Laserstream verwendet Yellowstone gRPC mit integrierten Hilfsfunktionen zur Decodierung. Statt für jeden Feldtyp einen eigenen Decoder zu schreiben, verwenden wir eine rekursive Funktion, die alle Binärdaten in ein menschenlesbares Format umwandelt.
Dieser Ansatz nutzt die integrierte Decodierung und verarbeitet zugleich die Binärfelder, die du manuell umwandeln musst. Die Transaktionsstruktur ist bereits geparst. Du musst nur noch die Binärfelder in ein menschenlesbares Format umwandeln.

Die Transaktionsstruktur verstehen

Da wir die decodierten Daten jetzt sehen können, sehen wir uns die beiden Hauptbestandteile jedes Laserstream-Transaktionsupdates an. Wie im ersten Beispiel gezeigt, enthält jede Transaktion zwei zentrale Objekte:
  • Message (Vorschlag) → transaction.transaction.transaction → die signierte Message (Vorschlag des Benutzers)
  • Meta (Ausführung) → transaction.transaction.meta → die Metadaten der Ausführung (response des Validators)
Diese zweiteilige Struktur erzählt die gesamte Geschichte: was der Benutzer angefordert hat und was tatsächlich passiert ist. Sehen wir uns beide Teile im Detail an.

Der Vorschlag: alles innerhalb von message

Der Benutzer erstellt eine Message, die festlegt, was, wer und bis wann. So decodierst du die einzelnen Teile:

Transaktions-Header

numRequiredSignatures teilt dem Validator mit, wie viele Signaturen er verifizieren muss. Die beiden numReadonly*-Werte kennzeichnen Konten, die die Laufzeitumgebung als schreibgeschützt behandeln kann. Das ermöglicht eine parallele Ausführung.

Wörterbuch der Kontoschlüssel

accountKeys ist eine einfache Liste öffentlicher Schlüssel, die als Nachschlagetabelle dient. Jede spätere Ganzzahl in der Transaktion – programIdIndex sowie jedes Element im accounts-Array einer Anweisung – verweist per Index auf diese Liste. Dadurch spart jede Message mehr als ein Kilobyte.

Schutz vor Wiederholung

recentBlockhash läuft ab, sobald er nicht mehr zu den letzten 150 Block-Hashes gehört. Das dauert im Mainnet ungefähr 90 Sekunden.

Anweisungen: die eigentlichen Befehle

Jede Anweisung enthält drei zentrale Teile:
  • Programm-ID (programIdIndex): Verweist auf eine Adresse im accountKeys-Array (z. B. Index 10 = ComputeBudget111111111111111111111111111111)
  • Konten (accounts): Ein base58-codierter String, der angibt, auf welche Kontoindizes diese Anweisung zugreift
  • Daten (data): Die eigentlichen, als base58 codierten Anweisungsdaten
Aufgrund der Funktion convertBuffers erscheinen Konten als base58, enthalten tatsächlich aber Kontoindizes. Beispielsweise wird "3vtmrQMafzDoG2CBz1iqgXPTnC" zu den Indizes [21, 19, 12, 17, 2, 6, 1, 22] decodiert. Dank dieses Designs verweist jede Anweisung nur auf Positionen in der Nachschlagetabelle, statt vollständige 32-Byte-Adressen zu wiederholen.

Signaturen: Autorisierungsnachweis

signatures enthält die kryptografischen Signaturen, die belegen, dass die erforderlichen Konten diese Transaktion autorisiert haben. Die Anzahl der Signaturen muss mit header.numRequiredSignatures übereinstimmen.

Nachschlagen in Adresstabellen

Wenn versioned den Wert true hat, erscheint addressTableLookups mit einer On-Chain-Tabelle und zwei Indexlisten. Nachschlagetabellen erhöhen die feste Obergrenze für die Anzahl der Adressen auf mehrere Dutzend und halten das Paket zugleich unter der MTU von 1.232 Byte.

Transaktion v1: Compute-Budget im Header

Transaktion v1 (SIMD-0385, Agave 4.2) fügt der Message ein weiteres Feld hinzu: transactionConfig.
Eine v1-Transaktion enthält ihr Compute-Budget hier und nicht in Anweisungen des ComputeBudget-Programms. Daher enthält das instructions-Array einer v1-Transaktion nie einen ComputeBudget111111111111111111111111111111-Eintrag. priorityFee ist die Gesamtgebühr in Lamports für die gesamte Transaktion und nicht die Anzahl der Mikro-Lamports pro Compute-Einheit. Ein null-Feld bedeutet, dass der Sender keinen Wert festgelegt hat. Legacy- und v0-Messages haben kein transactionConfig. Daher kennzeichnet die Anwesenheit dieses Felds eine v1-Transaktion. Prüfe in deinem Decoder zwei Dinge:
  • Prioritätsgebühr extrahieren. Lies transactionConfig.priorityFee, wenn es vorhanden ist. Durchsuche ComputeBudget-Anweisungen nur bei Legacy- und v0-Transaktionen als Fallback. Code, der ausschließlich Anweisungen durchsucht, interpretiert jede v1-Transaktion so, als würde sie keine Prioritätsgebühr zahlen.
  • Proto-Version. yellowstone-grpc-proto 12.6.0 ist die erste Version, die die v1-Felder überträgt. helius-laserstream 0.8.4 (JavaScript), 0.6.3 (Rust) und 0.2.0 (Go) sind die ersten darauf basierenden SDK-Versionen. Ältere Versionen verwerfen transactionConfig ohne Hinweis.
Unter Unterstützung für Transaktion v1 findest du die vollständige Liste der Änderungen.

So hängt alles zusammen: der Ablauf

Ausgehend von den Grundlagen passiert Folgendes:
  1. Nachschlagetabelle erstellen: accountKeys listet alle Adressen auf, auf die diese Transaktion zugreift
  2. Regeln festlegen: header legt fest, wie viele Signaturen erforderlich und welche Konten schreibgeschützt sind
  3. Befehle erstellen: Jedes instruction verweist auf:
    • Ein Programm (über programIdIndex → accountKeys[index])
    • Die benötigten Konten (über accounts → mehrere accountKeys[index]-Positionen)
    • Die Anweisungsdaten (codiert in data)
  4. Autorisierung hinzufügen: signatures belegt, dass die erforderlichen Konten diese Transaktion genehmigt haben
  5. Ablaufzeit festlegen: recentBlockhash verhindert, dass diese Transaktion später erneut abgespielt werden kann

Die Ausführung: alles innerhalb von meta

Die Message zeigt, was der Benutzer tun wollte. Die Meta zeigt dagegen, was tatsächlich passiert ist, als die Validatoren die Transaktion ausgeführt haben.

Grundlegende Ausführungsinformationen

Erfolg/Fehlschlag
  • err: null = Erfolg
  • err: {...} = Fehlschlag mit Fehlerdetails
  • fee = für diese Transaktion berechnete Lamports
Kontostandsänderungen
Die Kontostands-Arrays entsprechen per Index dem accountKeys-Array:
  • Konto 0: 15000 Lamports verloren (Gebührenzahlung)
  • Konto 1: 1461600 Lamports erhalten (neues Konto erstellt)
  • Konto 3: 2001231920 Lamports erhalten (Programmkonto)
Compute-Nutzung
Zeigt, wie viel des angeforderten Compute-Budgets verwendet wurde.

Erweiterte Ausführungsdetails

Innere Anweisungen
Innere Anweisungen sind zusätzliche Anweisungen, die Programme während der Ausführung aufgerufen haben. Sie gehören nicht zur ursprünglichen Transaktion, sondern wurden durch die Hauptanweisungen ausgelöst. Logmeldungen
Logmeldungen zeichnen die Programmausführung chronologisch auf. Sie zeigen, welche Programme aufgerufen wurden und welche benutzerdefinierten Logmeldungen sie ausgegeben haben. Änderungen des Token-Guthabens
Änderungen des Token-Guthabens zeigen die Zustände von SPL-Token-Konten vor und nach der Transaktion. Dazu gehören menschenlesbare Beträge mit korrekter Dezimalstellenverarbeitung.

Praktische Decodierungsmuster

Hier sind gängige Muster, mit denen du nützliche Informationen aus decodierten Transaktionen extrahierst:

Vollständiges Beispiel: Decoder für Jupiter-Swaps

Hier ist ein vollständiges Beispiel, das Jupiter-Swap-Transaktionen decodiert und relevante Informationen extrahiert:
Dieses Beispiel zeigt, wie du die Decodierung der Message mit der Analyse der Meta kombinierst, um geschäftsrelevante Informationen aus komplexen DeFi-Transaktionen zu extrahieren.

Das Wichtigste

  • Zweiteilige Struktur: Jede Transaktion hat eine Message (was angefordert wurde) und eine Meta (was tatsächlich passiert ist)
  • Binärdaten decodieren: Verwende bs58.encode(), um Binärfelder in lesbare base58-Strings umzuwandeln
  • Kontoschlüssel nachschlagen: Anweisungen referenzieren Konten über ihren Index im accountKeys-Array
  • Kontostände verfolgen: Vergleiche preBalances und postBalances, um Änderungen zu erkennen
  • Transaktion v1: Lies das Compute-Budget und die Prioritätsgebühr aus transactionConfig, wenn es vorhanden ist. v1-Transaktionen enthalten keine ComputeBudget-Anweisungen
Um Solana-Transaktionen zu verstehen, musst du erkennen, dass sie auf Effizienz ausgelegt sind: Statt Adressen zu wiederholen, verwenden sie Nachschlagetabellen und Indizes. So minimieren sie die Transaktionsgröße und maximieren zugleich die Informationsdichte.