litegeyser
v0.0.3
Published
Experimental in-memory Yellowstone gRPC (Dragon's Mouth) geyser server for Node/TypeScript integration tests; no validator, ships a LiteSVM adapter
Readme
litegeyser
📍 Overview
litegeyser is an experimental in-memory Yellowstone gRPC (Dragon's Mouth) geyser server for Rust
and Node. It's useful for integration tests against a real gRPC server, and you can also write tests
without a gRPC client at all (reading updates in-process). Under the hood the real Yellowstone
pipeline code is compiled and run as-is, but instead of a Solana validator as the data source,
notifications are driven through an API. The result is deterministic notifications produced by real
Yellowstone logic, with full support for commitment levels, from_slot replay, filtering, block
sealing and arbitrary interleaving of account, transaction and slot updates within and across slots.
The core is written in Rust and exposed to both Rust (the crate builds as an rlib) and Node (a
native addon via NAPI-RS).
🚀 Getting Started
🔧 Installation
npm install --save-dev litegeyser🤖 Minimal Example
The primary use case: the code under test connects with a stock @triton-one/yellowstone-grpc
client, exactly as it would to a real Dragon's Mouth endpoint, while your test emits the specific
notifications it needs and drives the slot lifecycle. Each GeyserServer binds its own ephemeral
loopback port, so independent test files (even running in parallel) never collide.
import Client, { CommitmentLevel, SubscribeRequest } from "@triton-one/yellowstone-grpc";
import { GeyserServer } from "litegeyser";
const server = new GeyserServer(); // binds an ephemeral 127.0.0.1 port
const client = new Client(`http://127.0.0.1:${server.port()}`, undefined, undefined);
const stream = await client.subscribe(SubscribeRequest.fromPartial({
commitment: CommitmentLevel.PROCESSED,
accounts: { client: {} },
}));
stream.on("data", (update) => console.log(update)); // SubscribeUpdate protobufs
// Emit exactly the notifications your test needs, then drive the slot lifecycle.
server.notifyAccount({
pubkey, // 32-byte account key
owner, // 32-byte owner program
lamports: 1_000_000n,
data: new Uint8Array(),
executable: false,
rentEpoch: 0n,
slot: 1n,
writeVersion: 1n,
});
server.setSlotProcessed({ slot: 1n }); // close slot 1 -> Slot(Processed)
server.setSlotConfirmed(1n); // promote -> confirmed subscribers get it nowModules
litegeyser/litesvm
The second primary use case: get real geyser notifications for transactions and accounts straight
from a LiteSVM execution. Instead of hand-crafting updates, run
a real transaction on litesvm and have the resulting account and transaction updates fed into the
pipeline for you, with the metadata (CPI inner instructions, token balances, loaded_addresses)
derived along the way.
import { LiteSVM } from "litesvm";
import { sendTransaction } from "litegeyser/litesvm";
const svm = new LiteSVM();
svm.warpToSlot(1n);
sendTransaction(tx, svm, server); // run tx on litesvm, feed its updates in
sendTransaction(tx2, svm, server); // same slot, index 1, until you close it
server.setSlotProcessed({ slot: 1n }); // close slot 1 -> Slot(Processed) + BlockMetalitegeyser/client
Read decoded updates without a gRPC client at all. Thin in-process wrappers own the vendored
Yellowstone proto, so tests never touch protobufjs. After subscribe (and after any setFilter),
ping(server, sub) awaits a round-trip pong, confirming the filter has been applied before you rely
on it; getUpdates(server, sub, n) then awaits exactly n updates (u64 fields decode to bigint).
import { subscribe, ping, setFilter, getUpdates, CommitmentLevel } from "litegeyser/client";
const sub = await subscribe(server, { commitment: CommitmentLevel.PROCESSED, accounts: { client: {} } });
await ping(server, sub); // barrier: the filter is now applied
// ... emit notifications ...
const updates = await getUpdates(server, sub, 3); // awaits exactly 3 decoded SubscribeUpdates
setFilter(server, sub, { commitment: CommitmentLevel.CONFIRMED, transactions: { client: {} }, fromSlot: 1 });
await ping(server, sub); // barrier: the new filter is applied
server.setSlotConfirmed(1n); // promote slot 1 -> confirmed sub now gets the txAPI
new GeyserServer(options?): channelCapacity, replayStoredSlots, xToken,
maxDecodingMessageSize, filterName(s)SizeLimit (each a ConfigGrpc knob).
subscribe(reqBytes) -> id: open a stream and return its id (the filter is queued, not yet awaited).await ping(id, timeoutMs?): send a ping and await the matching pong, confirming the filter (and any precedingsetFilter) has been applied. Use aftersubscribe/setFilter.notifyTransaction(n) -> slot: feed an executed transaction (emits the transaction only).notifyAccount({ pubkey, lamports, owner, data, executable, rentEpoch, slot, writeVersion, txnSignature? }): an account update. WithtxnSignatureit's a tx's writable-changed account, without it a standalone update.setSlotProcessed({ slot, parentSlot?, blockhash?, parentBlockhash?, blockHeight?, blockTime? }): mark a slot Processed (block fields also seal aBlockMeta/Block).setSlotConfirmed(slot)/setSlotFinalized(slot): promote a slot; the pipeline re-broadcasts to confirmed / finalized subscribers.notifySlotStatus(slot, status, parent?, deadReason?): fork/liveness states beyond the commitment lifecycle (firstShredReceived/completed/createdBank/dead, plus skipped parents).setFilter(id, reqBytes): live filter/commitment change orfrom_slotreplay.getUpdates(id, count?, timeoutMs?):count0 returns whatever is queued now;count = Nawaits exactly N (up totimeoutMs).unsubscribe(id),port(),shutdown().
Known gaps & out of scope
Not implemented, and not planned:
- Exact
TransactionError(a failed tx carries a generic error, not the specific code). - Entry messages and entry subscriptions (
entries_count = 0). - Unary RPCs (
getSlot,getBlockHeight,getLatestBlockhash,isBlockhashValid,getVersion). - Real
block_height(the slot number is used as a synthetic value). - The
is_startupsnapshot account stream. - Rewards and vote transactions.
