diff --git a/cmd/solana_exporter/slots.go b/cmd/solana_exporter/slots.go index 455303c..6466158 100644 --- a/cmd/solana_exporter/slots.go +++ b/cmd/solana_exporter/slots.go @@ -2,8 +2,10 @@ package main import ( "context" + "errors" "fmt" "slices" + "strings" "time" "github.com/asymmetric-research/solana_exporter/pkg/rpc" @@ -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) @@ -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 } @@ -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 } diff --git a/pkg/rpc/client.go b/pkg/rpc/client.go index b69417b..fb9fe64 100644 --- a/pkg/rpc/client.go +++ b/pkg/rpc/client.go @@ -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"` @@ -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) @@ -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) } @@ -130,15 +127,14 @@ 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 } @@ -146,7 +142,7 @@ func (c *Client) getResponse(ctx context.Context, method string, params []any, r // 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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, @@ -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 diff --git a/pkg/rpc/errors.go b/pkg/rpc/errors.go new file mode 100644 index 0000000..5e3fb9f --- /dev/null +++ b/pkg/rpc/errors.go @@ -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 +) diff --git a/pkg/rpc/responses.go b/pkg/rpc/responses.go index c88f682..f6cc3dd 100644 --- a/pkg/rpc/responses.go +++ b/pkg/rpc/responses.go @@ -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"` } @@ -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 { @@ -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 -}