Skip to content

Commit

Permalink
Merge pull request asymmetric-research#40 from asymmetric-research/rp…
Browse files Browse the repository at this point in the history
…c-errors

RPC errors
  • Loading branch information
johnstonematt authored Oct 14, 2024
2 parents a3c498d + bc96a3e commit b2cde35
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 33 deletions.
15 changes: 13 additions & 2 deletions cmd/solana_exporter/slots.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package main

import (
"context"
"errors"
"fmt"
"slices"
"strings"
"time"

"github.com/asymmetric-research/solana_exporter/pkg/rpc"
Expand Down Expand Up @@ -131,7 +133,8 @@ func (c *SlotWatcher) WatchSlots(ctx context.Context, pace time.Duration) {

ctx_, cancel := context.WithTimeout(ctx, httpTimeout)
// TODO: separate fee-rewards watching from general slot watching, such that general slot watching commitment level can be dropped to confirmed
epochInfo, err := c.client.GetEpochInfo(ctx_, rpc.CommitmentFinalized)
commitment := rpc.CommitmentFinalized
epochInfo, err := c.client.GetEpochInfo(ctx_, commitment)
cancel()
if err != nil {
klog.Errorf("Failed to get epoch info, bailing out: %v", err)
Expand All @@ -149,7 +152,7 @@ func (c *SlotWatcher) WatchSlots(ctx context.Context, pace time.Duration) {
// if we get here, then the tracking numbers are set, so this is a "normal" run.
// start by checking if we have progressed since last run:
if epochInfo.AbsoluteSlot <= c.slotWatermark {
klog.Infof("confirmed slot number has not advanced from %v, skipping", c.slotWatermark)
klog.Infof("%v slot number has not advanced from %v, skipping", commitment, c.slotWatermark)
continue
}

Expand Down Expand Up @@ -331,6 +334,14 @@ func (c *SlotWatcher) fetchAndEmitSingleFeeReward(
) error {
block, err := c.client.GetBlock(ctx, rpc.CommitmentConfirmed, slot)
if err != nil {
var rpcError *rpc.RPCError
if errors.As(err, &rpcError) {
// this is the error code for slot was skipped:
if rpcError.Code == rpc.SlotSkippedCode && strings.Contains(rpcError.Message, "skipped") {
klog.Infof("slot %v was skipped, no fee rewards.", slot)
return nil
}
}
return err
}

Expand Down
41 changes: 19 additions & 22 deletions pkg/rpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@ type (
rpcAddr string
}

rpcError struct {
Message string `json:"message"`
Code int64 `json:"code"`
}

rpcRequest struct {
Version string `json:"jsonrpc"`
ID int `json:"id"`
Expand Down Expand Up @@ -99,7 +94,9 @@ func NewRPCClient(rpcAddr string) *Client {
return &Client{httpClient: http.Client{}, rpcAddr: rpcAddr}
}

func (c *Client) getResponse(ctx context.Context, method string, params []any, result HasRPCError) error {
func getResponse[T any](
ctx context.Context, httpClient http.Client, url string, method string, params []any, rpcResponse *response[T],
) error {
// format request:
request := &rpcRequest{Version: "2.0", ID: 1, Method: method, Params: params}
buffer, err := json.Marshal(request)
Expand All @@ -109,13 +106,13 @@ func (c *Client) getResponse(ctx context.Context, method string, params []any, r
klog.V(2).Infof("jsonrpc request: %s", string(buffer))

// make request:
req, err := http.NewRequestWithContext(ctx, "POST", c.rpcAddr, bytes.NewBuffer(buffer))
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(buffer))
if err != nil {
klog.Fatalf("failed to create request: %v", err)
}
req.Header.Set("content-type", "application/json")

resp, err := c.httpClient.Do(req)
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("%s RPC call failed: %w", method, err)
}
Expand All @@ -130,23 +127,22 @@ func (c *Client) getResponse(ctx context.Context, method string, params []any, r
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 {
if err = json.Unmarshal(body, rpcResponse); err != nil {
return fmt.Errorf("failed to decode %s response body: %w", method, err)
}

// last error check:
if result.getError().Code != 0 {
return fmt.Errorf("RPC error: %d %v", result.getError().Code, result.getError().Message)
if rpcResponse.Error.Code != 0 {
return &rpcResponse.Error
}

return nil
}

// GetEpochInfo returns information about the current epoch.
// See API docs: https://solana.com/docs/rpc/http/getepochinfo
func (c *Client) GetEpochInfo(ctx context.Context, commitment Commitment) (*EpochInfo, error) {
var resp response[EpochInfo]
if err := c.getResponse(ctx, "getEpochInfo", []any{commitment}, &resp); err != nil {
if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getEpochInfo", []any{commitment}, &resp); err != nil {
return nil, err
}
return &resp.Result, nil
Expand All @@ -164,7 +160,7 @@ func (c *Client) GetVoteAccounts(
}

var resp response[VoteAccounts]
if err := c.getResponse(ctx, "getVoteAccounts", []any{config}, &resp); err != nil {
if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getVoteAccounts", []any{config}, &resp); err != nil {
return nil, err
}
return &resp.Result, nil
Expand All @@ -176,7 +172,7 @@ func (c *Client) GetVersion(ctx context.Context) (string, error) {
var resp response[struct {
Version string `json:"solana-core"`
}]
if err := c.getResponse(ctx, "getVersion", []any{}, &resp); err != nil {
if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getVersion", []any{}, &resp); err != nil {
return "", err
}
return resp.Result.Version, nil
Expand All @@ -187,7 +183,7 @@ func (c *Client) GetVersion(ctx context.Context) (string, error) {
func (c *Client) GetSlot(ctx context.Context, commitment Commitment) (int64, error) {
config := map[string]string{"commitment": string(commitment)}
var resp response[int64]
if err := c.getResponse(ctx, "getSlot", []any{config}, &resp); err != nil {
if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getSlot", []any{config}, &resp); err != nil {
return 0, err
}
return resp.Result, nil
Expand Down Expand Up @@ -223,7 +219,7 @@ func (c *Client) GetBlockProduction(

// make request:
var resp response[contextualResult[BlockProduction]]
if err := c.getResponse(ctx, "getBlockProduction", []any{config}, &resp); err != nil {
if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getBlockProduction", []any{config}, &resp); err != nil {
return nil, err
}
return &resp.Result.Value, nil
Expand All @@ -234,7 +230,7 @@ func (c *Client) GetBlockProduction(
func (c *Client) GetBalance(ctx context.Context, commitment Commitment, address string) (float64, error) {
config := map[string]string{"commitment": string(commitment)}
var resp response[contextualResult[int64]]
if err := c.getResponse(ctx, "getBalance", []any{address, config}, &resp); err != nil {
if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getBalance", []any{address, config}, &resp); err != nil {
return 0, err
}
return float64(resp.Result.Value) / float64(LamportsInSol), nil
Expand All @@ -255,7 +251,7 @@ func (c *Client) GetInflationReward(
}

var resp response[[]InflationReward]
if err := c.getResponse(ctx, "getInflationReward", []any{addresses, config}, &resp); err != nil {
if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getInflationReward", []any{addresses, config}, &resp); err != nil {
return nil, err
}
return resp.Result, nil
Expand All @@ -266,7 +262,7 @@ func (c *Client) GetInflationReward(
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]
if err := c.getResponse(ctx, "getLeaderSchedule", []any{slot, config}, &resp); err != nil {
if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getLeaderSchedule", []any{slot, config}, &resp); err != nil {
return nil, err
}
return resp.Result, nil
Expand All @@ -276,7 +272,8 @@ func (c *Client) GetLeaderSchedule(ctx context.Context, commitment Commitment, s
// See API docs: https://solana.com/docs/rpc/http/getblock
func (c *Client) GetBlock(ctx context.Context, commitment Commitment, slot int64) (*Block, error) {
if commitment == CommitmentProcessed {
klog.Fatalf("commitment %v is not supported for GetBlock", commitment)
// as per https://solana.com/docs/rpc/http/getblock
klog.Fatalf("commitment '%v' is not supported for GetBlock", CommitmentProcessed)
}
config := map[string]any{
"commitment": commitment,
Expand All @@ -285,7 +282,7 @@ func (c *Client) GetBlock(ctx context.Context, commitment Commitment, slot int64
"rewards": true, // what we here for!
}
var resp response[Block]
if err := c.getResponse(ctx, "getBlock", []any{slot, config}, &resp); err != nil {
if err := getResponse(ctx, c.httpClient, c.rpcAddr, "getBlock", []any{slot, config}, &resp); err != nil {
return nil, err
}
return &resp.Result, nil
Expand Down
23 changes: 23 additions & 0 deletions pkg/rpc/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package rpc

// error codes: https://github.com/anza-xyz/agave/blob/489f483e1d7b30ef114e0123994818b2accfa389/rpc-client-api/src/custom_error.rs#L17
const (
BlockCleanedUpCode = -32001
SendTransactionPreflightFailureCode = -32002
TransactionSignatureVerificationFailureCode = -32003
BlockNotAvailableCode = -32004
NodeUnhealthyCode = -32005
TransactionPrecompileVerificationFailureCode = -32006
SlotSkippedCode = -32007
NoSnapshotCode = -32008
LongTermStorageSlotSkippedCode = -32009
KeyExcludedFromSecondaryIndexCode = -32010
TransactionHistoryNotAvailableCode = -32011
ScanErrorCode = -32012
TransactionSignatureLengthMismatchCode = -32013
BlockStatusNotYetAvailableCode = -32014
UnsupportedTransactionVersionCode = -32015
MinContextSlotNotReachedCode = -32016
EpochRewardsPeriodActiveCode = -32017
SlotNotEpochBoundaryCode = -32018
)
19 changes: 10 additions & 9 deletions pkg/rpc/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ import (
)

type (
RPCError struct {
Message string `json:"message"`
Code int64 `json:"code"`
}

response[T any] struct {
jsonrpc string
Result T `json:"result"`
Error rpcError `json:"error"`
Error RPCError `json:"error"`
Id int `json:"id"`
}

Expand Down Expand Up @@ -91,6 +96,10 @@ type (
}
)

func (e *RPCError) Error() string {
return fmt.Sprintf("RPC Error (%d): %s", e.Code, e.Message)
}

func (hp *HostProduction) UnmarshalJSON(data []byte) error {
var arr []int64
if err := json.Unmarshal(data, &arr); err != nil {
Expand All @@ -104,11 +113,3 @@ func (hp *HostProduction) UnmarshalJSON(data []byte) error {
hp.BlocksProduced = arr[1]
return nil
}

func (r response[T]) getError() rpcError {
return r.Error
}

type HasRPCError interface {
getError() rpcError
}

0 comments on commit b2cde35

Please sign in to comment.