Skip to content

go ​

Send a transaction using Go with raw HTTP requests.

Full Example ​

go
package main

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

const ENDPOINT = "https://fra.blocksprint.io/api/v1/YOUR_API_KEY"

// Reuse one client so the connection to BlockSprint stays open between sends.
var httpClient = &http.Client{Timeout: 10 * time.Second}

type JsonRpcRequest struct {
	Jsonrpc string        `json:"jsonrpc"`
	ID      int           `json:"id"`
	Method  string        `json:"method"`
	Params  []interface{} `json:"params"`
}

type JsonRpcResponse struct {
	Jsonrpc string    `json:"jsonrpc"`
	ID      int       `json:"id"`
	Result  string    `json:"result,omitempty"`
	Error   *RpcError `json:"error,omitempty"`
}

type RpcError struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

func (e *RpcError) Error() string {
	return fmt.Sprintf("RPC error %d: %s", e.Code, e.Message)
}

type SendOptions struct {
	Encoding      string `json:"encoding"`
	SkipPreflight bool   `json:"skipPreflight"`
	MaxRetries    int    `json:"maxRetries"`
}

func sendTransaction(transactionBytes []byte) (string, error) {
	base64Tx := base64.StdEncoding.EncodeToString(transactionBytes)

	request := JsonRpcRequest{
		Jsonrpc: "2.0",
		ID:      1,
		Method:  "sendTransaction",
		Params: []interface{}{
			base64Tx,
			SendOptions{
				Encoding:      "base64",
				SkipPreflight: true,
				MaxRetries:    0,
			},
		},
	}

	body, err := json.Marshal(request)
	if err != nil {
		return "", err
	}

	resp, err := httpClient.Post(ENDPOINT, "application/json", bytes.NewReader(body))
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	// Errors come back as JSON-RPC errors with an HTTP 4xx/5xx status.
	var response JsonRpcResponse
	if err := json.Unmarshal(respBody, &response); err != nil {
		return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, respBody)
	}
	if response.Error != nil {
		return "", response.Error
	}

	// A signature means "accepted", not "landed": confirm it on your Solana RPC.
	return response.Result, nil
}

func main() {
	// Your signed, serialized transaction bytes (wire format)
	txBytes, err := os.ReadFile("signed_tx.bin")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	signature, err := sendTransaction(txBytes)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Signature: %s\n", signature)
}

Using gagliardetto/solana-go ​

go
package main

import (
	"context"
	"fmt"
	"log"
	"math/rand"
	"os"

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

const (
	// Your Solana RPC, for the blockhash and confirmations.
	RPC_URL = "https://api.mainnet-beta.solana.com"
	// BlockSprint, for sending.
	ENDPOINT = "https://fra.blocksprint.io/api/v1/YOUR_API_KEY"
)

// BlockSprint tip addresses
var TIP_ADDRESSES = []solana.PublicKey{
	solana.MustPublicKeyFromBase58("Sp1x2AqpQckPLaWnWCJUNg8k6qQexfaEWcSRKf5JcDV"),
	solana.MustPublicKeyFromBase58("Sp4JHSh9cksfzXbgK7Pq2ovtn8LirLQydaJKTsiNT77"),
	solana.MustPublicKeyFromBase58("Sp1xMS2cbw83SZDNr4AGqkBYYLjb3LvVnmDSrTMaHkr"),
	solana.MustPublicKeyFromBase58("SpagSJmnh8E9cGT5Y431xPPaS2c1xLREGGCWN9yDeUf"),
	solana.MustPublicKeyFromBase58("SpWrza9E63MQuHeGnnfzmtLVCs3pBdjyKPXUABPo9nq"),
	solana.MustPublicKeyFromBase58("SpXyoqMHJEhsNQ9fHh3DTGGzrGroZ5zfDtJwFvN4kXi"),
	solana.MustPublicKeyFromBase58("Spv2g3L837bVeHwkyb18LhA5P1bvJbkvaZbxcZXbwC2"),
	solana.MustPublicKeyFromBase58("SpDUe5MyFGG97SmdBQMWNPbx2Et9bZt2TCbZm3TujgX"),
	solana.MustPublicKeyFromBase58("SpKZ28tFzQ13kVjHhsyDEiQfkPGjA7Wmk185KcEDyuh"),
	solana.MustPublicKeyFromBase58("SpxPqegfoR3QkBN7guRUiU6QfhtTW1CnKSTSSapumqm"),
}

func getRandomTipAddress() solana.PublicKey {
	return TIP_ADDRESSES[rand.Intn(len(TIP_ADDRESSES))]
}

func main() {
	ctx := context.Background()
	rpcClient := rpc.New(RPC_URL)
	blocksprint := rpc.New(ENDPOINT)

	// Your funded wallet (base58 secret key) and the recipient
	wallet := solana.MustPrivateKeyFromBase58(os.Getenv("PAYER_SECRET_BASE58"))
	recipient := solana.MustPublicKeyFromBase58(os.Getenv("RECIPIENT"))

	// Get blockhash from your RPC
	recent, err := rpcClient.GetLatestBlockhash(ctx, rpc.CommitmentConfirmed)
	if err != nil {
		log.Fatal(err)
	}

	// Build transaction
	tx, err := solana.NewTransaction(
		[]solana.Instruction{
			// Your transfer
			system.NewTransferInstruction(
				1_000_000,
				wallet.PublicKey(),
				recipient,
			).Build(),

			// Tip instruction (0.001 SOL minimum)
			system.NewTransferInstruction(
				1_000_000,
				wallet.PublicKey(),
				getRandomTipAddress(),
			).Build(),
		},
		recent.Value.Blockhash,
		solana.TransactionPayer(wallet.PublicKey()),
	)
	if err != nil {
		log.Fatal(err)
	}

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

	// Send through BlockSprint (base64 is solana-go's default encoding)
	maxRetries := uint(0)
	sig, err := blocksprint.SendTransactionWithOpts(ctx, tx, rpc.TransactionOpts{
		SkipPreflight: true,
		MaxRetries:    &maxRetries,
	})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Signature: %s\n", sig)
}

With Retry Logic ​

Re-send the same signed transaction every ~2 seconds until it confirms or its blockhash expires. Uses sendTransaction from the Full Example; sig is tx.Signatures[0] and lastValidBlockHeight is recent.Value.LastValidBlockHeight.

go
// Additional imports: "context", "errors", "time",
// "github.com/gagliardetto/solana-go", "github.com/gagliardetto/solana-go/rpc"
func sendUntilConfirmed(
	ctx context.Context,
	rpcClient *rpc.Client,
	txBytes []byte,
	sig solana.Signature,
	lastValidBlockHeight uint64,
) error {
	for {
		if _, err := sendTransaction(txBytes); err != nil {
			var rpcErr *RpcError
			// JSON-RPC errors (key, tip, format) will not succeed on retry.
			// Network errors are worth retrying.
			if errors.As(err, &rpcErr) {
				return err
			}
		}

		time.Sleep(2 * time.Second)

		statuses, err := rpcClient.GetSignatureStatuses(ctx, false, sig)
		if err == nil && len(statuses.Value) > 0 && statuses.Value[0] != nil {
			status := statuses.Value[0]
			if status.Err != nil {
				return fmt.Errorf("transaction failed: %v", status.Err)
			}
			if status.ConfirmationStatus == rpc.ConfirmationStatusConfirmed ||
				status.ConfirmationStatus == rpc.ConfirmationStatusFinalized {
				return nil
			}
		}

		height, err := rpcClient.GetBlockHeight(ctx, rpc.CommitmentConfirmed)
		if err == nil && height > lastValidBlockHeight {
			return errors.New("blockhash expired: rebuild and re-sign the transaction")
		}
	}
}

INFO

Transaction V1 needs solana-go v1.23.0 or later: build it with solana.TransactionV1Config, and set the compute-unit limit and the loaded-accounts data size limit explicitly (in V1 an unset limit is 0). Sign and send it like the examples above.