Go library to interface with Solana JSON RPC and WebSocket interfaces.
More contracts to come.
If you're using/developing Solana programs written in Anchor Framework, you can use anchor-go to generate Golang clients
- Solana SDK library for Go
- Contents
- Features
- Current development status
- Note
- Requirements
- Installation
- Pretty-Print transactions/instructions
- SendAndConfirmTransaction
- Address Lookup Tables
- V1 Transactions (SIMD-0385)
- Parse/decode an instruction from a transaction
- Borsh encoding/decoding
- ZSTD account data encoding
- Working with rate-limited RPC providers
- Custom Headers for authenticating with RPC providers
- Timeouts and Custom HTTP Clients
- Examples
- RPC Methods
- WebSocket Subscriptions
- About v2
- Contributing
- License
- Credits
- Full JSON RPC API
- Full WebSocket JSON streaming API
- Wallet, account, and keys management
- Clients for native programs
- system
- config
- stake
- vote
- BPF Loader
- compute-budget (for V1 transactions use the inline
solana.TransactionConfiginstead) - Secp256k1
- Clients for Solana Program Library (SPL)
- SPL token
- associated-token-account
- memo
- name-service
- ...
- Metaplex:
- auction
- metaplex
- token-metadata
- token-vault
- nft-candy-machine
- More programs
solana-go ships from two parallel branches:
main(v1) — recommended default. Backward-compatible improvements only. Latest release: v1.22.0.v2— opt-in. Same content asmainplus breaking API cleanups. Latest:v2.0.0-rc(release candidate). See About v2 below.
Most users should stay on v1. Only choose v2 if you specifically want the changes listed in About v2.
The RPC and WS client implementation is based on the Solana RPC API documentation.
- solana-go is in active development, so all APIs are subject to change.
- This code is unaudited. Use at your own risk.
- Go 1.25 or later
$ cd my-project
$ go get github.com/gagliardetto/solana-go@latestLooking for v2? See About v2.
Instructions can be pretty-printed with the String() method on a Transaction:
tx, err := solana.NewTransaction(
[]solana.Instruction{
system.NewTransferInstruction(
amount,
accountFrom.PublicKey(),
accountTo,
).Build(),
},
recent.Value.Blockhash,
solana.TransactionPayer(accountFrom.PublicKey()),
)
...
// Pretty print the transaction:
fmt.Println(tx.String())
// OR you can choose a destination and a title:
// tx.EncodeTree(text.NewTreeEncoder(os.Stdout, "Transfer SOL"))You can wait for a transaction confirmation using the github.com/gagliardetto/solana-go/rpc/sendAndConfirmTransaction package tools (for a complete example: see here)
// Send transaction, and wait for confirmation:
sig, err := confirm.SendAndConfirmTransaction(
context.TODO(),
rpcClient,
wsClient,
tx,
)
if err != nil {
panic(err)
}
spew.Dump(sig)The above command will send the transaction, and wait for its confirmation.
Resolve lookups for a transaction:
package main
import (
"context"
"fmt"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/gagliardetto/solana-go"
lookup "github.com/gagliardetto/solana-go/programs/address-lookup-table"
"github.com/gagliardetto/solana-go/rpc"
"golang.org/x/time/rate"
)
func main() {
cluster := rpc.MainNetBeta
rpcClient := rpc.NewWithCustomRPCClient(rpc.NewWithLimiter(
cluster.RPC,
rate.Every(time.Second), // time frame
5, // limit of requests per time frame
))
version := uint64(0)
tx, err := rpcClient.GetTransaction(
context.Background(),
solana.MustSignatureFromBase58("24jRjMP3medE9iMqVSPRbkwfe9GdPmLfeftKPuwRHZdYTZJ6UyzNMGGKo4BHrTu2zVj4CgFF3CEuzS79QXUo2CMC"),
&rpc.GetTransactionOpts{
MaxSupportedTransactionVersion: &version,
Encoding: solana.EncodingBase64,
},
)
if err != nil {
panic(err)
}
parsed, err := tx.Transaction.GetTransaction()
if err != nil {
panic(err)
}
processTransactionWithAddressLookups(parsed, rpcClient)
}
func processTransactionWithAddressLookups(txx *solana.Transaction, rpcClient *rpc.Client) {
if !txx.Message.IsVersioned() {
fmt.Println("tx is not versioned; only versioned transactions can contain lookups")
return
}
tblKeys := txx.Message.GetAddressTableLookups().GetTableIDs()
if len(tblKeys) == 0 {
fmt.Println("no lookup tables in versioned transaction")
return
}
numLookups := txx.Message.GetAddressTableLookups().NumLookups()
if numLookups == 0 {
fmt.Println("no lookups in versioned transaction")
return
}
fmt.Println("num lookups:", numLookups)
fmt.Println("num tbl keys:", len(tblKeys))
resolutions := make(map[solana.PublicKey]solana.PublicKeySlice)
for _, key := range tblKeys {
fmt.Println("Getting table", key)
info, err := rpcClient.GetAccountInfo(
context.Background(),
key,
)
if err != nil {
panic(err)
}
fmt.Println("got table "+key.String())
tableContent, err := lookup.DecodeAddressLookupTableState(info.GetBinary())
if err != nil {
panic(err)
}
fmt.Println("table content:", spew.Sdump(tableContent))
fmt.Println("isActive", tableContent.IsActive())
resolutions[key] = tableContent.Addresses
}
err := txx.Message.SetAddressTables(resolutions)
if err != nil {
panic(err)
}
err = txx.Message.ResolveLookups()
if err != nil {
panic(err)
}
fmt.Println(txx.String())
}V1 is the transaction format introduced by SIMD-0385 (anza-xyz/solana-sdk #538). Compared to legacy/v0 transactions:
| legacy / v0 | v1 | |
|---|---|---|
| max transaction size | 1232 bytes | 4096 bytes |
| compute budget | ComputeBudget program instructions |
inline TransactionConfig in the message header |
| address lookup tables | v0 only | not supported |
| max signatures / addresses / instructions | bounded by the 1232-byte size (indices are u8) | 12 / 64 / 64 (enforced by Sanitize) |
| wire layout | sigs (compact-u16 len) || message |
0x81 || message || sigs (fixed, no len) |
The Go API stays the same; you opt in with solana.TransactionV1Config
(or solana.TransactionMessageVersion(solana.MessageVersionV1)):
package main
import (
"context"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/programs/system"
"github.com/gagliardetto/solana-go/rpc"
)
func main() {
ctx := context.Background()
client := rpc.New(rpc.DevNet_RPC)
sender, _ := solana.PrivateKeyFromSolanaKeygenFile("/path/to/id.json")
recipient := solana.NewWallet().PublicKey()
recent, err := client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized)
if err != nil {
panic(err)
}
// The compute budget lives in the message header; every field is optional.
config := solana.TransactionConfig{}.
WithComputeUnitLimit(20_000). // unset means 0 CUs -> always set it
WithPriorityFee(1_000) // TOTAL lamports, not micro-lamports/CU
// Also available: WithLoadedAccountsDataSizeLimit(bytes), WithHeapSize(bytes)
// (heap must be a multiple of 1024 within [32 KiB, 256 KiB]).
tx, err := solana.NewTransaction(
[]solana.Instruction{
system.NewTransferInstruction(solana.LAMPORTS_PER_SOL/1000, sender.PublicKey(), recipient).Build(),
},
recent.Value.Blockhash,
solana.TransactionPayer(sender.PublicKey()),
solana.TransactionV1Config(config), // <- selects the V1 format
)
if err != nil {
panic(err)
}
// Signing, sending and confirming are unchanged.
if _, err := tx.Sign(func(key solana.PublicKey) *solana.PrivateKey {
if sender.PublicKey().Equals(key) {
return &sender
}
return nil
}); err != nil {
panic(err)
}
sig, err := client.SendTransaction(ctx, tx)
if err != nil {
panic(err)
}
fmt.Println(sig)
}Decoding is automatic (TransactionFromBytes, TransactionFromBase64,
rpc results, JSON): the version byte 0x81 selects the V1 decoder.
tx, err := solana.TransactionFromBase64(b64)
if err != nil {
panic(err)
}
if tx.Message.GetVersion() == solana.MessageVersionV1 {
cfg := tx.Message.TransactionConfig // *uint64 / *uint32 fields, nil = not requested
if cfg.ComputeUnitLimit != nil {
fmt.Println("compute unit limit:", *cfg.ComputeUnitLimit)
}
}
if err := tx.Sanitize(); err != nil { // structural limits; the 4096-byte cap is enforced by the cluster
panic(err)
}When fetching transactions/blocks from the RPC, ask for version 1
(getTransaction, getBlock, blockSubscribe, ... all take the same option):
out, err := client.GetTransaction(ctx, sig, &rpc.GetTransactionOpts{
MaxSupportedTransactionVersion: &rpc.MaxSupportedTransactionVersion1,
})Things to keep in mind:
- The cluster must have SIMD-0385 activated, otherwise V1 transactions are rejected during sanitization.
PriorityFeeis the total priority fee in lamports for the whole transaction (unlikeSetComputeUnitPrice, which is micro-lamports per CU).- An unset
ComputeUnitLimitmeans a requested limit of 0 compute units. ComputeBudgetprogram instructions are no-ops in V1 transactions;NewTransactionrejects them for V1.- V1 transactions cannot use address lookup tables; combining
TransactionV1ConfigwithTransactionAddressTablesis an error. solana.MaxTransactionSizeV1(4096) is the size cap including signatures (checked by the cluster, not the SDK).
See rpc/examples/sendTransactionV1 and the
ExampleNewTransaction_v1 godoc example for complete programs.
package main
import (
"context"
"encoding/base64"
"os"
"reflect"
"github.com/davecgh/go-spew/spew"
bin "github.com/gagliardetto/binary"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/programs/system"
"github.com/gagliardetto/solana-go/rpc"
"github.com/gagliardetto/solana-go/text"
)
func main() {
exampleFromGetTransaction()
}
func exampleFromBase64() {
encoded := "AfjEs3XhTc3hrxEvlnMPkm/cocvAUbFNbCl00qKnrFue6J53AhEqIFmcJJlJW3EDP5RmcMz+cNTTcZHW/WJYwAcBAAEDO8hh4VddzfcO5jbCt95jryl6y8ff65UcgukHNLWH+UQGgxCGGpgyfQVQV02EQYqm4QwzUt2qf9f1gVLM7rI4hwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6ANIF55zOZWROWRkeh+lExxZBnKFqbvIxZDLE7EijjoBAgIAAQwCAAAAOTAAAAAAAAA="
data, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
panic(err)
}
// parse transaction:
tx, err := solana.TransactionFromDecoder(bin.NewBinDecoder(data))
if err != nil {
panic(err)
}
decodeSystemTransfer(tx)
}
func exampleFromGetTransaction() {
endpoint := rpc.TestNet_RPC
client := rpc.New(endpoint)
txSig := solana.MustSignatureFromBase58("3hZorctJtD3QLCRV3zF6JM6FDbFR5kAvsuKEG1RH9rWdz8YgnDzAvMWZFjdJgoL8KSNzZnx7aiExm1JEMC8KHfyy")
{
out, err := client.GetTransaction(
context.TODO(),
txSig,
&rpc.GetTransactionOpts{
Encoding: solana.EncodingBase64,
},
)
if err != nil {
panic(err)
}
tx, err := solana.TransactionFromDecoder(bin.NewBinDecoder(out.Transaction.GetBinary()))
if err != nil {
panic(err)
}
decodeSystemTransfer(tx)
}
}
func decodeSystemTransfer(tx *solana.Transaction) {
spew.Dump(tx)
// Get (for example) the first instruction of this transaction
// which we know is a `system` program instruction:
i0 := tx.Message.Instructions[0]
// Find the program address of this instruction:
progKey, err := tx.ResolveProgramIDIndex(i0.ProgramIDIndex)
if err != nil {
panic(err)
}
// Find the accounts of this instruction:
accounts, err := i0.ResolveInstructionAccounts(&tx.Message)
if err != nil {
panic(err)
}
// Feed the accounts and data to the system program parser
// OR see below for alternative parsing when you DON'T know
// what program the instruction is for / you don't have a parser.
inst, err := system.DecodeInstruction(accounts, i0.Data)
if err != nil {
panic(err)
}
// inst.Impl contains the specific instruction type (in this case, `inst.Impl` is a `*system.Transfer`)
spew.Dump(inst)
if _, ok := inst.Impl.(*system.Transfer); !ok {
panic("the instruction is not a *system.Transfer")
}
// OR
{
// There is a more general instruction decoder: `solana.DecodeInstruction`.
// It looks up the decoder for `progKey` in a central registry, so it
// works for any program ID regardless of what you know at compile time.
//
// Each `programs/<name>` package registers its decoder via init(),
// which only runs when the package is imported. If you are not
// already using the package's builders (e.g. `system.NewTransferInstruction`),
// blank-import it so the decoder is available - same idiom as
// database/sql drivers:
//
// import (
// _ "github.com/gagliardetto/solana-go/programs/system"
// _ "github.com/gagliardetto/solana-go/programs/token"
// // ...add more as needed
// )
//
// For a program that solana-go does not ship (e.g. a custom Anchor
// program), register its decoder yourself:
//
// solana.MustRegisterInstructionDecoder(myProgramID, myDecoderFunc)
decodedInstruction, err := solana.DecodeInstruction(
progKey,
accounts,
i0.Data,
)
if err != nil {
panic(err)
}
// The returned `decodedInstruction` is the decoded instruction.
spew.Dump(decodedInstruction)
// decodedInstruction == inst
if !reflect.DeepEqual(inst, decodedInstruction) {
panic("they are NOT equal (this would never happen)")
}
}
{
// pretty-print whole transaction:
_, err := tx.EncodeTree(text.NewTreeEncoder(os.Stdout, text.Bold("TEST TRANSACTION")))
if err != nil {
panic(err)
}
}
}You can use the github.com/gagliardetto/binary package for encoding/decoding borsh-encoded data:
Decoder:
resp, err := client.GetAccountInfo(
context.TODO(),
pubKey,
)
if err != nil {
panic(err)
}
borshDec := bin.NewBorshDecoder(resp.GetBinary())
var meta token_metadata.Metadata
err = borshDec.Decode(&meta)
if err != nil {
panic(err)
}Encoder:
buf := new(bytes.Buffer)
borshEncoder := bin.NewBorshEncoder(buf)
err := borshEncoder.Encode(meta)
if err != nil {
panic(err)
}
// fmt.Print(buf.Bytes())You can request account data to be encoded with base64+zstd in the Encoding parameter:
resp, err := client.GetAccountInfoWithOpts(
context.TODO(),
pubKey,
&rpc.GetAccountInfoOpts{
Encoding: solana.EncodingBase64Zstd,
Commitment: rpc.CommitmentFinalized,
},
)
if err != nil {
panic(err)
}
spew.Dump(resp)
var mint token.Mint
err = bin.NewDecoder(resp.GetBinary()).Decode(&mint)
if err != nil {
panic(err)
}
spew.Dump(mint)package main
import (
"context"
"golang.org/x/time/rate"
"github.com/davecgh/go-spew/spew"
"github.com/gagliardetto/solana-go/rpc"
)
func main() {
cluster := rpc.MainNetBeta
client := rpc.NewWithCustomRPCClient(rpc.NewWithLimiter(
cluster.RPC,
rate.Every(time.Second), // time frame
5, // limit of requests per time frame
))
out, err := client.GetVersion(
context.TODO(),
)
if err != nil {
panic(err)
}
spew.Dump(out)
}package main
import (
"context"
"golang.org/x/time/rate"
"github.com/davecgh/go-spew/spew"
"github.com/gagliardetto/solana-go/rpc"
)
func main() {
cluster := rpc.MainNetBeta
client := rpc.NewWithHeaders(
cluster.RPC,
map[string]string{
"x-api-key": "...",
},
)
out, err := client.GetVersion(
context.TODO(),
)
if err != nil {
panic(err)
}
spew.Dump(out)
}The data will AUTOMATICALLY get decoded and returned (the right decoder will be used) when you call the resp.GetBinary() method.
You can use a timeout context:
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
acc, err := rpcClient.GetAccountInfoWithOpts(
ctx,
accountID,
&rpc.GetAccountInfoOpts{
Commitment: rpc.CommitmentProcessed,
},
)Or you can initialize the RPC client using a custom HTTP client using rpc.NewWithCustomRPCClient:
import (
"net"
"net/http"
"time"
"github.com/gagliardetto/solana-go/rpc"
"github.com/gagliardetto/solana-go/rpc/jsonrpc"
)
func NewHTTPTransport(
timeout time.Duration,
maxIdleConnsPerHost int,
keepAlive time.Duration,
) *http.Transport {
return &http.Transport{
IdleConnTimeout: timeout,
MaxIdleConnsPerHost: maxIdleConnsPerHost,
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: timeout,
KeepAlive: keepAlive,
}).Dial,
}
}
// NewHTTP returns a new Client from the provided config.
func NewHTTP(
timeout time.Duration,
maxIdleConnsPerHost int,
keepAlive time.Duration,
) *http.Client {
tr := NewHTTPTransport(
timeout,
maxIdleConnsPerHost,
keepAlive,
)
return &http.Client{
Timeout: timeout,
Transport: tr,
}
}
// NewRPC creates a new Solana JSON RPC client.
func NewRPC(rpcEndpoint string) *rpc.Client {
var (
defaultMaxIdleConnsPerHost = 10
defaultTimeout = 25 * time.Second
defaultKeepAlive = 180 * time.Second
)
opts := &jsonrpc.RPCClientOpts{
HTTPClient: NewHTTP(
defaultTimeout,
defaultMaxIdleConnsPerHost,
defaultKeepAlive,
),
}
rpcClient := jsonrpc.NewClientWithOpts(rpcEndpoint, opts)
return rpc.NewWithCustomRPCClient(rpcClient)
}package main
import (
"context"
"fmt"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
)
func main() {
// Create a new account:
account := solana.NewWallet()
fmt.Println("account private key:", account.PrivateKey)
fmt.Println("account public key:", account.PublicKey())
// Create a new RPC client:
client := rpc.New(rpc.TestNet_RPC)
// Airdrop 1 SOL to the new account:
out, err := client.RequestAirdrop(
context.TODO(),
account.PublicKey(),
solana.LAMPORTS_PER_SOL*1,
rpc.CommitmentFinalized,
)
if err != nil {
panic(err)
}
fmt.Println("airdrop transaction signature:", out)
}{
// Load private key from a json file generated with
// $ solana-keygen new --outfile=standard.solana-keygen.json
privateKey, err := solana.PrivateKeyFromSolanaKeygenFile("/path/to/standard.solana-keygen.json")
if err != nil {
panic(err)
}
fmt.Println("private key:", privateKey.String())
// To get the public key, you need to call the `PublicKey()` method:
publicKey := privateKey.PublicKey()
// To get the base58 string of a public key, you can call the `String()` method:
fmt.Println("public key:", publicKey.String())
}
{
// Load private key from base58:
{
privateKey, err := solana.PrivateKeyFromBase58("66cDvko73yAf8LYvFMM3r8vF5vJtkk7JKMgEKwkmBC86oHdq41C7i1a2vS3zE1yCcdLLk6VUatUb32ZzVjSBXtRs")
if err != nil {
panic(err)
}
fmt.Println("private key:", privateKey.String())
fmt.Println("public key:", privateKey.PublicKey().String())
}
// OR:
{
privateKey := solana.MustPrivateKeyFromBase58("66cDvko73yAf8LYvFMM3r8vF5vJtkk7JKMgEKwkmBC86oHdq41C7i1a2vS3zE1yCcdLLk6VUatUb32ZzVjSBXtRs")
_ = privateKey
}
}
{
// Generate a new key pair:
{
privateKey, err := solana.NewRandomPrivateKey()
if err != nil {
panic(err)
}
_ = privateKey
}
{
{ // Generate a new private key (a Wallet struct is just a wrapper around a private key)
account := solana.NewWallet()
_ = account
}
}
}
{
// Parse a public key from a base58 string:
{
publicKey, err := solana.PublicKeyFromBase58("F8UvVsKnzWyp2nF8aDcqvQ2GVcRpqT91WDsAtvBKCMt9")
if err != nil {
panic(err)
}
_ = publicKey
}
// OR:
{
publicKey := solana.MustPublicKeyFromBase58("F8UvVsKnzWyp2nF8aDcqvQ2GVcRpqT91WDsAtvBKCMt9")
_ = publicKey
}
}package main
import (
"context"
"fmt"
"os"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/programs/system"
"github.com/gagliardetto/solana-go/rpc"
confirm "github.com/gagliardetto/solana-go/rpc/sendAndConfirmTransaction"
"github.com/gagliardetto/solana-go/rpc/jsonrpc"
"github.com/gagliardetto/solana-go/rpc/ws"
"github.com/gagliardetto/solana-go/text"
)
func main() {
// Create a new RPC client:
rpcClient := rpc.New(rpc.DevNet_RPC)
// Create a new WS client (used for confirming transactions)
wsClient, err := ws.Connect(context.Background(), rpc.DevNet_WS)
if err != nil {
panic(err)
}
// Load the account that you will send funds FROM:
accountFrom, err := solana.PrivateKeyFromSolanaKeygenFile("/path/to/.config/solana/id.json")
if err != nil {
panic(err)
}
fmt.Println("accountFrom private key:", accountFrom)
fmt.Println("accountFrom public key:", accountFrom.PublicKey())
// The public key of the account that you will send sol TO:
accountTo := solana.MustPublicKeyFromBase58("TODO")
// The amount to send (in lamports);
// 1 sol = 1000000000 lamports
amount := uint64(3333)
if true {
// Airdrop 1 sol to the account so it will have something to transfer:
out, err := rpcClient.RequestAirdrop(
context.TODO(),
accountFrom.PublicKey(),
solana.LAMPORTS_PER_SOL*1,
rpc.CommitmentFinalized,
)
if err != nil {
panic(err)
}
fmt.Println("airdrop transaction signature:", out)
time.Sleep(time.Second * 5)
}
//---------------
recent, err := rpcClient.GetLatestBlockhash(context.TODO(), rpc.CommitmentFinalized)
if err != nil {
panic(err)
}
tx, err := solana.NewTransaction(
[]solana.Instruction{
system.NewTransferInstruction(
amount,
accountFrom.PublicKey(),
accountTo,
).Build(),
},
recent.Value.Blockhash,
solana.TransactionPayer(accountFrom.PublicKey()),
)
if err != nil {
panic(err)
}
_, err = tx.Sign(
func(key solana.PublicKey) *solana.PrivateKey {
if accountFrom.PublicKey().Equals(key) {
return &accountFrom
}
return nil
},
)
if err != nil {
panic(fmt.Errorf("unable to sign transaction: %w", err))
}
spew.Dump(tx)
// Pretty print the transaction:
tx.EncodeTree(text.NewTreeEncoder(os.Stdout, "Transfer SOL"))
// Send transaction, and wait for confirmation:
sig, err := confirm.SendAndConfirmTransaction(
context.TODO(),
rpcClient,
wsClient,
tx,
)
if err != nil {
panic(err)
}
spew.Dump(sig)
// Or just send the transaction WITHOUT waiting for confirmation:
// sig, err := rpcClient.SendTransactionWithOpts(
// context.TODO(),
// tx,
// false,
// rpc.CommitmentFinalized,
// )
// if err != nil {
// panic(err)
// }
// spew.Dump(sig)
}All RPC methods from the Solana JSON RPC API are supported.
Each method has a testable example in rpc/example_test.go that is rendered on
pkg.go.dev.
All WebSocket subscriptions from the Solana WebSocket API are supported.
Each subscription has a testable example in rpc/ws/example_test.go that is rendered on
pkg.go.dev.
A v2 branch exists in parallel with main. It contains everything on main plus three breaking changes that couldn't ship on v1 without disrupting existing users:
- Module path —
github.com/solana-foundation/solana-go/v2 - Loader program packages (
programs/loader-v2/-v3/-v4) realigned to on-chain program specs (#410) - WebSocket subscription API aligned to the JSON-RPC spec (#407)
v1 remains the recommended default. Choose v2 only if you specifically need one of the above.
To use v2 (currently v2.0.0-rc, a release candidate):
$ go get github.com/solana-foundation/solana-go/v2@v2.0.0-rc
Full v2 details, release notes, and migration guide:
We encourage everyone to contribute, submit issues, PRs, discuss. Every kind of help is welcome.
Please read CONTRIBUTING.md for guidelines on commit messages, semver policy, linting, and CI checks.
Before opening a PR, run:
go test ./... -count=1
golangci-lint runBoth must pass in CI.
- Gopher logo was originally created by Takuya Ueda (https://twitter.com/tenntenn). Licensed under the Creative Commons 3.0 Attributions license.

