diff --git a/api/indexer/client.go b/api/indexer/client.go index c8ca2b9347..3cc139659d 100644 --- a/api/indexer/client.go +++ b/api/indexer/client.go @@ -32,7 +32,7 @@ type Client struct { // Use a separate type that only decodes the block bytes because we cannot decode block JSON // due to Actions/Auth interfaces included in the block's transactions. type GetBlockClientResponse struct { - BlockBytes codec.Bytes `json:"blockBytes"` + BlockBytes codec.Bytes `json:"block"` } func (c *Client) GetBlock(ctx context.Context, blkID ids.ID, parser chain.Parser) (*chain.ExecutedBlock, error) { @@ -40,7 +40,7 @@ func (c *Client) GetBlock(ctx context.Context, blkID ids.ID, parser chain.Parser err := c.requester.SendRequest( ctx, "getBlock", - &GetBlockRequest{BlockID: blkID}, + &GetBlockRequest{BlockID: blkID, Encoding: Hex}, &resp, ) if err != nil { @@ -54,7 +54,7 @@ func (c *Client) GetBlockByHeight(ctx context.Context, height uint64, parser cha err := c.requester.SendRequest( ctx, "getBlockByHeight", - &GetBlockByHeightRequest{Height: height}, + &GetBlockByHeightRequest{Height: height, Encoding: Hex}, &resp, ) if err != nil { @@ -68,7 +68,7 @@ func (c *Client) GetLatestBlock(ctx context.Context, parser chain.Parser) (*chai err := c.requester.SendRequest( ctx, "getLatestBlock", - nil, + &GetLatestBlockRequest{Encoding: Hex}, &resp, ) if err != nil { diff --git a/api/indexer/server.go b/api/indexer/server.go index e8cec42f03..2d1f9e2d7f 100644 --- a/api/indexer/server.go +++ b/api/indexer/server.go @@ -19,7 +19,9 @@ import ( const Endpoint = "/indexer" var ( - ErrTxNotFound = errors.New("tx not found") + ErrTxNotFound = errors.New("tx not found") + ErrInvalidEncodingParameter = errors.New("invalid encoding parameter") + ErrUnsupportedEncoding = errors.New("unsupported encoding") _ api.HandlerFactory[api.VM] = (*apiFactory)(nil) ) @@ -46,25 +48,36 @@ func (f *apiFactory) New(vm api.VM) (api.Handler, error) { } type GetBlockRequest struct { - BlockID ids.ID `json:"blockID"` + BlockID ids.ID `json:"blockID"` + Encoding Encoding `json:"encoding"` } type GetBlockByHeightRequest struct { - Height uint64 `json:"height"` + Height uint64 `json:"height"` + Encoding Encoding `json:"encoding"` +} + +type GetLatestBlockRequest struct { + Encoding Encoding `json:"encoding"` } type GetBlockResponse struct { - Block *chain.ExecutedBlock `json:"block"` - BlockBytes codec.Bytes `json:"blockBytes"` + Block any `json:"block"` } -func (g *GetBlockResponse) setResponse(block *chain.ExecutedBlock) error { - g.Block = block - blockBytes, err := block.Marshal() - if err != nil { - return err +func (g *GetBlockResponse) setResponse(block *chain.ExecutedBlock, encoding Encoding) error { + switch encoding { + case JSON: + g.Block = block + case Hex: + blockBytes, err := block.Marshal() + if err != nil { + return err + } + g.Block = codec.Bytes(blockBytes) + default: + return ErrUnsupportedEncoding } - g.BlockBytes = blockBytes return nil } @@ -72,33 +85,46 @@ func (s *Server) GetBlock(req *http.Request, args *GetBlockRequest, reply *GetBl _, span := s.tracer.Start(req.Context(), "Indexer.GetBlock") defer span.End() + if err := args.Encoding.Validate(); err != nil { + return err + } + block, err := s.indexer.GetBlock(args.BlockID) if err != nil { return err } - return reply.setResponse(block) + + return reply.setResponse(block, args.Encoding) } func (s *Server) GetBlockByHeight(req *http.Request, args *GetBlockByHeightRequest, reply *GetBlockResponse) error { _, span := s.tracer.Start(req.Context(), "Indexer.GetBlockByHeight") defer span.End() + if err := args.Encoding.Validate(); err != nil { + return err + } + block, err := s.indexer.GetBlockByHeight(args.Height) if err != nil { return err } - return reply.setResponse(block) + return reply.setResponse(block, args.Encoding) } -func (s *Server) GetLatestBlock(req *http.Request, _ *struct{}, reply *GetBlockResponse) error { +func (s *Server) GetLatestBlock(req *http.Request, args *GetLatestBlockRequest, reply *GetBlockResponse) error { _, span := s.tracer.Start(req.Context(), "Indexer.GetLatestBlock") defer span.End() + if err := args.Encoding.Validate(); err != nil { + return err + } + block, err := s.indexer.GetLatestBlock() if err != nil { return err } - return reply.setResponse(block) + return reply.setResponse(block, args.Encoding) } type GetTxRequest struct { @@ -116,7 +142,7 @@ type GetTxResponse struct { type Server struct { tracer trace.Tracer - indexer *Indexer + indexer AbstractIndexer } func (s *Server) GetTx(req *http.Request, args *GetTxRequest, reply *GetTxResponse) error { @@ -143,3 +169,27 @@ func (s *Server) GetTx(req *http.Request, args *GetTxRequest, reply *GetTxRespon reply.ErrorStr = errorStr return nil } + +type Encoding string + +var ( + JSON Encoding = "json" + Hex Encoding = "hex" +) + +func (e *Encoding) Validate() error { + if *e == "" { + *e = JSON + } + if *e != JSON && *e != Hex { + return ErrInvalidEncodingParameter + } + return nil +} + +type AbstractIndexer interface { + GetBlock(blkID ids.ID) (*chain.ExecutedBlock, error) + GetBlockByHeight(height uint64) (*chain.ExecutedBlock, error) + GetLatestBlock() (*chain.ExecutedBlock, error) + GetTransaction(txID ids.ID) (bool, int64, bool, fees.Dimensions, uint64, [][]byte, string, error) +} diff --git a/api/indexer/server_test.go b/api/indexer/server_test.go new file mode 100644 index 0000000000..ef7890ef20 --- /dev/null +++ b/api/indexer/server_test.go @@ -0,0 +1,116 @@ +// Copyright (C) 2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package indexer + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/trace" + "github.com/stretchr/testify/require" + + "github.com/ava-labs/hypersdk/api" + "github.com/ava-labs/hypersdk/chain" + "github.com/ava-labs/hypersdk/chain/chaintest" + "github.com/ava-labs/hypersdk/fees" +) + +func TestEncodingValidate(t *testing.T) { + tests := []struct { + input Encoding + exp Encoding + expErr error + }{ + { + input: JSON, + exp: JSON, + }, + { + input: Hex, + exp: Hex, + }, + { + input: Encoding(""), + exp: JSON, + }, + { + input: Encoding("another"), + expErr: ErrInvalidEncodingParameter, + }, + } + + for _, test := range tests { + err := test.input.Validate() + if test.expErr != nil { + require.EqualError(t, ErrInvalidEncodingParameter, err.Error()) + } else { + require.Equal(t, test.exp, test.input) + } + } +} + +func TestBlockRequests(t *testing.T) { + require := require.New(t) + blocks := chaintest.GenerateEmptyExecutedBlocks(require, ids.GenerateTestID(), 0, 0, 0, 1) + server, client := newIndexerHTTPServerAndClient(require, blocks[0]) + defer server.Close() + + res, err := client.GetBlock(context.Background(), blocks[0].BlockID, chaintest.NewEmptyParser()) + require.NoError(err) + require.Equal(blocks[0].BlockID, res.BlockID) + + res, err = client.GetBlockByHeight(context.Background(), blocks[0].Block.Hght, chaintest.NewEmptyParser()) + require.NoError(err) + require.Equal(blocks[0].BlockID, res.BlockID) + + res, err = client.GetLatestBlock(context.Background(), chaintest.NewEmptyParser()) + require.NoError(err) + require.Equal(blocks[0].BlockID, res.BlockID) +} + +func newIndexerHTTPServerAndClient(require *require.Assertions, block *chain.ExecutedBlock) (*httptest.Server, *Client) { + rpcServer := &Server{ + tracer: trace.Noop, + indexer: &mockIndexer{ + execuredBlock: block, + }, + } + handler, err := api.NewJSONRPCHandler(Name, rpcServer) + require.NoError(err) + mux := http.NewServeMux() + mux.Handle(Endpoint, handler) + testServer := httptest.NewServer(mux) + client := NewClient(testServer.URL) + return testServer, client +} + +type mockIndexer struct { + execuredBlock *chain.ExecutedBlock +} + +func (m *mockIndexer) GetBlock(blockID ids.ID) (*chain.ExecutedBlock, error) { + if blockID == m.execuredBlock.BlockID { + return m.execuredBlock, nil + } + return nil, errors.New("not found") +} + +func (m *mockIndexer) GetBlockByHeight(height uint64) (*chain.ExecutedBlock, error) { + if height == m.execuredBlock.Block.Hght { + return m.execuredBlock, nil + } + return nil, errors.New("not found") +} + +func (m *mockIndexer) GetLatestBlock() (*chain.ExecutedBlock, error) { + return m.execuredBlock, nil +} + +func (*mockIndexer) GetTransaction(_ ids.ID) (bool, int64, bool, fees.Dimensions, uint64, [][]byte, string, error) { + panic("unimplemented") +}