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"
|
|
|
|
|
)
|
|
|
|
|
|
2021-01-03 14:58:24 -08:00
|
|
|
|
type (
|
2024-06-11 02:34:27 -07:00
|
|
|
|
Client struct {
|
2021-01-03 14:58:24 -08:00
|
|
|
|
httpClient http.Client
|
|
|
|
|
rpcAddr string
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 03:58:32 -07:00
|
|
|
|
rpcError struct {
|
2024-05-29 04:56:04 -07:00
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
Code int64 `json:"code"`
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-03 14:58:24 -08:00
|
|
|
|
rpcRequest struct {
|
|
|
|
|
Version string `json:"jsonrpc"`
|
|
|
|
|
ID int `json:"id"`
|
|
|
|
|
Method string `json:"method"`
|
|
|
|
|
Params []interface{} `json:"params"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(
|
|
|
|
|
ctx context.Context, identity *string, firstSlot *int64, lastSlot *int64,
|
|
|
|
|
) (*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-06-11 13:23:10 -07:00
|
|
|
|
GetSlot(ctx context.Context) (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-06-12 02:21:16 -07:00
|
|
|
|
GetVoteAccounts(ctx context.Context, params []interface{}) (*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
|
|
|
|
|
GetBalance(ctx context.Context, address string) (float64, 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-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-06-11 02:34:27 -07:00
|
|
|
|
func NewRPCClient(rpcAddr string) *Client {
|
2024-10-01 02:52:02 -07:00
|
|
|
|
return &Client{httpClient: http.Client{}, rpcAddr: rpcAddr}
|
2021-01-03 14:58:24 -08:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 02:09:39 -07:00
|
|
|
|
func (c *Client) getResponse(ctx context.Context, method string, params []interface{}, result HasRPCError) 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 {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
klog.V(2).Infof("jsonrpc request: %s", string(buffer))
|
|
|
|
|
|
|
|
|
|
// make request:
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", c.rpcAddr, bytes.NewBuffer(buffer))
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
req.Header.Set("content-type", "application/json")
|
|
|
|
|
|
|
|
|
|
resp, err := c.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, result); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to decode %s response body: %w", method, err)
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-01 12:49:48 -07:00
|
|
|
|
// last error check:
|
2024-06-14 02:09:39 -07:00
|
|
|
|
if result.getError().Code != 0 {
|
|
|
|
|
return fmt.Errorf("RPC error: %d %v", result.getError().Code, result.getError().Message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *Client) GetEpochInfo(ctx context.Context, commitment Commitment) (*EpochInfo, error) {
|
|
|
|
|
var resp response[EpochInfo]
|
|
|
|
|
if err := c.getResponse(ctx, "getEpochInfo", []interface{}{commitment}, &resp); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &resp.Result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *Client) GetVoteAccounts(ctx context.Context, params []interface{}) (*VoteAccounts, error) {
|
|
|
|
|
var resp response[VoteAccounts]
|
|
|
|
|
if err := c.getResponse(ctx, "getVoteAccounts", params, &resp); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return &resp.Result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-06-14 02:09:39 -07:00
|
|
|
|
if err := c.getResponse(ctx, "getVersion", []interface{}{}, &resp); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
return resp.Result.Version, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *Client) GetSlot(ctx context.Context) (int64, error) {
|
|
|
|
|
var resp response[int64]
|
|
|
|
|
if err := c.getResponse(ctx, "getSlot", []interface{}{}, &resp); err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
return resp.Result, nil
|
|
|
|
|
}
|
2024-06-14 03:58:32 -07:00
|
|
|
|
|
2024-10-02 05:47:04 -07:00
|
|
|
|
func (c *Client) GetBlockProduction(
|
|
|
|
|
ctx context.Context, identity *string, firstSlot *int64, lastSlot *int64,
|
|
|
|
|
) (*BlockProduction, error) {
|
|
|
|
|
// can't provide a last slot without a first:
|
|
|
|
|
if firstSlot == nil && lastSlot != nil {
|
|
|
|
|
panic("can't provide a last slot without a first!")
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-14 03:58:32 -07:00
|
|
|
|
// format params:
|
2024-10-02 05:47:04 -07:00
|
|
|
|
config := make(map[string]interface{})
|
|
|
|
|
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 {
|
|
|
|
|
blockRange["lastSlot"] = *lastSlot
|
|
|
|
|
}
|
|
|
|
|
config["range"] = blockRange
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var params []interface{}
|
|
|
|
|
if len(config) > 0 {
|
|
|
|
|
params = append(params, config)
|
2024-06-14 03:58:32 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// make request:
|
2024-10-02 05:47:04 -07:00
|
|
|
|
var resp response[contextualResult[BlockProduction]]
|
2024-06-14 03:58:32 -07:00
|
|
|
|
if err := c.getResponse(ctx, "getBlockProduction", params, &resp); err != nil {
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
func (c *Client) GetBalance(ctx context.Context, address string) (float64, error) {
|
2024-10-02 05:47:04 -07:00
|
|
|
|
var resp response[contextualResult[int64]]
|
2024-10-01 02:52:02 -07:00
|
|
|
|
if err := c.getResponse(ctx, "getBalance", []interface{}{address}, &resp); err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
return float64(resp.Result.Value / 1_000_000_000), nil
|
|
|
|
|
}
|