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"
|
2024-10-25 01:10:21 -07:00
|
|
|
|
"github.com/asymmetric-research/solana_exporter/pkg/slog"
|
|
|
|
|
"go.uber.org/zap"
|
2021-01-03 14:58:24 -08:00
|
|
|
|
"io"
|
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
|
2024-10-25 01:10:21 -07:00
|
|
|
|
logger *zap.SugaredLogger
|
2021-01-03 14:58:24 -08:00
|
|
|
|
}
|
|
|
|
|
|
2024-10-25 01:00:52 -07:00
|
|
|
|
Request struct {
|
|
|
|
|
Jsonrpc string `json:"jsonrpc"`
|
|
|
|
|
Id int `json:"id"`
|
2024-10-02 05:56:38 -07:00
|
|
|
|
Method string `json:"method"`
|
|
|
|
|
Params []any `json:"params"`
|
2021-01-03 14:58:24 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Commitment string
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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 network’s 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 {
|
2024-10-25 01:10:21 -07:00
|
|
|
|
return &Client{HttpClient: http.Client{}, RpcUrl: rpcAddr, HttpTimeout: httpTimeout, logger: slog.Get()}
|
2021-01-03 14:58:24 -08:00
|
|
|
|
}
|
|
|
|
|
|
2024-10-08 08:16:10 -07:00
|
|
|
|
func getResponse[T any](
|
2024-10-27 09:14:08 -07:00
|
|
|
|
ctx context.Context, client *Client, method string, params []any, rpcResponse *Response[T],
|
2024-10-08 08:16:10 -07:00
|
|
|
|
) error {
|
2024-10-25 01:10:21 -07:00
|
|
|
|
logger := slog.Get()
|
2024-10-01 02:52:02 -07:00
|
|
|
|
// format request:
|
2024-10-25 01:00:52 -07:00
|
|
|
|
request := &Request{Jsonrpc: "2.0", Id: 1, Method: method, Params: params}
|
2024-10-01 02:52:02 -07:00
|
|
|
|
buffer, err := json.Marshal(request)
|
|
|
|
|
if err != nil {
|
2024-10-25 01:10:21 -07:00
|
|
|
|
logger.Fatalf("failed to marshal request: %v", err)
|
2024-10-01 02:52:02 -07:00
|
|
|
|
}
|
2024-10-25 01:10:21 -07:00
|
|
|
|
logger.Debugf("jsonrpc request: %s", string(buffer))
|
2024-10-01 02:52:02 -07:00
|
|
|
|
|
|
|
|
|
// 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-25 01:10:21 -07:00
|
|
|
|
logger.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 {
|
2024-10-28 09:19:07 -07:00
|
|
|
|
return fmt.Errorf("%s rpc call failed: %w", method, err)
|
2024-06-14 02:09:39 -07:00
|
|
|
|
}
|
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:
|
2024-10-25 01:10:21 -07:00
|
|
|
|
logger.Debugf("%s response: %v", method, string(body))
|
2024-06-14 02:09:39 -07:00
|
|
|
|
|
|
|
|
|
// unmarshal the response into the predicted format
|
2024-10-08 08:16:10 -07:00
|
|
|
|
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
|
2024-10-08 08:16:10 -07:00
|
|
|
|
if rpcResponse.Error.Code != 0 {
|
2024-10-18 07:57:44 -07:00
|
|
|
|
rpcResponse.Error.Method = method
|
2024-10-08 09:10:34 -07:00
|
|
|
|
return &rpcResponse.Error
|
2024-06-14 02:09:39 -07:00
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-07 05:06:46 -07:00
|
|
|
|
// 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) {
|
2024-10-27 09:14:08 -07:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-07 05:06:46 -07:00
|
|
|
|
// 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-28 09:19:07 -07:00
|
|
|
|
func (c *Client) GetVoteAccounts(ctx context.Context, commitment Commitment) (*VoteAccounts, error) {
|
2024-10-02 05:56:38 -07:00
|
|
|
|
// format params:
|
|
|
|
|
config := map[string]string{"commitment": string(commitment)}
|
|
|
|
|
|
2024-10-27 09:14:08 -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
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-07 05:06:46 -07:00
|
|
|
|
// 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-10-27 09:14:08 -07:00
|
|
|
|
var resp Response[struct {
|
2024-06-14 03:58:32 -07:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-07 05:06:46 -07:00
|
|
|
|
// 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-10-27 09:14:08 -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
|
|
|
|
|
2024-10-07 05:06:46 -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-28 09:19:07 -07:00
|
|
|
|
ctx context.Context, commitment Commitment, firstSlot int64, lastSlot int64,
|
2024-10-02 05:47:04 -07:00
|
|
|
|
) (*BlockProduction, error) {
|
2024-06-14 03:58:32 -07:00
|
|
|
|
// format params:
|
2024-10-28 09:19:07 -07:00
|
|
|
|
config := map[string]any{
|
|
|
|
|
"commitment": string(commitment),
|
|
|
|
|
"range": map[string]int64{"firstSlot": firstSlot, "lastSlot": lastSlot},
|
2024-10-02 05:47:04 -07:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 03:58:32 -07:00
|
|
|
|
// make request:
|
2024-10-27 09:14:08 -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
|
|
|
|
|
2024-10-07 05:06:46 -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-27 09:14:08 -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
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-07 05:06:46 -07:00
|
|
|
|
// 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-28 09:19:07 -07:00
|
|
|
|
ctx context.Context, commitment Commitment, addresses []string, epoch int64,
|
2024-10-06 07:52:18 -07:00
|
|
|
|
) ([]InflationReward, error) {
|
2024-10-02 08:08:46 -07:00
|
|
|
|
// format params:
|
2024-10-28 09:19:07 -07:00
|
|
|
|
config := map[string]any{"commitment": string(commitment), "epoch": epoch}
|
2024-10-02 08:08:46 -07:00
|
|
|
|
|
2024-10-27 09:14:08 -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
|
|
|
|
}
|
2024-10-07 05:06:46 -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) {
|
2024-10-07 05:06:46 -07:00
|
|
|
|
config := map[string]any{"commitment": string(commitment)}
|
2024-10-27 09:14:08 -07:00
|
|
|
|
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 {
|
2024-10-07 05:06:46 -07:00
|
|
|
|
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) {
|
2024-10-28 09:19:07 -07:00
|
|
|
|
detailsOptions := []string{"full", "none"}
|
2024-10-15 08:50:04 -07:00
|
|
|
|
if !slices.Contains(detailsOptions, transactionDetails) {
|
2024-10-25 01:10:21 -07:00
|
|
|
|
c.logger.Fatalf(
|
2024-10-15 08:50:04 -07:00
|
|
|
|
"%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 {
|
2024-10-08 09:10:34 -07:00
|
|
|
|
// as per https://solana.com/docs/rpc/http/getblock
|
2024-10-25 01:10:21 -07:00
|
|
|
|
c.logger.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
|
|
|
|
}
|
2024-10-27 09:14:08 -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
|
2024-10-27 09:14:08 -07:00
|
|
|
|
func (c *Client) GetHealth(ctx context.Context) (string, error) {
|
|
|
|
|
var resp Response[string]
|
2024-10-18 02:28:22 -07:00
|
|
|
|
if err := getResponse(ctx, c, "getHealth", []any{}, &resp); err != nil {
|
2024-10-27 09:14:08 -07:00
|
|
|
|
return "", err
|
2024-10-18 02:28:22 -07:00
|
|
|
|
}
|
2024-10-27 09:14:08 -07:00
|
|
|
|
return resp.Result, nil
|
2024-10-18 02:28:22 -07:00
|
|
|
|
}
|
2024-10-18 12:05:47 -07:00
|
|
|
|
|
2024-10-25 01:10:21 -07:00
|
|
|
|
// GetMinimumLedgerSlot returns the lowest slot that the node has information about in its ledger.
|
2024-10-21 00:50:15 -07:00
|
|
|
|
// See API docs: https://solana.com/docs/rpc/http/minimumledgerslot
|
2024-10-27 09:14:08 -07:00
|
|
|
|
func (c *Client) GetMinimumLedgerSlot(ctx context.Context) (int64, error) {
|
|
|
|
|
var resp Response[int64]
|
2024-10-21 00:50:15 -07:00
|
|
|
|
if err := getResponse(ctx, c, "minimumLedgerSlot", []any{}, &resp); err != nil {
|
2024-10-27 09:14:08 -07:00
|
|
|
|
return 0, err
|
2024-10-21 00:50:15 -07:00
|
|
|
|
}
|
2024-10-27 09:14:08 -07:00
|
|
|
|
return resp.Result, nil
|
2024-10-21 00:50:15 -07:00
|
|
|
|
}
|
|
|
|
|
|
2024-10-23 14:48:37 -07:00
|
|
|
|
// GetFirstAvailableBlock returns the slot of the lowest confirmed block that has not been purged from the ledger
|
2024-10-21 00:50:15 -07:00
|
|
|
|
// See API docs: https://solana.com/docs/rpc/http/getfirstavailableblock
|
2024-10-27 09:14:08 -07:00
|
|
|
|
func (c *Client) GetFirstAvailableBlock(ctx context.Context) (int64, error) {
|
|
|
|
|
var resp Response[int64]
|
2024-10-21 00:50:15 -07:00
|
|
|
|
if err := getResponse(ctx, c, "getFirstAvailableBlock", []any{}, &resp); err != nil {
|
2024-10-27 09:14:08 -07:00
|
|
|
|
return 0, err
|
2024-10-21 00:50:15 -07:00
|
|
|
|
}
|
2024-10-27 09:14:08 -07:00
|
|
|
|
return resp.Result, nil
|
2024-10-21 00:50:15 -07:00
|
|
|
|
}
|