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:
Dial a regional endpoint such as
frankfurt.nextblock.io:11100Authenticate once on a bidirectional stream
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:11100for your nearest region.Replace
rawTxwith the serialized bytes of your signed transaction.Keep one
NextblockQuicClientalive 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 intoSendTransaction().
See QUIC Transaction Submission for the full endpoint list and protocol summary.
Last updated