From 3e03ae9a387045dd52abb17dc480ab90222c1c6a Mon Sep 17 00:00:00 2001 From: Andy Pliszka Date: Sun, 12 Nov 2023 13:45:05 -0500 Subject: [PATCH] fix: handles SEI status (#388) --- internal/cosmos/comet_client.go | 108 ++++++++++------ internal/cosmos/comet_client_test.go | 185 +++++++++++++++++++-------- 2 files changed, 204 insertions(+), 89 deletions(-) diff --git a/internal/cosmos/comet_client.go b/internal/cosmos/comet_client.go index e46d6fe4..8b30b598 100644 --- a/internal/cosmos/comet_client.go +++ b/internal/cosmos/comet_client.go @@ -11,48 +11,68 @@ import ( "time" ) -// CometStatus is the response from the /status RPC endpoint. -type CometStatus struct { +type ValidatorInfo struct { + Address string `json:"address"` + PubKey struct { + Type string `json:"type"` + Value string `json:"value"` + } `json:"pub_key"` + VotingPower string `json:"voting_power"` +} + +type NodeInfo struct { + ProtocolVersion struct { + P2P string `json:"p2p"` + Block string `json:"block"` + App string `json:"app"` + } `json:"protocol_version"` + ID string `json:"id"` + ListenAddr string `json:"listen_addr"` + Network string `json:"network"` + Version string `json:"version"` + Channels string `json:"channels"` + Moniker string `json:"moniker"` + Other struct { + TxIndex string `json:"tx_index"` + RPCAddress string `json:"rpc_address"` + } `json:"other"` +} + +type SyncInfo struct { + LatestBlockHash string `json:"latest_block_hash"` + LatestAppHash string `json:"latest_app_hash"` + LatestBlockHeight string `json:"latest_block_height"` + LatestBlockTime time.Time `json:"latest_block_time"` + EarliestBlockHash string `json:"earliest_block_hash"` + EarliestAppHash string `json:"earliest_app_hash"` + EarliestBlockHeight string `json:"earliest_block_height"` + EarliestBlockTime time.Time `json:"earliest_block_time"` + CatchingUp bool `json:"catching_up"` +} + +// rpcCometStatusResponse is the union of possible server response +type rpcCometStatusResponse struct { JSONRPC string `json:"jsonrpc"` ID int `json:"id"` Result struct { - NodeInfo struct { - ProtocolVersion struct { - P2P string `json:"p2p"` - Block string `json:"block"` - App string `json:"app"` - } `json:"protocol_version"` - ID string `json:"id"` - ListenAddr string `json:"listen_addr"` - Network string `json:"network"` - Version string `json:"version"` - Channels string `json:"channels"` - Moniker string `json:"moniker"` - Other struct { - TxIndex string `json:"tx_index"` - RPCAddress string `json:"rpc_address"` - } `json:"other"` - } `json:"node_info"` - SyncInfo struct { - LatestBlockHash string `json:"latest_block_hash"` - LatestAppHash string `json:"latest_app_hash"` - LatestBlockHeight string `json:"latest_block_height"` - LatestBlockTime time.Time `json:"latest_block_time"` - EarliestBlockHash string `json:"earliest_block_hash"` - EarliestAppHash string `json:"earliest_app_hash"` - EarliestBlockHeight string `json:"earliest_block_height"` - EarliestBlockTime time.Time `json:"earliest_block_time"` - CatchingUp bool `json:"catching_up"` - } `json:"sync_info"` - ValidatorInfo struct { - Address string `json:"address"` - PubKey struct { - Type string `json:"type"` - Value string `json:"value"` - } `json:"pub_key"` - VotingPower string `json:"voting_power"` - } `json:"validator_info"` + NodeInfo *NodeInfo `json:"node_info"` + SyncInfo *SyncInfo `json:"sync_info"` + ValidatorInfo *ValidatorInfo `json:"validator_info"` } `json:"result"` + NodeInfo *NodeInfo `json:"node_info"` + SyncInfo *SyncInfo `json:"sync_info"` + ValidatorInfo *ValidatorInfo `json:"validator_info"` +} + +// CometStatus is the common response from the /status RPC endpoint. +type CometStatus struct { + JSONRPC string + ID int + Result struct { + NodeInfo NodeInfo + SyncInfo SyncInfo + ValidatorInfo ValidatorInfo + } } // LatestBlockHeight parses the latest block height string. If the string is malformed, returns 0. @@ -93,9 +113,19 @@ func (client *CometClient) Status(ctx context.Context, rpcHost string) (CometSta if resp.StatusCode != http.StatusOK { return status, errors.New(resp.Status) } - err = json.NewDecoder(resp.Body).Decode(&status) + var rpcStatusResponse rpcCometStatusResponse + err = json.NewDecoder(resp.Body).Decode(&rpcStatusResponse) if err != nil { return status, fmt.Errorf("malformed json: %w", err) } + if rpcStatusResponse.ValidatorInfo != nil { + status.Result.ValidatorInfo = *rpcStatusResponse.ValidatorInfo + status.Result.SyncInfo = *rpcStatusResponse.SyncInfo + status.Result.NodeInfo = *rpcStatusResponse.NodeInfo + } else { + status.Result.ValidatorInfo = *rpcStatusResponse.Result.ValidatorInfo + status.Result.SyncInfo = *rpcStatusResponse.Result.SyncInfo + status.Result.NodeInfo = *rpcStatusResponse.Result.NodeInfo + } return status, err } diff --git a/internal/cosmos/comet_client_test.go b/internal/cosmos/comet_client_test.go index 728141be..0261bbaf 100644 --- a/internal/cosmos/comet_client_test.go +++ b/internal/cosmos/comet_client_test.go @@ -34,61 +34,93 @@ func TestCometStatus_LatestBlockHeight(t *testing.T) { func TestCometClient_Status(t *testing.T) { t.Parallel() - t.Run("happy path", func(t *testing.T) { - // This context ensures we're not comparing instances of context.Background(). - cctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - client := NewCometClient(http.DefaultClient) - require.NotNil(t, client.httpDo) - - client.httpDo = func(req *http.Request) (*http.Response, error) { - require.Same(t, cctx, req.Context()) - require.Equal(t, "GET", req.Method) - require.Equal(t, "http://10.2.3.4:26657/status", req.URL.String()) - - return &http.Response{ - StatusCode: 200, - Body: io.NopCloser(strings.NewReader(statusResponseFixture)), - }, nil - } - - got, err := client.Status(cctx, "http://10.2.3.4:26657") - require.NoError(t, err) - require.Equal(t, "cosmoshub-testnet-fullnode-0", got.Result.NodeInfo.Moniker) - require.Equal(t, false, got.Result.SyncInfo.CatchingUp) - require.Equal(t, "13348657", got.Result.SyncInfo.LatestBlockHeight) - require.Equal(t, "9034670", got.Result.SyncInfo.EarliestBlockHeight) + t.Run("when there are no errors", func(t *testing.T) { + t.Run("given common response it returns the status", func(t *testing.T) { + // This context ensures we're not comparing instances of context.Background(). + cctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + client := NewCometClient(http.DefaultClient) + require.NotNil(t, client.httpDo) + + client.httpDo = func(req *http.Request) (*http.Response, error) { + require.Same(t, cctx, req.Context()) + require.Equal(t, "GET", req.Method) + require.Equal(t, "http://10.2.3.4:26657/status", req.URL.String()) + + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(statusResponseFixture)), + }, nil + } + + got, err := client.Status(cctx, "http://10.2.3.4:26657") + require.NoError(t, err) + require.Equal(t, "cosmoshub-testnet-fullnode-0", got.Result.NodeInfo.Moniker) + require.Equal(t, false, got.Result.SyncInfo.CatchingUp) + require.Equal(t, "13348657", got.Result.SyncInfo.LatestBlockHeight) + require.Equal(t, "9034670", got.Result.SyncInfo.EarliestBlockHeight) + }) + + t.Run("given SEI response it returns the status", func(t *testing.T) { + // This context ensures we're not comparing instances of context.Background(). + cctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + client := NewCometClient(http.DefaultClient) + require.NotNil(t, client.httpDo) + + client.httpDo = func(req *http.Request) (*http.Response, error) { + require.Same(t, cctx, req.Context()) + require.Equal(t, "GET", req.Method) + require.Equal(t, "http://10.2.3.4:26657/status", req.URL.String()) + + return &http.Response{ + StatusCode: 200, + Body: io.NopCloser(strings.NewReader(seiStatusResponseFixture)), + }, nil + } + + got, err := client.Status(cctx, "http://10.2.3.4:26657") + require.NoError(t, err) + require.Equal(t, "hello-sei-relayer", got.Result.NodeInfo.Moniker) + require.Equal(t, false, got.Result.SyncInfo.CatchingUp) + require.Equal(t, "37909189", got.Result.SyncInfo.LatestBlockHeight) + require.Equal(t, "33517999", got.Result.SyncInfo.EarliestBlockHeight) + }) }) - t.Run("non 200 response", func(t *testing.T) { - client := NewCometClient(http.DefaultClient) - client.httpDo = func(req *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: 500, - Status: "internal server error", - Body: io.NopCloser(strings.NewReader("")), - }, nil - } - - _, err := client.Status(context.Background(), "http://10.2.3.4:26657") - require.Error(t, err) - require.EqualError(t, err, "internal server error") - }) - - t.Run("http error", func(t *testing.T) { - client := NewCometClient(http.DefaultClient) - client.httpDo = func(req *http.Request) (*http.Response, error) { - return nil, errors.New("boom") - } - - _, err := client.Status(context.Background(), "http://10.2.3.4:26657") - require.Error(t, err) - require.EqualError(t, err, "boom") + t.Run("when there is an error", func(t *testing.T) { + t.Run("non 200 response", func(t *testing.T) { + client := NewCometClient(http.DefaultClient) + client.httpDo = func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 500, + Status: "internal server error", + Body: io.NopCloser(strings.NewReader("")), + }, nil + } + + _, err := client.Status(context.Background(), "http://10.2.3.4:26657") + require.Error(t, err) + require.EqualError(t, err, "internal server error") + }) + + t.Run("http error", func(t *testing.T) { + client := NewCometClient(http.DefaultClient) + client.httpDo = func(req *http.Request) (*http.Response, error) { + return nil, errors.New("boom") + } + + _, err := client.Status(context.Background(), "http://10.2.3.4:26657") + require.Error(t, err) + require.EqualError(t, err, "boom") + }) }) } -const statusResponseFixture = `{ +const statusResponseFixture = ` +{ "jsonrpc": "2.0", "id": -1, "result": { @@ -131,3 +163,56 @@ const statusResponseFixture = `{ } } ` +const seiStatusResponseFixture = ` +{ + "node_info": { + "protocol_version": { + "p2p": "8", + "block": "11", + "app": "0" + }, + "id": "a08b049a9d1909252f96973c3038db88b4b12883", + "listen_addr": "65.109.78.7:11956", + "network": "pacific-1", + "version": "0.35.0-unreleased", + "channels": "40202122233038606162630070717273", + "moniker": "hello-sei-relayer", + "other": { + "tx_index": "on", + "rpc_address": "tcp://0.0.0.0:11957" + } + }, + "application_info": { + "version": "9" + }, + "sync_info": { + "latest_block_hash": "24F4FEF7C5B704C99B3C36964ECCA855B9480E07F711B0A5FB2C348A4CAC5D48", + "latest_app_hash": "65271A3CA49CDC29FDC3A33974C508626F47CA178BCEA9421275E752824BC107", + "latest_block_height": "37909189", + "latest_block_time": "2023-11-09T17:36:20.235115543Z", + "earliest_block_hash": "0127F5D1CF7B53007180EB11052BB2B54D06C75EDE94E0F6686EA1988464B5B9", + "earliest_app_hash": "643CB8B56EA1A5F58D12396D26D8D89C7F9601FC199C6ACF76569BFCEE8C548C", + "earliest_block_height": "33517999", + "earliest_block_time": "2023-10-20T18:56:19.244399909Z", + "max_peer_block_height": "37909188", + "catching_up": false, + "total_synced_time": "0", + "remaining_time": "0", + "total_snapshots": "0", + "chunk_process_avg_time": "0", + "snapshot_height": "0", + "snapshot_chunks_count": "0", + "snapshot_chunks_total": "0", + "backfilled_blocks": "0", + "backfill_blocks_total": "0" + }, + "validator_info": { + "address": "FD7DCAA2E2C25770720E844E0FEA6D0004940B02", + "pub_key": { + "type": "tendermint/PubKeyEd25519", + "value": "7znc+z+uh4QFjLHTQPgSrXZvUpFRNMM9hjQCXNZYtyA=" + }, + "voting_power": "0" + } +} +`