solana_exporter/pkg/rpc/client.go

317 lines
12 KiB
Go
Raw Normal View History

2021-01-03 05:30:23 -08:00
package rpc
import (
"bytes"
"context"
2021-01-03 14:58:24 -08:00
"encoding/json"
2024-06-14 02:09:39 -07:00
"fmt"
2021-01-03 14:58:24 -08:00
"io"
"k8s.io/klog/v2"
2021-01-03 05:30:23 -08:00
"net/http"
2024-10-15 08:50:04 -07:00
"slices"
2024-10-15 03:30:53 -07:00
"time"
2021-01-03 05:30:23 -08:00
)
2021-01-03 14:58:24 -08:00
type (
2024-06-11 02:34:27 -07:00
Client struct {
2024-10-15 03:30:53 -07:00
HttpClient http.Client
RpcUrl string
HttpTimeout time.Duration
2021-01-03 14:58:24 -08:00
}
rpcRequest struct {
2024-10-02 05:56:38 -07:00
Version string `json:"jsonrpc"`
ID int `json:"id"`
Method string `json:"method"`
Params []any `json:"params"`
2021-01-03 14:58:24 -08:00
}
Commitment string
)
2024-06-12 02:23:33 -07:00
// Provider is an interface that defines the methods required to interact with the Solana blockchain.
// It provides methods to retrieve block production information, epoch info, slot info, vote accounts, and node version.
2024-06-11 13:23:10 -07:00
type Provider interface {
2024-06-12 02:23:33 -07:00
// GetBlockProduction retrieves the block production information for the specified slot range.
// The method takes a context for cancellation, and pointers to the first and last slots of the range.
// It returns a BlockProduction struct containing the block production details, or an error if the operation fails.
2024-10-02 05:47:04 -07:00
GetBlockProduction(
2024-10-07 06:05:24 -07:00
ctx context.Context, commitment Commitment, identity *string, firstSlot *int64, lastSlot *int64,
2024-10-02 05:47:04 -07:00
) (*BlockProduction, error)
2024-06-12 02:23:33 -07:00
// GetEpochInfo retrieves the information regarding the current epoch.
// The method takes a context for cancellation and a commitment level to specify the desired state.
// It returns a pointer to an EpochInfo struct containing the epoch details, or an error if the operation fails.
2024-06-11 13:23:10 -07:00
GetEpochInfo(ctx context.Context, commitment Commitment) (*EpochInfo, error)
2024-06-12 02:23:33 -07:00
// GetSlot retrieves the current slot number.
// The method takes a context for cancellation.
// It returns the current slot number as an int64, or an error if the operation fails.
2024-10-07 06:05:24 -07:00
GetSlot(ctx context.Context, commitment Commitment) (int64, error)
2024-06-12 02:23:33 -07:00
// GetVoteAccounts retrieves the vote accounts information.
// The method takes a context for cancellation and a slice of parameters to filter the vote accounts.
2024-06-14 02:09:39 -07:00
// It returns a pointer to a VoteAccounts struct containing the vote accounts details,
2024-06-12 02:23:33 -07:00
// or an error if the operation fails.
2024-10-02 05:56:38 -07:00
GetVoteAccounts(ctx context.Context, commitment Commitment, votePubkey *string) (*VoteAccounts, error)
2024-06-12 02:23:33 -07:00
// GetVersion retrieves the version of the Solana node.
// The method takes a context for cancellation.
2024-06-14 02:09:39 -07:00
// It returns a string containing the version information, or an error if the operation fails.
2024-06-13 15:13:53 -07:00
GetVersion(ctx context.Context) (string, error)
2024-10-01 02:52:02 -07:00
// GetBalance returns the SOL balance of the account at the provided address
2024-10-07 06:05:24 -07:00
GetBalance(ctx context.Context, commitment Commitment, address string) (float64, error)
2024-10-06 07:52:18 -07:00
// GetInflationReward returns the inflation rewards (in lamports) awarded to the given addresses (vote accounts)
// during the given epoch.
GetInflationReward(
2024-10-07 06:05:24 -07:00
ctx context.Context, commitment Commitment, addresses []string, epoch *int64, minContextSlot *int64,
2024-10-06 07:52:18 -07:00
) ([]InflationReward, error)
2024-10-07 07:55:24 -07:00
GetLeaderSchedule(ctx context.Context, commitment Commitment, slot int64) (map[string][]int64, error)
2024-10-15 08:50:04 -07:00
GetBlock(ctx context.Context, commitment Commitment, slot int64, transactionDetails string) (*Block, error)
2024-10-18 02:28:22 -07:00
GetHealth(ctx context.Context) (*string, error)
2024-06-11 13:23:10 -07:00
}
2021-01-03 14:58:24 -08:00
func (c Commitment) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{"commitment": string(c)})
2021-01-03 05:30:23 -08:00
}
2021-01-03 14:58:24 -08:00
const (
2024-10-06 07:52:18 -07:00
// LamportsInSol is the number of lamports in 1 SOL (a billion)
LamportsInSol = 1_000_000_000
2024-10-02 05:47:04 -07:00
// CommitmentFinalized level offers the highest level of certainty for a transaction on the Solana blockchain.
// A transaction is considered “Finalized” when it is included in a block that has been confirmed by a
// supermajority of the stake, and at least 31 additional confirmed blocks have been built on top of it.
CommitmentFinalized Commitment = "finalized"
// CommitmentConfirmed level is reached when a transaction is included in a block that has been voted on
// by a supermajority (66%+) of the networks stake.
CommitmentConfirmed Commitment = "confirmed"
// CommitmentProcessed level represents a transaction that has been received by the network and included in a block.
CommitmentProcessed Commitment = "processed"
2021-01-03 14:58:24 -08:00
)
2024-10-15 03:30:53 -07:00
func NewRPCClient(rpcAddr string, httpTimeout time.Duration) *Client {
return &Client{HttpClient: http.Client{}, RpcUrl: rpcAddr, HttpTimeout: httpTimeout}
2021-01-03 14:58:24 -08:00
}
func getResponse[T any](
2024-10-15 03:30:53 -07:00
ctx context.Context, client *Client, method string, params []any, rpcResponse *response[T],
) error {
2024-10-01 02:52:02 -07:00
// format request:
request := &rpcRequest{Version: "2.0", ID: 1, Method: method, Params: params}
buffer, err := json.Marshal(request)
if err != nil {
2024-10-06 07:58:15 -07:00
klog.Fatalf("failed to marshal request: %v", err)
2024-10-01 02:52:02 -07:00
}
klog.V(2).Infof("jsonrpc request: %s", string(buffer))
// make request:
2024-10-15 03:30:53 -07:00
ctx, cancel := context.WithTimeout(ctx, client.HttpTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "POST", client.RpcUrl, bytes.NewBuffer(buffer))
2024-10-01 02:52:02 -07:00
if err != nil {
2024-10-06 07:58:15 -07:00
klog.Fatalf("failed to create request: %v", err)
2024-10-01 02:52:02 -07:00
}
req.Header.Set("content-type", "application/json")
2024-10-15 03:30:53 -07:00
resp, err := client.HttpClient.Do(req)
2024-06-14 02:09:39 -07:00
if err != nil {
return fmt.Errorf("%s RPC call failed: %w", method, err)
}
2024-10-01 02:52:02 -07:00
//goland:noinspection GoUnhandledErrorResult
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("error processing %s rpc call: %w", method, err)
}
2024-06-14 02:09:39 -07:00
// log response:
klog.V(2).Infof("%s response: %v", method, string(body))
// unmarshal the response into the predicted format
if err = json.Unmarshal(body, rpcResponse); err != nil {
2024-06-14 02:09:39 -07:00
return fmt.Errorf("failed to decode %s response body: %w", method, err)
}
2024-10-14 08:20:34 -07:00
// check for an actual rpc error
if rpcResponse.Error.Code != 0 {
return &rpcResponse.Error
2024-06-14 02:09:39 -07:00
}
return nil
}
// GetEpochInfo returns information about the current epoch.
// See API docs: https://solana.com/docs/rpc/http/getepochinfo
2024-06-14 02:09:39 -07:00
func (c *Client) GetEpochInfo(ctx context.Context, commitment Commitment) (*EpochInfo, error) {
var resp response[EpochInfo]
2024-10-15 03:30:53 -07:00
if err := getResponse(ctx, c, "getEpochInfo", []any{commitment}, &resp); err != nil {
2024-06-14 02:09:39 -07:00
return nil, err
}
return &resp.Result, nil
}
// GetVoteAccounts returns the account info and associated stake for all the voting accounts in the current bank.
// See API docs: https://solana.com/docs/rpc/http/getvoteaccounts
2024-10-02 05:56:38 -07:00
func (c *Client) GetVoteAccounts(
ctx context.Context, commitment Commitment, votePubkey *string,
) (*VoteAccounts, error) {
// format params:
config := map[string]string{"commitment": string(commitment)}
if votePubkey != nil {
config["votePubkey"] = *votePubkey
}
2024-06-14 02:09:39 -07:00
var resp response[VoteAccounts]
2024-10-15 03:30:53 -07:00
if err := getResponse(ctx, c, "getVoteAccounts", []any{config}, &resp); err != nil {
2024-06-14 02:09:39 -07:00
return nil, err
}
return &resp.Result, nil
}
// GetVersion returns the current Solana version running on the node.
// See API docs: https://solana.com/docs/rpc/http/getversion
2024-06-14 02:09:39 -07:00
func (c *Client) GetVersion(ctx context.Context) (string, error) {
2024-06-14 03:58:32 -07:00
var resp response[struct {
Version string `json:"solana-core"`
}]
2024-10-15 03:30:53 -07:00
if err := getResponse(ctx, c, "getVersion", []any{}, &resp); err != nil {
2024-06-14 02:09:39 -07:00
return "", err
}
return resp.Result.Version, nil
}
// GetSlot returns the slot that has reached the given or default commitment level.
// See API docs: https://solana.com/docs/rpc/http/getslot
2024-10-07 05:49:37 -07:00
func (c *Client) GetSlot(ctx context.Context, commitment Commitment) (int64, error) {
config := map[string]string{"commitment": string(commitment)}
2024-06-14 02:09:39 -07:00
var resp response[int64]
2024-10-15 03:30:53 -07:00
if err := getResponse(ctx, c, "getSlot", []any{config}, &resp); err != nil {
2024-06-14 02:09:39 -07:00
return 0, err
}
return resp.Result, nil
}
2024-06-14 03:58:32 -07:00
// GetBlockProduction returns recent block production information from the current or previous epoch.
// See API docs: https://solana.com/docs/rpc/http/getblockproduction
2024-10-02 05:47:04 -07:00
func (c *Client) GetBlockProduction(
2024-10-07 06:05:24 -07:00
ctx context.Context, commitment Commitment, identity *string, firstSlot *int64, lastSlot *int64,
2024-10-02 05:47:04 -07:00
) (*BlockProduction, error) {
// can't provide a last slot without a first:
if firstSlot == nil && lastSlot != nil {
2024-10-06 07:58:15 -07:00
klog.Fatalf("can't provide a last slot without a first!")
2024-10-02 05:47:04 -07:00
}
2024-06-14 03:58:32 -07:00
// format params:
2024-10-07 06:05:24 -07:00
config := map[string]any{"commitment": string(commitment)}
2024-10-02 05:47:04 -07:00
if identity != nil {
config["identity"] = *identity
}
2024-06-14 03:58:32 -07:00
if firstSlot != nil {
2024-10-02 05:47:04 -07:00
blockRange := map[string]int64{"firstSlot": *firstSlot}
if lastSlot != nil {
2024-10-03 08:33:28 -07:00
// make sure first and last slot are in order:
if *firstSlot > *lastSlot {
2024-10-06 07:58:15 -07:00
err := fmt.Errorf("last slot %v is greater than first slot %v", *lastSlot, *firstSlot)
klog.Fatalf("%v", err)
2024-10-03 08:33:28 -07:00
}
2024-10-02 05:47:04 -07:00
blockRange["lastSlot"] = *lastSlot
}
config["range"] = blockRange
}
2024-06-14 03:58:32 -07:00
// make request:
2024-10-02 05:47:04 -07:00
var resp response[contextualResult[BlockProduction]]
2024-10-15 03:30:53 -07:00
if err := getResponse(ctx, c, "getBlockProduction", []any{config}, &resp); err != nil {
2024-06-14 03:58:32 -07:00
return nil, err
}
2024-10-02 05:47:04 -07:00
return &resp.Result.Value, nil
2024-06-14 03:58:32 -07:00
}
2024-10-01 02:52:02 -07:00
// GetBalance returns the lamport balance of the account of provided pubkey.
// See API docs:https://solana.com/docs/rpc/http/getbalance
2024-10-07 06:05:24 -07:00
func (c *Client) GetBalance(ctx context.Context, commitment Commitment, address string) (float64, error) {
config := map[string]string{"commitment": string(commitment)}
2024-10-02 05:47:04 -07:00
var resp response[contextualResult[int64]]
2024-10-15 03:30:53 -07:00
if err := getResponse(ctx, c, "getBalance", []any{address, config}, &resp); err != nil {
2024-10-01 02:52:02 -07:00
return 0, err
}
2024-10-02 08:08:46 -07:00
return float64(resp.Result.Value) / float64(LamportsInSol), nil
}
// GetInflationReward returns the inflation / staking reward for a list of addresses for an epoch.
// See API docs: https://solana.com/docs/rpc/http/getinflationreward
2024-10-02 08:08:46 -07:00
func (c *Client) GetInflationReward(
2024-10-07 06:05:24 -07:00
ctx context.Context, commitment Commitment, addresses []string, epoch *int64, minContextSlot *int64,
2024-10-06 07:52:18 -07:00
) ([]InflationReward, error) {
2024-10-02 08:08:46 -07:00
// format params:
2024-10-06 07:52:18 -07:00
config := map[string]any{"commitment": string(commitment)}
2024-10-02 08:08:46 -07:00
if epoch != nil {
config["epoch"] = *epoch
}
if minContextSlot != nil {
config["minContextSlot"] = *minContextSlot
}
2024-10-06 07:52:18 -07:00
var resp response[[]InflationReward]
2024-10-15 03:30:53 -07:00
if err := getResponse(ctx, c, "getInflationReward", []any{addresses, config}, &resp); err != nil {
2024-10-02 08:08:46 -07:00
return nil, err
}
2024-10-06 07:52:18 -07:00
return resp.Result, nil
2024-10-01 02:52:02 -07:00
}
// GetLeaderSchedule returns the leader schedule for an epoch.
// See API docs: https://solana.com/docs/rpc/http/getleaderschedule
2024-10-07 07:55:24 -07:00
func (c *Client) GetLeaderSchedule(ctx context.Context, commitment Commitment, slot int64) (map[string][]int64, error) {
config := map[string]any{"commitment": string(commitment)}
var resp response[map[string][]int64]
2024-10-15 03:30:53 -07:00
if err := getResponse(ctx, c, "getLeaderSchedule", []any{slot, config}, &resp); err != nil {
return nil, err
}
return resp.Result, nil
}
2024-10-07 05:49:37 -07:00
// GetBlock returns identity and transaction information about a confirmed block in the ledger.
// See API docs: https://solana.com/docs/rpc/http/getblock
2024-10-15 08:50:04 -07:00
func (c *Client) GetBlock(
ctx context.Context, commitment Commitment, slot int64, transactionDetails string,
) (*Block, error) {
detailsOptions := []string{"full", "accounts", "none"}
if !slices.Contains(detailsOptions, transactionDetails) {
klog.Fatalf(
"%s is not a valid transaction-details option, must be one of %v", transactionDetails, detailsOptions,
)
}
2024-10-07 05:49:37 -07:00
if commitment == CommitmentProcessed {
// as per https://solana.com/docs/rpc/http/getblock
klog.Fatalf("commitment '%v' is not supported for GetBlock", CommitmentProcessed)
2024-10-07 05:49:37 -07:00
}
config := map[string]any{
2024-10-15 08:50:04 -07:00
"commitment": commitment,
"encoding": "json", // this is default, but no harm in specifying it
"transactionDetails": transactionDetails,
"rewards": true, // what we here for!
"maxSupportedTransactionVersion": 0,
2024-10-07 05:49:37 -07:00
}
var resp response[Block]
2024-10-15 03:30:53 -07:00
if err := getResponse(ctx, c, "getBlock", []any{slot, config}, &resp); err != nil {
2024-10-07 05:49:37 -07:00
return nil, err
}
return &resp.Result, nil
}
2024-10-18 02:28:22 -07:00
// GetHealth returns the current health of the node. A healthy node is one that is within a blockchain-configured slots
// of the latest cluster confirmed slot.
// See API docs: https://solana.com/docs/rpc/http/gethealth
func (c *Client) GetHealth(ctx context.Context) (*string, error) {
var resp response[string]
if err := getResponse(ctx, c, "getHealth", []any{}, &resp); err != nil {
return nil, err
}
return &resp.Result, nil
}