QUIC Transaction Submission

Use QUIC when you already have signed Solana transaction bytes and want a low-overhead submission path from Node.js or TypeScript.

This example uses @matrixai/quicarrow-up-right and follows the same flow as the other languages:

  1. Connect to a regional endpoint with ALPN nb-tx/1

  2. Authenticate once with your API key on a bidirectional stream

  3. Reuse the connection and send each transaction on a unidirectional stream

Example

import { randomFillSync } from "node:crypto";
import { QUICClient } from "@matrixai/quic";

const AUTH_OK = 0x00;
const MAX_TX_SIZE = 1232;

class NextblockQuicClient {
  constructor(private readonly client: QUICClient) {}

  static async connect(serverAddr: string, apiKey: string): Promise<NextblockQuicClient> {
    const [host, portString] = serverAddr.split(":");
    const port = Number(portString);

    const client = await QUICClient.createQUICClient({
      host,
      port,
      serverName: host,
      crypto: {
        ops: {
          async randomBytes(data: ArrayBuffer): Promise<void> {
            randomFillSync(new Uint8Array(data));
          },
        },
      },
      config: {
        applicationProtos: ["nb-tx/1"],
        maxIdleTimeout: 60_000,
        keepAliveIntervalTime: 15_000,
      },
    });

    const authStream = client.connection.newStream("bidi");
    const authWriter = authStream.writable.getWriter();
    await authWriter.write(new TextEncoder().encode(apiKey));
    await authWriter.close();

    const authReader = authStream.readable.getReader();
    const { value } = await authReader.read();
    if (!value || value[0] !== AUTH_OK) {
      await client.destroy();
      throw new Error("Authentication rejected");
    }

    return new NextblockQuicClient(client);
  }

  async sendTransaction(rawTx: Uint8Array): Promise<void> {
    if (rawTx.byteLength > MAX_TX_SIZE) {
      throw new Error(`Transaction too large: ${rawTx.byteLength}`);
    }

    const txStream = this.client.connection.newStream("uni");
    const txWriter = txStream.writable.getWriter();
    await txWriter.write(rawTx);
    await txWriter.close();
  }

  async close(): Promise<void> {
    await this.client.destroy();
  }
}

async function main(): Promise<void> {
  const apiKey = process.env.NEXTBLOCK_API_KEY;
  if (!apiKey) {
    throw new Error("Set NEXTBLOCK_API_KEY before running");
  }

  const client = await NextblockQuicClient.connect(
    "amsterdam.nextblock.io:11100",
    apiKey,
  );

  try {
    // Replace this with the serialized bytes of your signed transaction.
    // Example with @solana/web3.js: const rawTx = signedTransaction.serialize();
    const rawTx = new Uint8Array([0, 1, 2, 3]);
    await client.sendTransaction(rawTx);
    console.log("transaction queued");
  } finally {
    await client.close();
  }
}

void main();

What To Replace

  • Replace amsterdam.nextblock.io:11100 with the region closest to you.

  • Read the API key from NEXTBLOCK_API_KEY or your own config source.

  • Replace rawTx with the serialized bytes of your signed transaction.

Notes

  • Send raw transaction bytes, not base64.

  • Reuse one NextblockQuicClient for many sends.

  • QUIC does not support the extra gRPC submission flags or atomic bundle submission.

  • If you build transactions with @solana/web3.js, serialize the signed transaction first and pass the resulting Uint8Array into sendTransaction().

See QUIC Transaction Submission for the full endpoint list and protocol summary.

Last updated