Skip to content

go ​

Send a transaction over UDP from Go using the standard library net package.

Minimal Example ​

go
package main

import (
    "log"
    "net"
)

const (
    Endpoint = "fra.blocksprint.io:5000"
    APIKey   = "YOUR_API_KEY"
)

// SendUDP submits a serialized Solana transaction to BlockSprint over UDP.
// tx must be the raw wire bytes (the same bytes you would base64-encode for sendTransaction).
func SendUDP(tx []byte) error {
    pkt := make([]byte, 0, 1+len(APIKey)+len(tx))
    pkt = append(pkt, byte(len(APIKey))) // key_len
    pkt = append(pkt, []byte(APIKey)...) // key (ASCII)
    pkt = append(pkt, tx...)             // tx wire bytes

    addr, err := net.ResolveUDPAddr("udp", Endpoint)
    if err != nil {
        return err
    }
    conn, err := net.DialUDP("udp", nil, addr)
    if err != nil {
        return err
    }
    defer conn.Close()

    _, err = conn.Write(pkt)
    return err
}

func main() {
    var tx []byte // = your serialized signed transaction
    if err := SendUDP(tx); err != nil {
        log.Fatalf("send: %v", err)
    }
}

Full Example: Sign + Send with solana-go ​

go
package main

import (
    "context"
    "log"
    "net"

    "github.com/gagliardetto/solana-go"
    "github.com/gagliardetto/solana-go/programs/system"
    "github.com/gagliardetto/solana-go/rpc"
)

const (
    Endpoint    = "fra.blocksprint.io:5000"
    APIKey      = "YOUR_API_KEY"
    TipAddress  = "YOUR_TIP_ADDRESS"
    TipLamports = 1_000_000 // 0.001 SOL (minimum)
)

func sendOverUDP(tx []byte) error {
    pkt := make([]byte, 0, 1+len(APIKey)+len(tx))
    pkt = append(pkt, byte(len(APIKey)))
    pkt = append(pkt, []byte(APIKey)...)
    pkt = append(pkt, tx...)

    addr, err := net.ResolveUDPAddr("udp", Endpoint)
    if err != nil {
        return err
    }
    conn, err := net.DialUDP("udp", nil, addr)
    if err != nil {
        return err
    }
    defer conn.Close()
    _, err = conn.Write(pkt)
    return err
}

func main() {
    ctx := context.Background()
    rpcClient := rpc.New("YOUR_SOLANA_RPC_URL") // your own Solana RPC, only used for the blockhash

    // Build instructions
    payer := solana.MustPrivateKeyFromBase58("YOUR_BASE58_SECRET_KEY")
    recipient := solana.MustPublicKeyFromBase58("RECIPIENT_ADDRESS")
    tipPubkey := solana.MustPublicKeyFromBase58(TipAddress)

    transferIx := system.NewTransferInstruction(1_000_000, payer.PublicKey(), recipient).Build()
    tipIx := system.NewTransferInstruction(TipLamports, payer.PublicKey(), tipPubkey).Build()

    recent, err := rpcClient.GetLatestBlockhash(ctx, rpc.CommitmentConfirmed)
    if err != nil {
        log.Fatal(err)
    }

    tx, err := solana.NewTransaction(
        []solana.Instruction{transferIx, tipIx},
        recent.Value.Blockhash,
        solana.TransactionPayer(payer.PublicKey()),
    )
    if err != nil {
        log.Fatal(err)
    }

    _, err = tx.Sign(func(key solana.PublicKey) *solana.PrivateKey {
        if key.Equals(payer.PublicKey()) {
            return &payer
        }
        return nil
    })
    if err != nil {
        log.Fatal(err)
    }

    txBytes, err := tx.MarshalBinary()
    if err != nil {
        log.Fatal(err)
    }

    if err := sendOverUDP(txBytes); err != nil {
        log.Fatal(err)
    }

    log.Printf("Sent signature: %s", tx.Signatures[0])
}

Reusing the Connection (High Throughput) ​

DialUDP creates a "connected" UDP socket that's cheap to reuse:

go
package udpsender

import (
    "errors"
    "net"
    "sync"
)

type Sender struct {
    conn   *net.UDPConn
    prefix []byte // [key_len][key bytes] — concatenated to every tx
    mu     sync.Mutex
}

func New(endpoint, apiKey string) (*Sender, error) {
    if len(apiKey) < 16 || len(apiKey) > 128 {
        return nil, errors.New("api key length must be 16..128 bytes")
    }
    addr, err := net.ResolveUDPAddr("udp", endpoint)
    if err != nil {
        return nil, err
    }
    conn, err := net.DialUDP("udp", nil, addr)
    if err != nil {
        return nil, err
    }
    prefix := make([]byte, 1+len(apiKey))
    prefix[0] = byte(len(apiKey))
    copy(prefix[1:], apiKey)
    return &Sender{conn: conn, prefix: prefix}, nil
}

func (s *Sender) Send(tx []byte) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    pkt := append(append(make([]byte, 0, len(s.prefix)+len(tx)), s.prefix...), tx...)
    _, err := s.conn.Write(pkt)
    return err
}

func (s *Sender) Close() error {
    return s.conn.Close()
}

Notes ​

INFO

  • No response. Write returns once the OS has accepted the packet. There is no ACK from BlockSprint.
  • Silent drops. Bad keys, missing tips, and rate-limit hits drop the packet without an error.
  • MTU. Keep the total packet under ~1300 bytes (1 + key length + ≤ 1232 tx bytes).
  • Transaction V1. Build it with solana-go v1.23.0 or later (solana.TransactionV1Config, with the compute-unit limit and the loaded-accounts data size limit set). MarshalBinary then writes the V1 wire bytes. Keep V1 datagrams within about 1,400 bytes, and send larger V1 transactions (up to 4,096 bytes) via QUIC or sendTransaction.