go
Send transactions over QUIC for the lowest possible latency.
Dependencies
bash
go get github.com/quic-go/quic-goFull Example
go
package blocksprintclient
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"errors"
"math/big"
"sync"
"time"
"github.com/quic-go/quic-go"
)
const (
alpnTPUProtocolID = "solana-tpu"
defaultServerName = "blocksprint-node"
defaultKeepAlive = 25 * time.Second
defaultIdleTimeout = 30 * time.Second
)
// Client maintains a QUIC connection to a Blocksprint ingress endpoint.
//
// Thread-safe: can be shared across goroutines.
type Client struct {
endpointAddr string
tlsConfig *tls.Config
quicConfig *quic.Config
connMu sync.RWMutex
conn *quic.Conn
reconnectMu sync.Mutex
}
// Connect creates a client using your Blocksprint API key.
func Connect(ctx context.Context, endpointAddr string, apiKey string) (*Client, error) {
cert, err := x509CertificateFromAPIKey(apiKey)
if err != nil {
return nil, err
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: defaultServerName,
InsecureSkipVerify: true,
NextProtos: []string{alpnTPUProtocolID},
MinVersion: tls.VersionTLS13,
}
quicConfig := &quic.Config{
KeepAlivePeriod: defaultKeepAlive,
MaxIdleTimeout: defaultIdleTimeout,
}
client := &Client{
endpointAddr: endpointAddr,
tlsConfig: tlsConfig,
quicConfig: quicConfig,
}
if err := client.reconnect(ctx); err != nil {
return nil, err
}
return client, nil
}
// Close closes the underlying QUIC connection.
func (c *Client) Close() error {
c.reconnectMu.Lock()
defer c.reconnectMu.Unlock()
c.connMu.Lock()
conn := c.conn
c.conn = nil
c.connMu.Unlock()
if conn == nil {
return nil
}
return conn.CloseWithError(0, "")
}
// SendTransaction sends a signed Solana transaction payload.
// The payload should be the raw wire bytes (e.g., from tx.MarshalBinary()).
// Automatically reconnects on failure and retries once.
func (c *Client) SendTransaction(ctx context.Context, transactionBytes []byte) error {
if len(transactionBytes) == 0 {
return nil
}
conn := c.getConn()
if conn != nil {
if err := trySendBytes(ctx, conn, transactionBytes); err == nil {
return nil
}
}
if err := c.reconnect(ctx); err != nil {
return err
}
conn = c.getConn()
if conn == nil {
return errors.New("missing QUIC connection")
}
return trySendBytes(ctx, conn, transactionBytes)
}
func (c *Client) getConn() *quic.Conn {
c.connMu.RLock()
defer c.connMu.RUnlock()
return c.conn
}
func (c *Client) reconnect(ctx context.Context) error {
c.reconnectMu.Lock()
defer c.reconnectMu.Unlock()
if existing := c.getConn(); existing != nil && existing.Context().Err() == nil {
return nil
}
conn, err := quic.DialAddr(ctx, c.endpointAddr, c.tlsConfig, c.quicConfig)
if err != nil {
return err
}
c.connMu.Lock()
old := c.conn
c.conn = conn
c.connMu.Unlock()
if old != nil {
_ = old.CloseWithError(0, "")
}
return nil
}
func trySendBytes(ctx context.Context, conn *quic.Conn, payload []byte) error {
stream, err := conn.OpenUniStreamSync(ctx)
if err != nil {
return err
}
if _, err := stream.Write(payload); err != nil {
_ = stream.Close()
return err
}
return stream.Close()
}
// x509CertificateFromAPIKey creates a self-signed X.509 certificate
// derived from the API key for client authentication.
func x509CertificateFromAPIKey(apiKey string) (tls.Certificate, error) {
// Derive keypair from API key using SHA256 hash
hash := sha256.Sum256([]byte(apiKey))
seed := hash[:]
priv := ed25519.NewKeyFromSeed(seed)
pub := priv.Public().(ed25519.PublicKey)
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return tls.Certificate{}, err
}
template := &x509.Certificate{
SerialNumber: serial,
NotBefore: time.Now().Add(-5 * time.Minute),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
der, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv)
if err != nil {
return tls.Certificate{}, err
}
return tls.Certificate{
Certificate: [][]byte{der},
PrivateKey: priv,
}, nil
}Usage
go
package main
import (
"context"
"log"
"time"
"github.com/gagliardetto/solana-go"
"example.com/yourapp/blocksprintclient" // the client above
)
func main() {
ctx := context.Background()
// Connect to Blocksprint
client, err := blocksprintclient.Connect(
ctx,
"fra.blocksprint.io:10000",
"YOUR_API_KEY",
)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Build your transaction
tx := /* your signed solana.Transaction */
txBytes, err := tx.MarshalBinary()
if err != nil {
log.Fatal(err)
}
// Send via QUIC
if err := client.SendTransaction(ctx, txBytes); err != nil {
log.Fatal(err)
}
log.Println("Transaction sent!")
// Give the transaction a moment to leave before Close
time.Sleep(time.Second)
}INFO
Notes
- The client automatically reconnects on connection failures
- Thread-safe: can be shared across goroutines
- Uses the same API key as HTTP endpoints
- Transactions must be fully signed before sending
- Transaction V1 (up to 4,096 bytes) is sent the same way:
tx.MarshalBinary()on a V1 transaction (solana-go v1.23.0 or later)
