diff --git a/README.md b/README.md index be3f5439..bef3670e 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ For more details, go [here](https://docs.multiversx.com/sdk-and-tools/proxy/). ### validator - `/v1.0/validator/statistics` (GET) --> returns the validator statistics data from an observer from any shard. Has a cache to avoid many requests +- `/v1.0/validator/auction` (GET) --> returns the validator auction list data from an observer from metachain. It doesn't have a cache mechanism, since there is already one in place at the node level ### block diff --git a/api/groups/baseValidatorGroup.go b/api/groups/baseValidatorGroup.go index cb1a6409..97017d8a 100644 --- a/api/groups/baseValidatorGroup.go +++ b/api/groups/baseValidatorGroup.go @@ -27,6 +27,7 @@ func NewValidatorGroup(facadeHandler data.FacadeHandler) (*validatorGroup, error baseRoutesHandlers := []*data.EndpointHandlerData{ {Path: "/statistics", Handler: vg.statistics, Method: http.MethodGet}, + {Path: "/auction", Handler: vg.auctionList, Method: http.MethodGet}, } vg.baseGroup.endpoints = baseRoutesHandlers @@ -43,3 +44,13 @@ func (group *validatorGroup) statistics(c *gin.Context) { shared.RespondWith(c, http.StatusOK, gin.H{"statistics": validatorStatistics}, "", data.ReturnCodeSuccess) } + +func (group *validatorGroup) auctionList(c *gin.Context) { + auctionList, err := group.facade.AuctionList() + if err != nil { + shared.RespondWith(c, http.StatusBadRequest, nil, err.Error(), data.ReturnCodeRequestError) + return + } + + shared.RespondWith(c, http.StatusOK, gin.H{"auctionList": auctionList}, "", data.ReturnCodeSuccess) +} diff --git a/api/groups/baseValidatorGroup_test.go b/api/groups/baseValidatorGroup_test.go index 6ac7cb6e..82850777 100644 --- a/api/groups/baseValidatorGroup_test.go +++ b/api/groups/baseValidatorGroup_test.go @@ -99,3 +99,73 @@ func TestValidatorStatistics_ShouldWork(t *testing.T) { assert.Equal(t, http.StatusOK, resp.Code) assert.Equal(t, response.Data.Statistics["statistics"], valStatsMap["statistics"]) } + +func TestValidatorGroup_GetAuctionList(t *testing.T) { + t.Parallel() + + t.Run("should work", func(t *testing.T) { + t.Parallel() + + auctionList := []*data.AuctionListValidatorAPIResponse{ + { + Owner: "owner", + NumStakedNodes: 1, + TotalTopUp: "100", + TopUpPerNode: "100", + QualifiedTopUp: "50", + }, + } + facade := &mock.FacadeStub{ + AuctionListHandler: func() ([]*data.AuctionListValidatorAPIResponse, error) { + return auctionList, nil + }, + } + + validatorGroup, _ := groups.NewValidatorGroup(facade) + ws := startProxyServer(validatorGroup, validatorPath) + + req, _ := http.NewRequest("GET", "/validator/auction", nil) + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + + response := data.AuctionListAPIResponse{} + loadResponse(resp.Body, &response) + + require.Equal(t, http.StatusOK, resp.Code) + require.Equal(t, data.AuctionListAPIResponse{ + Data: data.AuctionListResponse{ + AuctionListValidators: auctionList, + }, + Error: "", + Code: string(data.ReturnCodeSuccess), + }, response) + }) + + t.Run("cannot get auction list from facade, should return error", func(t *testing.T) { + t.Parallel() + + errFacade := errors.New("error getting auction list") + facade := &mock.FacadeStub{ + AuctionListHandler: func() ([]*data.AuctionListValidatorAPIResponse, error) { + return nil, errFacade + }, + } + + validatorGroup, _ := groups.NewValidatorGroup(facade) + ws := startProxyServer(validatorGroup, validatorPath) + + req, _ := http.NewRequest("GET", "/validator/auction", nil) + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + + response := data.GenericAPIResponse{} + loadResponse(resp.Body, &response) + + require.Equal(t, http.StatusBadRequest, resp.Code) + require.Equal(t, data.GenericAPIResponse{ + Data: nil, + Error: errFacade.Error(), + Code: data.ReturnCodeRequestError, + }, response) + }) +} diff --git a/api/groups/interface.go b/api/groups/interface.go index 75951203..d58a6906 100644 --- a/api/groups/interface.go +++ b/api/groups/interface.go @@ -121,6 +121,7 @@ type ProofFacadeHandler interface { // ValidatorFacadeHandler interface defines methods that can be used from the facade type ValidatorFacadeHandler interface { ValidatorStatistics() (map[string]*data.ValidatorApiResponse, error) + AuctionList() ([]*data.AuctionListValidatorAPIResponse, error) } // VmValuesFacadeHandler interface defines methods that can be used from the facade diff --git a/api/mock/facadeStub.go b/api/mock/facadeStub.go index e63e0b57..71e38b2d 100644 --- a/api/mock/facadeStub.go +++ b/api/mock/facadeStub.go @@ -37,6 +37,7 @@ type FacadeStub struct { ExecuteSCQueryHandler func(query *data.SCQuery) (*vm.VMOutputApi, data.BlockInfo, error) GetHeartbeatDataHandler func() (*data.HeartbeatResponse, error) ValidatorStatisticsHandler func() (map[string]*data.ValidatorApiResponse, error) + AuctionListHandler func() ([]*data.AuctionListValidatorAPIResponse, error) TransactionCostRequestHandler func(tx *data.Transaction) (*data.TxCostResponseData, error) GetTransactionStatusHandler func(txHash string, sender string) (string, error) GetProcessedTransactionStatusHandler func(txHash string) (string, error) @@ -250,7 +251,20 @@ func (f *FacadeStub) GetESDTSupply(token string) (*data.ESDTSupplyResponse, erro // ValidatorStatistics - func (f *FacadeStub) ValidatorStatistics() (map[string]*data.ValidatorApiResponse, error) { - return f.ValidatorStatisticsHandler() + if f.ValidatorStatisticsHandler != nil { + return f.ValidatorStatisticsHandler() + } + + return nil, nil +} + +// AuctionList - +func (f *FacadeStub) AuctionList() ([]*data.AuctionListValidatorAPIResponse, error) { + if f.AuctionListHandler != nil { + return f.AuctionListHandler() + } + + return nil, nil } // GetAccount - diff --git a/cmd/proxy/config/apiConfig/v1_0.toml b/cmd/proxy/config/apiConfig/v1_0.toml index 1d198b8f..1f5d2aed 100644 --- a/cmd/proxy/config/apiConfig/v1_0.toml +++ b/cmd/proxy/config/apiConfig/v1_0.toml @@ -78,7 +78,8 @@ Routes = [ [APIPackages.validator] Routes = [ - { Name = "/statistics", Open = true, Secured = false, RateLimit = 0 } + { Name = "/statistics", Open = true, Secured = false, RateLimit = 0 }, + { Name = "/auction", Open = true, Secured = false, RateLimit = 0 } ] [APIPackages.vm-values] diff --git a/cmd/proxy/config/apiConfig/v_next.toml b/cmd/proxy/config/apiConfig/v_next.toml index c561c186..1bdf4e85 100644 --- a/cmd/proxy/config/apiConfig/v_next.toml +++ b/cmd/proxy/config/apiConfig/v_next.toml @@ -78,7 +78,8 @@ Routes = [ [APIPackages.validator] Routes = [ - { Name = "/statistics", Open = true, Secured = false, RateLimit = 0 } + { Name = "/statistics", Open = true, Secured = false, RateLimit = 0 }, + { Name = "/auction", Open = true, Secured = false, RateLimit = 0 } ] [APIPackages.vm-values] diff --git a/data/auctionList.go b/data/auctionList.go new file mode 100644 index 00000000..325b4a92 --- /dev/null +++ b/data/auctionList.go @@ -0,0 +1,29 @@ +package data + +// AuctionNode holds data needed for a node in auction to respond to API calls +type AuctionNode struct { + BlsKey string `json:"blsKey"` + Qualified bool `json:"qualified"` +} + +// AuctionListValidatorAPIResponse holds the data needed for an auction node validator for responding to API calls +type AuctionListValidatorAPIResponse struct { + Owner string `json:"owner"` + NumStakedNodes int64 `json:"numStakedNodes"` + TotalTopUp string `json:"totalTopUp"` + TopUpPerNode string `json:"topUpPerNode"` + QualifiedTopUp string `json:"qualifiedTopUp"` + Nodes []*AuctionNode `json:"nodes"` +} + +// AuctionListResponse respects the format the auction list api response received from the observers +type AuctionListResponse struct { + AuctionListValidators []*AuctionListValidatorAPIResponse `json:"auctionList"` +} + +// AuctionListAPIResponse respects the format the auction list received from the observers +type AuctionListAPIResponse struct { + Data AuctionListResponse `json:"data"` + Error string `json:"error"` + Code string `json:"code"` +} diff --git a/facade/baseFacade.go b/facade/baseFacade.go index a9df9552..49a5d3b9 100644 --- a/facade/baseFacade.go +++ b/facade/baseFacade.go @@ -411,6 +411,16 @@ func (pf *ProxyFacade) ValidatorStatistics() (map[string]*data.ValidatorApiRespo return valStats.Statistics, nil } +// AuctionList will return the auction list +func (epf *ProxyFacade) AuctionList() ([]*data.AuctionListValidatorAPIResponse, error) { + auctionList, err := epf.valStatsProc.GetAuctionList() + if err != nil { + return nil, err + } + + return auctionList.AuctionListValidators, nil +} + // GetAtlasBlockByShardIDAndNonce returns block by shardID and nonce in a BlockAtlas-friendly-format func (pf *ProxyFacade) GetAtlasBlockByShardIDAndNonce(shardID uint32, nonce uint64) (data.AtlasBlock, error) { return pf.blockProc.GetAtlasBlockByShardIDAndNonce(shardID, nonce) diff --git a/facade/interface.go b/facade/interface.go index 4e6dd340..6dda0787 100644 --- a/facade/interface.go +++ b/facade/interface.go @@ -76,6 +76,7 @@ type NodeGroupProcessor interface { // ValidatorStatisticsProcessor defines what a validator statistics processor should do type ValidatorStatisticsProcessor interface { GetValidatorStatistics() (*data.ValidatorStatisticsResponse, error) + GetAuctionList() (*data.AuctionListResponse, error) } // ESDTSupplyProcessor defines what an esdt supply processor should do diff --git a/facade/mock/accountProccessorStub.go b/facade/mock/accountProccessorStub.go index 9ed6019e..e50e728c 100644 --- a/facade/mock/accountProccessorStub.go +++ b/facade/mock/accountProccessorStub.go @@ -112,3 +112,8 @@ func (aps *AccountProcessorStub) IsDataTrieMigrated(address string, options comm return &data.GenericAPIResponse{}, nil } + +// AuctionList - +func (aps *AccountProcessorStub) AuctionList() ([]*data.AuctionListValidatorAPIResponse, error) { + return nil, nil +} diff --git a/facade/mock/validatorStatisticsProcessorStub.go b/facade/mock/validatorStatisticsProcessorStub.go index 7d65c22b..9a0b8e52 100644 --- a/facade/mock/validatorStatisticsProcessorStub.go +++ b/facade/mock/validatorStatisticsProcessorStub.go @@ -11,3 +11,8 @@ type ValidatorStatisticsProcessorStub struct { func (v *ValidatorStatisticsProcessorStub) GetValidatorStatistics() (*data.ValidatorStatisticsResponse, error) { return v.GetValidatorStatisticsCalled() } + +// GetAuctionList - +func (v *ValidatorStatisticsProcessorStub) GetAuctionList() (*data.AuctionListResponse, error) { + return nil, nil +} diff --git a/process/errors.go b/process/errors.go index 65b03271..134e7bf9 100644 --- a/process/errors.go +++ b/process/errors.go @@ -36,7 +36,10 @@ var ErrNilValidatorStatisticsCacher = errors.New("nil validator statistics cache var ErrNilEconomicMetricsCacher = errors.New("nil economic metrics cacher") // ErrValidatorStatisticsNotAvailable signals that the validator statistics data is not found -var ErrValidatorStatisticsNotAvailable = errors.New("validator statistics data not found at any observer") +var ErrValidatorStatisticsNotAvailable = errors.New("validator statistics data not found on any observer") + +// ErrAuctionListNotAvailable signals that the auction list data is not found +var ErrAuctionListNotAvailable = errors.New("auction list data not found on any observer") // ErrInvalidCacheValidityDuration signals that the given validity duration for cache data is invalid var ErrInvalidCacheValidityDuration = errors.New("invalid cache validity duration") diff --git a/process/validatorAuctionProcessor.go b/process/validatorAuctionProcessor.go new file mode 100644 index 00000000..6acb588d --- /dev/null +++ b/process/validatorAuctionProcessor.go @@ -0,0 +1,27 @@ +package process + +import ( + "github.com/multiversx/mx-chain-core-go/core" + "github.com/multiversx/mx-chain-proxy-go/data" +) + +// GetAuctionList returns the auction list from a metachain observer node +func (vsp *ValidatorStatisticsProcessor) GetAuctionList() (*data.AuctionListResponse, error) { + observers, errFetchObs := vsp.proc.GetObservers(core.MetachainShardId, data.AvailabilityRecent) + if errFetchObs != nil { + return nil, errFetchObs + } + + var valStatsResponse data.AuctionListAPIResponse + for _, observer := range observers { + _, err := vsp.proc.CallGetRestEndPoint(observer.Address, auctionListPath, &valStatsResponse) + if err == nil { + log.Info("auction list fetched from API", "observer", observer.Address) + return &valStatsResponse.Data, nil + } + + log.Error("getAuctionListFromApi", "observer", observer.Address, "error", err) + } + + return nil, ErrAuctionListNotAvailable +} diff --git a/process/validatorAuctionProcessor_test.go b/process/validatorAuctionProcessor_test.go new file mode 100644 index 00000000..cd86c90a --- /dev/null +++ b/process/validatorAuctionProcessor_test.go @@ -0,0 +1,118 @@ +package process + +import ( + "encoding/json" + "errors" + "sync/atomic" + "testing" + "time" + + "github.com/multiversx/mx-chain-core-go/core" + "github.com/multiversx/mx-chain-proxy-go/data" + "github.com/multiversx/mx-chain-proxy-go/process/mock" + "github.com/stretchr/testify/require" +) + +func TestValidatorStatisticsProcessor_GetAuctionList(t *testing.T) { + t.Parallel() + + t.Run("should work", func(t *testing.T) { + t.Parallel() + + node := &data.NodeData{ + Address: "addr", + ShardId: core.MetachainShardId, + } + expectedResp := &data.AuctionListAPIResponse{ + Data: data.AuctionListResponse{ + AuctionListValidators: []*data.AuctionListValidatorAPIResponse{ + { + Owner: "owner", + NumStakedNodes: 4, + TotalTopUp: "100", + TopUpPerNode: "50", + QualifiedTopUp: "50", + }, + }, + }, + Error: "", + Code: "ok", + } + expectedRespMarshalled, err := json.Marshal(expectedResp) + require.Nil(t, err) + + processor := &mock.ProcessorStub{ + GetObserversCalled: func(shardId uint32, _ data.ObserverDataAvailabilityType) ([]*data.NodeData, error) { + require.Equal(t, core.MetachainShardId, shardId) + + return []*data.NodeData{node}, nil + }, + CallGetRestEndPointCalled: func(address string, path string, value interface{}) (int, error) { + require.Equal(t, node.Address, address) + require.Equal(t, auctionListPath, path) + + err = json.Unmarshal(expectedRespMarshalled, value) + require.Nil(t, err) + return 0, nil + }, + } + vsp, _ := NewValidatorStatisticsProcessor(processor, &mock.ValStatsCacherMock{}, time.Second) + resp, err := vsp.GetAuctionList() + require.Nil(t, err) + require.Equal(t, expectedResp.Data, *resp) + }) + + t.Run("get observers failed, should return error", func(t *testing.T) { + t.Parallel() + + errGetObservers := errors.New("err getting observers") + callGetRestEndPointCalledCt := int32(0) + + processor := &mock.ProcessorStub{ + GetObserversCalled: func(shardId uint32, _ data.ObserverDataAvailabilityType) ([]*data.NodeData, error) { + require.Equal(t, core.MetachainShardId, shardId) + return nil, errGetObservers + }, + CallGetRestEndPointCalled: func(address string, path string, value interface{}) (int, error) { + atomic.AddInt32(&callGetRestEndPointCalledCt, 1) + + return 0, nil + }, + } + vsp, _ := NewValidatorStatisticsProcessor(processor, &mock.ValStatsCacherMock{}, time.Second) + + resp, err := vsp.GetAuctionList() + require.Equal(t, errGetObservers, err) + require.Nil(t, resp) + require.Equal(t, int32(0), callGetRestEndPointCalledCt) + }) + + t.Run("could not get auction list from observer, should return error", func(t *testing.T) { + t.Parallel() + + node := &data.NodeData{ + Address: "addr", + ShardId: core.MetachainShardId, + } + + errCallEndpoint := errors.New("error call endpoint") + processor := &mock.ProcessorStub{ + GetObserversCalled: func(shardId uint32, _ data.ObserverDataAvailabilityType) ([]*data.NodeData, error) { + require.Equal(t, core.MetachainShardId, shardId) + + return []*data.NodeData{node}, nil + }, + CallGetRestEndPointCalled: func(address string, path string, value interface{}) (int, error) { + require.Equal(t, node.Address, address) + require.Equal(t, auctionListPath, path) + + return 0, errCallEndpoint + }, + } + vsp, _ := NewValidatorStatisticsProcessor(processor, &mock.ValStatsCacherMock{}, time.Second) + + resp, err := vsp.GetAuctionList() + require.Equal(t, ErrAuctionListNotAvailable, err) + require.Nil(t, resp) + }) +} diff --git a/process/validatorStatisticsProcessor.go b/process/validatorStatisticsProcessor.go index b52f3dc0..9416405c 100644 --- a/process/validatorStatisticsProcessor.go +++ b/process/validatorStatisticsProcessor.go @@ -9,8 +9,10 @@ import ( "github.com/multiversx/mx-chain-proxy-go/data" ) -// ValidatorStatisticsPath represents the path where an observer exposes his validator statistics data -const ValidatorStatisticsPath = "/validator/statistics" +const ( + validatorStatisticsPath = "/validator/statistics" + auctionListPath = "/validator/auction" +) // ValidatorStatisticsProcessor is able to process validator statistics data requests type ValidatorStatisticsProcessor struct { @@ -65,7 +67,7 @@ func (vsp *ValidatorStatisticsProcessor) getValidatorStatisticsFromApi() (*data. var valStatsResponse data.ValidatorStatisticsApiResponse var err error for _, observer := range observers { - _, err = vsp.proc.CallGetRestEndPoint(observer.Address, ValidatorStatisticsPath, &valStatsResponse) + _, err = vsp.proc.CallGetRestEndPoint(observer.Address, validatorStatisticsPath, &valStatsResponse) if err == nil { log.Info("validator statistics fetched from API", "observer", observer.Address) return &valStatsResponse.Data, nil