npm.io
6.0.0 • Published 3d ago

@triton-one/yellowstone-grpc

Licence
Apache-2.0
Version
6.0.0
Deps
4
Size
1.0 MB
Vulns
0
Weekly
0
Stars
989

Yellowstone Node.js gRPC client

This library implements a client for streaming account updates for backend applications.

You can find more information and documentation on the Triton One website.

Prerequisites

You need to have the latest version of protoc installed. Please refer to the installation guide on the Protobuf website.

Usage

Install required dependencies by running

npm install

Build the project (this will generate the gRPC client and compile TypeScript):

npm run build

Please refer to examples/typescript for some usage examples.

Auto reconnect

Standard subscribe streams can opt into the native Rust client's reconnect, backfill, and deduplication layer by passing reconnect options as the fourth constructor argument:

const client = new Client(endpoint, xToken, channelOptions, {
  backoff: {
    initialIntervalMs: 100,
    multiplier: 2,
    maxRetries: 10,
  },
  slotRetention: 250,
});

await client.connect();
const stream = await client.subscribe(request);

Omit the fourth argument, or pass { enabled: false }, to keep the previous no-reconnect behavior. Deshred subscriptions are unchanged.

Compressed account filters

For large account sets, use CompressedAccountFilterSet to send a compact cuckoo filter instead of a full explicit account list. The local set keeps exact membership for false-positive filtering.

import Client, {
  CompressedAccountFilterSet,
  SubscribeRequest,
  TokenAccountExpansionControlFlag,
} from "@triton-one/yellowstone-grpc";

const accounts = new CompressedAccountFilterSet(2_000_000);
for (const pubkey of trackedPubkeys) {
  accounts.insert(pubkey); // base58 string, Buffer, or Uint8Array
}

const request: SubscribeRequest = {
  accounts: {},
  slots: {},
  transactions: {},
  transactionsStatus: {},
  blocks: {},
  blocksMeta: {},
  entry: {},
  accountsDataSlice: [],
};

accounts.insertIntoSubscribeRequest(request, "tracked");
const stream = await client.subscribe(request);

stream.on("data", (update) => {
  const pubkey = update.account?.account?.pubkey;
  if (pubkey && accounts.contains(pubkey)) {
    // exact local match
  }
});

accounts.insert(newPubkey);
accounts.remove(oldPubkey);
accounts.insertIntoSubscribeRequest(request, "tracked");
stream.write(request);

Use insertIntoBlockSubscribeRequest(request, name) when filtering account includes inside block subscriptions.

Use insertIntoTransactionSubscribeRequest(request, name) for full transaction updates and insertIntoTransactionStatusSubscribeRequest(request, name) for transaction status updates:

accounts.insertIntoTransactionSubscribeRequest(request, "trackedTransactions");
accounts.insertIntoTransactionStatusSubscribeRequest(
  request,
  "trackedTransactionStatuses",
);

The compressed include filter and accountInclude use OR logic. Other fields, such as vote, failed, accountExclude, and accountRequired, are applied as separate conditions. Add them after creating the compressed filter:

request.transactions.trackedTransactions = {
  ...accounts.toTransactionFilter(),
  vote: false,
  failed: false,
  accountExclude: blockedPubkeys,
};

Transaction filters can also match token account owners.

  • ALL matches an owner listed before or after the transaction.
  • BALANCE_CHANGED matches an owner when its token balance changes. It also matches when a token account is created or closed.

This works with transaction and transaction status subscriptions. It applies to accountInclude, accountExclude, and accountRequired.

request.transactions.trackedTransactions = {
  ...accounts.toTransactionFilter(),
  tokenAccounts: TokenAccountExpansionControlFlag.BALANCE_CHANGED,
};

request.transactionsStatus.trackedTransactionStatuses = {
  ...accounts.toTransactionFilter(),
  tokenAccounts: TokenAccountExpansionControlFlag.ALL,
};

If you do not set tokenAccounts, filters only match transaction account keys.

A compressed filter can match an account that you did not add. Before using a full transaction update, check its account keys against your local set. If you set tokenAccounts, also check the token account owners:

stream.on("data", (update) => {
  const info = update.transaction?.transaction;
  if (!info) return;

  const accountKeys = [
    ...(info.transaction?.message?.accountKeys ?? []),
    ...(info.meta?.loadedWritableAddresses ?? []),
    ...(info.meta?.loadedReadonlyAddresses ?? []),
  ];

  if (!accountKeys.some((pubkey) => accounts.contains(pubkey))) {
    return; // compressed-filter false positive
  }

  // exact local match
});

Transaction status updates do not include account keys. A false positive from a transactionsStatus filter cannot be removed from that update alone. Use a full transaction subscription or another transaction data source when exact local matching is required.

Troubleshooting

For macOS:

You might have to run npm run build with RUSTFLAGS="-Clink-arg=-undefined -Clink-arg=dynamic_lookup" to skip the strict linkers from failing the build step and resolve dylibs via runtime.

RUSTFLAGS="-Clink-arg=-undefined -Clink-arg=dynamic_lookup" npm run build

Working

Since the start, the @triton-one/yellowstone-grpc package has used the @grpc/grpc-js lib for gRPC types enforcement, connection and subscription management. This hit a bottleneck, described in this blog

From v5.0.0 the napi-rs framework is used for gRPC connection and subscription management. It's described into this blog

These changes are internal to the SDK and do not have any breaking changes for client code. If you face any issues, please open an issue

The napi-rs based implementation is inspired from the implemenation of the LaserStream SDK

Type Compatibility

The public SDK always returns the generated protobuf-compatible types from src/grpc/geyser.ts.

  • Unary methods return generated response objects (for example PongResponse, GetSlotResponse, GetVersionResponse) instead of raw N-API wrapper shapes.
  • Subscription stream updates are normalized to SubscribeUpdate with top-level oneof fields (account, slot, transaction, etc).
  • The internal N-API Js... objects are an implementation detail and are converted automatically by the SDK wrapper.

This allows existing user code typed against the generated src/grpc types to remain stable while using the N-API backend.

Development

Local Testing

When building for local testing at the root of the project where the Makefile resides, you must:

  1. Clean build artifacts if any with make clean

  2. Navigate to the SDK (where this README resides) and install dependencies with npm install and npm run build:dev. Make sure to use build:dev to reflect local changes in your test runs and NOT build.

  3. Navigate to examples/typescript folder and install dependencies with npm install.

  4. Run client.ts with an example subscription request below: tsx examples/typescript/src/client.ts --endpoint <ENDPOINT> --x-token <X-TOKEN> --commitment processed subscribe --transactions TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

Keywords