For the complete documentation index, see llms.txt. This page is also available as Markdown.

QUIC Transaction Submission

Use QUIC when you want to send signed transaction bytes to NextBlock with minimal transport overhead from Go.

The pattern is simple:

  1. Dial a regional endpoint such as frankfurt.nextblock.io:11100

  2. Authenticate once on a bidirectional stream

  3. Reuse the same QUIC connection and open one unidirectional stream per transaction

Example

package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"io"
	"net"
	"os"
	"time"

	quic "github.com/quic-go/quic-go"
)

const (
	authOK    byte = 0x00
	maxTxSize      = 1232
)

type NextblockQuicClient struct {
	conn quic.Connection
}

func Connect(ctx context.Context, serverAddr string, apiKey string) (*NextblockQuicClient, error) {
	host, _, err := net.SplitHostPort(serverAddr)
	if err != nil {
		return nil, fmt.Errorf("invalid server address: %w", err)
	}

	conn, err := quic.DialAddr(ctx, serverAddr, &tls.Config{
		ServerName: host,
		NextProtos: []string{"nb-tx/1"},
		MinVersion: tls.VersionTLS13,
	}, &quic.Config{
		KeepAlivePeriod: 15 * time.Second,
		MaxIdleTimeout:  60 * time.Second,
	})
	if err != nil {
		return nil, fmt.Errorf("quic dial failed: %w", err)
	}

	authStream, err := conn.OpenStreamSync(ctx)
	if err != nil {
		conn.CloseWithError(1, "auth stream failed")
		return nil, fmt.Errorf("open auth stream: %w", err)
	}

	if _, err := authStream.Write([]byte(apiKey)); err != nil {
		conn.CloseWithError(1, "auth write failed")
		return nil, fmt.Errorf("write api key: %w", err)
	}
	if err := authStream.Close(); err != nil {
		conn.CloseWithError(1, "auth close failed")
		return nil, fmt.Errorf("close auth stream: %w", err)
	}

	response := make([]byte, 1)
	if _, err := io.ReadFull(authStream, response); err != nil {
		conn.CloseWithError(1, "auth read failed")
		return nil, fmt.Errorf("read auth response: %w", err)
	}
	if response[0] != authOK {
		conn.CloseWithError(1, "auth rejected")
		return nil, fmt.Errorf("authentication rejected")
	}

	return &NextblockQuicClient{conn: conn}, nil
}

func (c *NextblockQuicClient) SendTransaction(ctx context.Context, rawTx []byte) error {
	if len(rawTx) > maxTxSize {
		return fmt.Errorf("transaction too large: %d", len(rawTx))
	}

	stream, err := c.conn.OpenUniStreamSync(ctx)
	if err != nil {
		return fmt.Errorf("open tx stream: %w", err)
	}

	if _, err := stream.Write(rawTx); err != nil {
		return fmt.Errorf("write tx bytes: %w", err)
	}

	return stream.Close()
}

func (c *NextblockQuicClient) Close() error {
	return c.conn.CloseWithError(0, "client closing")
}

func main() {
	ctx := context.Background()
	apiKey := os.Getenv("NEXTBLOCK_API_KEY")
	if apiKey == "" {
		panic("set NEXTBLOCK_API_KEY before running")
	}

	client, err := Connect(ctx, "frankfurt.nextblock.io:11100", apiKey)
	if err != nil {
		panic(err)
	}
	defer client.Close()

	// Replace this with the serialized bytes of your signed Solana transaction.
	// Example with solana-go: rawTx, err := tx.MarshalBinary()
	rawTx := []byte{0, 1, 2, 3}

	if err := client.SendTransaction(ctx, rawTx); err != nil {
		panic(err)
	}

	fmt.Println("transaction queued")
}

What To Replace

  • Swap frankfurt.nextblock.io:11100 for your nearest region.

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

  • Keep one NextblockQuicClient alive and reuse it instead of reconnecting per send.

Notes

  • Send raw transaction bytes, not base64.

  • Each transaction goes on its own unidirectional stream.

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

  • If you already build transactions with solana-go, serialize the signed transaction and pass those bytes into SendTransaction().

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

Last updated