solana-go

repository·main·Indexed 23 days ago

https://github.com/solana-foundation/solana-go

A Go library for interfacing with Solana's JSON RPC and WebSocket interfaces. It provides clients for native programs and the Solana Program Library (SPL), supporting features such as transaction signing, SOL transfers, Borsh encoding/decoding, Address Lookup Table (ALT) resolution, and RPC rate-limiting.

Tokens
18.5K
Snippets
22
Records
125
Agent score
81%

What's inside solana-go

  1. Get started with the JSON-RPC 2.0 Client

    main

    The client allows you to make JSON-RPC 2.0 calls over HTTP. You can use NewClient to initialize a client with a service URL.

    To retrieve data into a pointer, use CallFor(). To send data to the server, use Call().

    type Person struct {
        Id   int `json:"id"`
        Name string `json:"name"`
        Age  int `json:"age"`
    }
    
    func main() {
        rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
    
        var person *Person
        // CallFor unmarshals the response into the provided pointer
        rpcClient.CallFor(&person, "getPersonById", 4711)
    
        person.Age = 33
        // Call sends the object as parameters
        rpcClient.Call("updatePerson", person)
    }
  2. Resolve Address Lookup Tables in versioned transactions

    main

    For versioned transactions containing Address Lookup Tables (ALTs), you must resolve the lookups to access the full set of addresses.

    1. Verify the transaction is versioned using tx.Message.IsVersioned().
    2. Extract table IDs using tx.Message.GetAddressTableLookups().GetTableIDs().
    3. Fetch account info for each table ID via rpcClient.GetAccountInfo().
    4. Decode the table state using lookup.DecodeAddressLookupTableState(info.GetBinary()).
    5. Map the table keys to their resolved addresses.
    6. Apply the resolutions to the message using tx.Message.SetAddressTables(resolutions).
    7. Call tx.Message.ResolveLookups() to finalize the process.
    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())
    }
  3. Configure RPC client with Custom Headers, OAuth, or Custom HTTP Client

    main

    Use NewClientWithOpts and RPCClientOpts to customize the underlying HTTP behavior.

    Custom Headers & Basic Auth: Set the CustomHeaders map in RPCClientOpts. For Basic Auth, manually encode the credentials.

    OAuth: Provide a custom HTTPClient configured with your OAuth credentials (e.g., using golang.org/x/oauth2/clientcredentials).

    Custom HTTP Client (e.g., Proxies): Provide a custom *http.Client to the HTTPClient field to control transport settings like proxies.

    // Custom Headers / Basic Auth
    rpcClient := jsonrpc.NewClientWithOpts("http://my-rpc-service:8080/rpc", &jsonrpc.RPCClientOpts{
        CustomHeaders: map[string]string{
            "Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("user:secret")),
        },
    })
    
    // Custom HTTP Client with Proxy
    proxyURL, _ := url.Parse("http://proxy:8080")
    transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
    httpClient := &http.Client{Transport: transport}
    
    rpcClient := jsonrpc.NewClientWithOpts("http://my-rpc-service:8080/rpc", &jsonrpc.RPCClientOpts{
        HTTPClient: httpClient,
    })
  4. Handle JSON-RPC responses and errors

    main

    When using Call, you must handle two layers of errors:

    1. Transport/HTTP Errors: Check the returned err from the Call method. If it is not nil, it might be an *HTTPError (e.g., 403, 404, 500).
    2. Protocol/RPC Errors: Even if err is nil, the RPC server might have returned an error object. Check if response.Error is not nil. You can inspect response.Error.Code, response.Error.Message, and response.Error.Data.

    Note: The result field in a successful response can still be nil.

  5. Parse and decode instructions from a transaction

    main

    To decode instructions within a transaction, you must first parse the transaction data into a *solana.Transaction object. Once parsed, you can resolve the program ID and accounts for a specific instruction.

    There are two ways to decode the instruction data:

    1. Program-specific decoding: If you know the program (e.g., system), use its specific decoder like system.DecodeInstruction(accounts, data).
    2. General decoding: Use solana.DecodeInstruction(progKey, accounts, data). This uses a central registry. To make decoders available for specific programs, you must blank-import their package (e.g., _ "github.com/gagliardetto/solana-go/programs/system"). For custom programs (like Anchor), use solana.MustRegisterInstructionDecoder(myProgramID, myDecoderFunc) to register a decoder manually.
    // 1. Parse transaction from binary data
    tx, err := solana.TransactionFromDecoder(bin.NewBinDecoder(data))
    
    // 2. Get instruction (e.g., the first one)
    i0 := tx.Message.Instructions[0]
    
    // 3. Resolve Program ID and Accounts
    progKey, err := tx.ResolveProgramIDIndex(i0.ProgramIDIndex)
    accounts, err := i0.ResolveInstructionAccounts(&tx.Message)
    
    // 4. Decode using the general registry
    decodedInstruction, err := solana.DecodeInstruction(
      progKey,
      accounts,
      i0.Data,
    )
  6. Configure timeouts and custom HTTP clients for RPC

    main

    You can manage RPC request lifecycles in two ways:

    1. Context Timeouts: Pass a context.WithTimeout to RPC method calls.
    2. Custom HTTP Client: Use rpc.NewWithCustomRPCClient to provide a pre-configured http.Client. This is useful for setting global timeouts, connection pooling (MaxIdleConnsPerHost), or custom transport settings (dial timeouts, keep-alives).
  7. Configure default commitment on the RPC Client

    main
    When creating a client via NewWithCommitment or NewWithTimeoutAndCommitment, you can pin a CommitmentType. This value is stored in the client and can be retrieved via DefaultCommitment(). This is useful for ensuring consistent consistency levels across multiple calls without explicitly passing the commitment every time.
  8. Manage durable transaction nonces

    main

    This package provides types and functions for managing Solana durable nonces, which allow transactions to be signed and sent without requiring a recent blockhash from the cluster. It includes support for both NonceVersionLegacy and NonceVersionCurrent versions, and provides helpers to decode, verify, and upgrade nonce accounts.

    Key concepts:

    • DurableNonce: A 32-byte value used in the recent_blockhash field of a transaction. It is derived from a blockhash using a domain-separation prefix to ensure it is not treated as a real blockhash.
    • NonceState: A discriminated union that is either NonceStateUninitialized or NonceStateInitialized (containing NonceData).
    • NonceVersions: The top-level type stored in a nonce account, wrapping a NonceState with its NonceVersion.
  9. How to resolve Address Table Lookups in a versioned Message

    main

    Versioned (V0+) Solana messages can use Address Table Lookups to reference accounts indirectly. To use these accounts (e.g., to get the full list of keys or account metadata), you must first provide the actual table contents to the message.

    1. Identify Table IDs: Use Message.GetAddressTableLookups().GetTableIDs() to get the list of required address table account keys.
    2. Fetch Tables: Fetch the account data for these IDs from the blockchain (e.g., via RPC getMultipleAccounts).
    3. Decode Tables: Decode the account data using addresslookuptable.DecodeAddressLookupTableState to get a map[PublicKey]PublicKeySlice.
    4. Set Tables: Pass this map to Message.SetAddressTables(tables).
    5. Resolve: Call Message.ResolveLookups() to append the resolved accounts to the AccountKeys slice.

    Note: If you attempt to call methods like AccountMetaList(), ResolveLookups(), or Writable() on a versioned message without calling SetAddressTables first, the library will return ErrAddressTablesNotSet.