diff --git a/README.md b/README.md index b50a0979..bff7426a 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ For more details, go [here](https://docs.multiversx.com/sdk-and-tools/proxy/). - `/v1.0/network/delegated-info` (GET) --> returns the list of delegated values - `/v1.0/network/enable-epochs` (GET) --> returns the activation epochs metric - `/v1.0/network/enable-epochs-v2` (GET) --> returns the newer version of activation epochs +- `/v1.0/network/enable-rounds` (GET) --> returns the activation rounds metric ### node - `/v1.0/node/heartbeatstatus` (GET) --> returns the heartbeat data from an observer from any shard. Has a cache to avoid many requests diff --git a/api/groups/baseNetworkGroup.go b/api/groups/baseNetworkGroup.go index d165c581..f1d62a16 100644 --- a/api/groups/baseNetworkGroup.go +++ b/api/groups/baseNetworkGroup.go @@ -38,6 +38,7 @@ func NewNetworkGroup(facadeHandler data.FacadeHandler) (*networkGroup, error) { {Path: "/esdt/supply/:token", Handler: ng.getESDTSupply, Method: http.MethodGet}, {Path: "/enable-epochs", Handler: ng.getEnableEpochs, Method: http.MethodGet}, {Path: "/enable-epochs-v2", Handler: ng.getEnableEpochsV2, Method: http.MethodGet}, + {Path: "/enable-rounds", Handler: ng.getEnableRounds, Method: http.MethodGet}, {Path: "/direct-staked-info", Handler: ng.getDirectStakedInfo, Method: http.MethodGet}, {Path: "/delegated-info", Handler: ng.getDelegatedInfo, Method: http.MethodGet}, {Path: "/ratings", Handler: ng.getRatingsConfig, Method: http.MethodGet}, @@ -155,6 +156,16 @@ func (group *networkGroup) getEnableEpochsV2(c *gin.Context) { c.JSON(http.StatusOK, enableEpochsMetrics) } +func (group *networkGroup) getEnableRounds(c *gin.Context) { + enableRoundsMetrics, err := group.facade.GetEnableRoundsMetrics() + if err != nil { + shared.RespondWith(c, http.StatusInternalServerError, nil, err.Error(), data.ReturnCodeInternalError) + return + } + + c.JSON(http.StatusOK, enableRoundsMetrics) +} + func (group *networkGroup) getESDTSupply(c *gin.Context) { tokenIdentifier := c.Param("token") if tokenIdentifier == "" { diff --git a/api/groups/baseNetworkGroup_test.go b/api/groups/baseNetworkGroup_test.go index 04b20580..1e172c4f 100644 --- a/api/groups/baseNetworkGroup_test.go +++ b/api/groups/baseNetworkGroup_test.go @@ -527,6 +527,81 @@ func TestGetEnableEpochsMetricsV2_OkRequestShouldWork(t *testing.T) { assert.Equal(t, value, res) } +func TestGetEnableRoundsMetrics_FacadeErrShouldErr(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("expected err") + facade := &mock.FacadeStub{ + GetEnableRoundsMetricsHandler: func() (*data.GenericAPIResponse, error) { + return nil, expectedErr + }, + } + networkGroup, err := groups.NewNetworkGroup(facade) + require.NoError(t, err) + ws := startProxyServer(networkGroup, networkPath) + + req, _ := http.NewRequest("GET", "/network/enable-rounds", nil) + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + assert.Equal(t, http.StatusInternalServerError, resp.Code) + + var result metricsResponse + loadResponse(resp.Body, &result) + + assert.Equal(t, expectedErr.Error(), result.Error) +} + +func TestGetEnableRoundsMetrics_BadRequestShouldErr(t *testing.T) { + t.Parallel() + + facade := &mock.FacadeStub{ + GetEnableRoundsMetricsHandler: func() (*data.GenericAPIResponse, error) { + return nil, errors.New("bad request") + }, + } + networkGroup, err := groups.NewNetworkGroup(facade) + require.NoError(t, err) + ws := startProxyServer(networkGroup, networkPath) + + req, _ := http.NewRequest("GET", "/network/enable-rounds", nil) + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + + assert.Equal(t, http.StatusInternalServerError, resp.Code) +} + +func TestGetEnableRoundsMetrics_OkRequestShouldWork(t *testing.T) { + t.Parallel() + + key := "SupernovaEnableRound" + value := float64(100) + facade := &mock.FacadeStub{ + GetEnableRoundsMetricsHandler: func() (*data.GenericAPIResponse, error) { + return &data.GenericAPIResponse{ + Data: map[string]interface{}{ + key: value, + }, + Error: "", + }, nil + }, + } + networkGroup, err := groups.NewNetworkGroup(facade) + require.NoError(t, err) + ws := startProxyServer(networkGroup, networkPath) + + req, _ := http.NewRequest("GET", "/network/enable-rounds", nil) + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + assert.Equal(t, http.StatusOK, resp.Code) + + var result metricsResponse + loadResponse(resp.Body, &result) + + res, ok := result.Data[key] + assert.True(t, ok) + assert.Equal(t, value, res) +} + func TestGetRatingsConfig_ShouldFail(t *testing.T) { t.Parallel() diff --git a/api/groups/interface.go b/api/groups/interface.go index f7a2ae11..6d87751f 100644 --- a/api/groups/interface.go +++ b/api/groups/interface.go @@ -66,6 +66,7 @@ type NetworkFacadeHandler interface { GetDelegatedInfo() (*data.GenericAPIResponse, error) GetEnableEpochsMetrics() (*data.GenericAPIResponse, error) GetEnableEpochsMetricsV2() (*data.GenericAPIResponse, error) + GetEnableRoundsMetrics() (*data.GenericAPIResponse, error) GetESDTSupply(token string) (*data.ESDTSupplyResponse, error) GetRatingsConfig() (*data.GenericAPIResponse, error) GetGenesisNodesPubKeys() (*data.GenericAPIResponse, error) diff --git a/api/mock/facadeStub.go b/api/mock/facadeStub.go index b8219c04..c044a63b 100644 --- a/api/mock/facadeStub.go +++ b/api/mock/facadeStub.go @@ -46,6 +46,7 @@ type FacadeStub struct { GetAllIssuedESDTsHandler func(tokenType string) (*data.GenericAPIResponse, error) GetEnableEpochsMetricsHandler func() (*data.GenericAPIResponse, error) GetEnableEpochsMetricsV2Handler func() (*data.GenericAPIResponse, error) + GetEnableRoundsMetricsHandler func() (*data.GenericAPIResponse, error) GetEconomicsDataMetricsHandler func() (*data.GenericAPIResponse, error) GetDirectStakedInfoCalled func() (*data.GenericAPIResponse, error) GetDelegatedInfoCalled func() (*data.GenericAPIResponse, error) @@ -241,6 +242,11 @@ func (f *FacadeStub) GetEnableEpochsMetricsV2() (*data.GenericAPIResponse, error return f.GetEnableEpochsMetricsV2Handler() } +// GetEnableRoundsMetrics - +func (f *FacadeStub) GetEnableRoundsMetrics() (*data.GenericAPIResponse, error) { + return f.GetEnableRoundsMetricsHandler() +} + // GetRatingsConfig - func (f *FacadeStub) GetRatingsConfig() (*data.GenericAPIResponse, error) { return f.GetRatingsConfigCalled() diff --git a/cmd/proxy/config/apiConfig/v1_0.toml b/cmd/proxy/config/apiConfig/v1_0.toml index 1e7e83b4..a41ab7c0 100644 --- a/cmd/proxy/config/apiConfig/v1_0.toml +++ b/cmd/proxy/config/apiConfig/v1_0.toml @@ -70,6 +70,7 @@ Routes = [ { Name = "/delegated-info", Open = true, Secured = true, RateLimit = 0 }, { Name = "/enable-epochs", Open = true, Secured = false, RateLimit = 0 }, { Name = "/enable-epochs-v2", Open = true, Secured = false, RateLimit = 0 }, + { Name = "/enable-rounds", Open = true, Secured = false, RateLimit = 0 }, { Name = "/ratings", Open = true, Secured = false, RateLimit = 0 }, { Name = "/genesis-nodes", Open = true, Secured = false, RateLimit = 0 }, { Name = "/gas-configs", Open = true, Secured = false, RateLimit = 0 }, diff --git a/cmd/proxy/config/apiConfig/v_next.toml b/cmd/proxy/config/apiConfig/v_next.toml index d38355ab..146d42bb 100644 --- a/cmd/proxy/config/apiConfig/v_next.toml +++ b/cmd/proxy/config/apiConfig/v_next.toml @@ -70,6 +70,7 @@ Routes = [ { Name = "/delegated-info", Open = true, Secured = true, RateLimit = 0 }, { Name = "/enable-epochs", Open = true, Secured = false, RateLimit = 0 }, { Name = "/enable-epochs-v2", Open = true, Secured = false, RateLimit = 0 }, + { Name = "/enable-rounds", Open = true, Secured = false, RateLimit = 0 }, { Name = "/ratings", Open = true, Secured = false, RateLimit = 0 }, { Name = "/genesis-nodes", Open = true, Secured = false, RateLimit = 0 }, { Name = "/gas-configs", Open = true, Secured = false, RateLimit = 0 }, diff --git a/cmd/proxy/config/swagger/openapi.json b/cmd/proxy/config/swagger/openapi.json index f38c35bf..94bba8e6 100644 --- a/cmd/proxy/config/swagger/openapi.json +++ b/cmd/proxy/config/swagger/openapi.json @@ -1091,6 +1091,26 @@ } } }, + "/network/enable-rounds": { + "get": { + "tags": [ + "network" + ], + "summary": "returns the activation rounds metric", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericResponse" + } + } + } + } + } + } + }, "/network/ratings": { "get": { "tags": [ diff --git a/facade/baseFacade.go b/facade/baseFacade.go index b3fb63c8..18b8ed7d 100644 --- a/facade/baseFacade.go +++ b/facade/baseFacade.go @@ -350,6 +350,11 @@ func (pf *ProxyFacade) GetEnableEpochsMetricsV2() (*data.GenericAPIResponse, err return pf.nodeStatusProc.GetEnableEpochsMetricsV2() } +// GetEnableRoundsMetrics retrieves the activation rounds +func (pf *ProxyFacade) GetEnableRoundsMetrics() (*data.GenericAPIResponse, error) { + return pf.nodeStatusProc.GetEnableRoundsMetrics() +} + // GetRatingsConfig retrieves the node's configuration's metrics func (pf *ProxyFacade) GetRatingsConfig() (*data.GenericAPIResponse, error) { return pf.nodeStatusProc.GetRatingsConfig() diff --git a/facade/interface.go b/facade/interface.go index aaa712fc..45238192 100644 --- a/facade/interface.go +++ b/facade/interface.go @@ -6,6 +6,7 @@ import ( "github.com/multiversx/mx-chain-core-go/data/transaction" "github.com/multiversx/mx-chain-core-go/data/vm" crypto "github.com/multiversx/mx-chain-crypto-go" + "github.com/multiversx/mx-chain-proxy-go/common" "github.com/multiversx/mx-chain-proxy-go/data" ) @@ -93,6 +94,7 @@ type NodeStatusProcessor interface { GetAllIssuedESDTs(tokenType string) (*data.GenericAPIResponse, error) GetEnableEpochsMetrics() (*data.GenericAPIResponse, error) GetEnableEpochsMetricsV2() (*data.GenericAPIResponse, error) + GetEnableRoundsMetrics() (*data.GenericAPIResponse, error) GetDirectStakedInfo() (*data.GenericAPIResponse, error) GetDelegatedInfo() (*data.GenericAPIResponse, error) GetRatingsConfig() (*data.GenericAPIResponse, error) diff --git a/facade/mock/nodeStatusProcessorStub.go b/facade/mock/nodeStatusProcessorStub.go index 991524e4..9dd0f451 100644 --- a/facade/mock/nodeStatusProcessorStub.go +++ b/facade/mock/nodeStatusProcessorStub.go @@ -13,6 +13,7 @@ type NodeStatusProcessorStub struct { GetDelegatedInfoCalled func() (*data.GenericAPIResponse, error) GetEnableEpochsMetricsCalled func() (*data.GenericAPIResponse, error) GetEnableEpochsMetricsV2Called func() (*data.GenericAPIResponse, error) + GetEnableRoundsMetricsCalled func() (*data.GenericAPIResponse, error) GetRatingsConfigCalled func() (*data.GenericAPIResponse, error) GetGenesisNodesPubKeysCalled func() (*data.GenericAPIResponse, error) GetGasConfigsCalled func() (*data.GenericAPIResponse, error) @@ -101,6 +102,15 @@ func (stub *NodeStatusProcessorStub) GetEnableEpochsMetricsV2() (*data.GenericAP return &data.GenericAPIResponse{}, nil } +// GetEnableRoundsMetrics - +func (stub *NodeStatusProcessorStub) GetEnableRoundsMetrics() (*data.GenericAPIResponse, error) { + if stub.GetEnableRoundsMetricsCalled != nil { + return stub.GetEnableRoundsMetricsCalled() + } + + return &data.GenericAPIResponse{}, nil +} + // GetRatingsConfig - func (stub *NodeStatusProcessorStub) GetRatingsConfig() (*data.GenericAPIResponse, error) { if stub.GetRatingsConfigCalled != nil { diff --git a/process/nodeStatusProcessor.go b/process/nodeStatusProcessor.go index b96d7128..365b045d 100644 --- a/process/nodeStatusProcessor.go +++ b/process/nodeStatusProcessor.go @@ -10,6 +10,7 @@ import ( "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-core-go/core/check" + "github.com/multiversx/mx-chain-proxy-go/data" ) @@ -50,6 +51,9 @@ const ( // EnableEpochsV2Path represents the path where an observer exposes all the activation epochs EnableEpochsV2Path = "/network/enable-epochs-v2" + // EnableRoundsPath represents the path where an observer exposes all the activation rounds + EnableRoundsPath = "/network/enable-rounds" + // MetricCrossCheckBlockHeight is the metric that stores cross block height MetricCrossCheckBlockHeight = "erd_cross_check_block_height" @@ -149,6 +153,11 @@ func (nsp *NodeStatusProcessor) GetEnableEpochsMetricsV2() (*data.GenericAPIResp return nsp.getEnableEpochsMetrics(EnableEpochsV2Path) } +// GetEnableRoundsMetrics will simply forward the activation rounds config metrics from an observer +func (nsp *NodeStatusProcessor) GetEnableRoundsMetrics() (*data.GenericAPIResponse, error) { + return nsp.getEnableRoundsMetrics(EnableRoundsPath) +} + func (nsp *NodeStatusProcessor) getEnableEpochsMetrics(path string) (*data.GenericAPIResponse, error) { observers, err := nsp.proc.GetAllObservers(data.AvailabilityRecent) if err != nil { @@ -171,6 +180,27 @@ func (nsp *NodeStatusProcessor) getEnableEpochsMetrics(path string) (*data.Gener return nil, WrapObserversError(responseEnableEpochsMetrics.Error) } +func (nsp *NodeStatusProcessor) getEnableRoundsMetrics(path string) (*data.GenericAPIResponse, error) { + observers, err := nsp.proc.GetAllObservers(data.AvailabilityRecent) + if err != nil { + return nil, err + } + + responseEnableRoundsMetrics := data.GenericAPIResponse{} + for _, observer := range observers { + _, err := nsp.proc.CallGetRestEndPoint(observer.Address, path, &responseEnableRoundsMetrics) + if err != nil { + log.Error("enable rounds metrics request", "observer", observer.Address, "error", err.Error()) + continue + } + + log.Info("enable rounds metrics request", "shard ID", observer.ShardId, "observer", observer.Address) + return &responseEnableRoundsMetrics, nil + } + + return nil, WrapObserversError(responseEnableRoundsMetrics.Error) +} + // GetAllIssuedESDTs will forward the issued ESDTs based on the provided type func (nsp *NodeStatusProcessor) GetAllIssuedESDTs(tokenType string) (*data.GenericAPIResponse, error) { if !data.IsValidEsdtPath(tokenType) && tokenType != "" { diff --git a/process/nodeStatusProcessor_test.go b/process/nodeStatusProcessor_test.go index d4b67c8b..d5764329 100644 --- a/process/nodeStatusProcessor_test.go +++ b/process/nodeStatusProcessor_test.go @@ -578,6 +578,84 @@ func TestNodeStatusProcessor_GetEnableEpochsMetricsGetObserversShouldErr(t *test require.Nil(t, status) } +func TestNodeStatusProcessor_GetEnableRoundsMetricsGetObserversShouldErr(t *testing.T) { + t.Parallel() + + localErr := errors.New("local error") + nodeStatusProc, _ := NewNodeStatusProcessor(&mock.ProcessorStub{ + GetAllObserversCalled: func(dataAvailability data.ObserverDataAvailabilityType) ([]*data.NodeData, error) { + return nil, localErr + }, + }, + &mock.GenericApiResponseCacherMock{}, + time.Nanosecond, + ) + + status, err := nodeStatusProc.GetEnableRoundsMetrics() + require.Equal(t, localErr, err) + require.Nil(t, status) +} + +func TestNodeStatusProcessor_GetEnableRoundsMetricsGetEndpointErr(t *testing.T) { + t.Parallel() + + localErr := errors.New("local error") + nodesStatusProc, _ := NewNodeStatusProcessor(&mock.ProcessorStub{ + GetAllObserversCalled: func(dataAvailability data.ObserverDataAvailabilityType) ([]*data.NodeData, error) { + return []*data.NodeData{ + {Address: "addr1", ShardId: 0}, + }, nil + }, + CallGetRestEndPointCalled: func(address string, path string, value interface{}) (int, error) { + return 0, localErr + }, + }, + &mock.GenericApiResponseCacherMock{}, + time.Nanosecond, + ) + + status, err := nodesStatusProc.GetEnableRoundsMetrics() + require.True(t, errors.Is(err, ErrSendingRequest)) + require.Nil(t, status) +} + +func TestNodeStatusProcessor_GetEnableRoundsMetricsShouldWork(t *testing.T) { + t.Parallel() + + key := "SupernovaEnableRound" + expectedValue := float64(100) + nodesStatusProc, _ := NewNodeStatusProcessor(&mock.ProcessorStub{ + GetAllObserversCalled: func(dataAvailability data.ObserverDataAvailabilityType) ([]*data.NodeData, error) { + return []*data.NodeData{ + {Address: "addr1", ShardId: 0}, + }, nil + }, + CallGetRestEndPointCalled: func(address string, path string, value interface{}) (int, error) { + metricMap := map[string]interface{}{ + key: expectedValue, + } + genericResp := &data.GenericAPIResponse{Data: metricMap} + genericRespBytes, _ := json.Marshal(genericResp) + + return 0, json.Unmarshal(genericRespBytes, value) + }, + }, + &mock.GenericApiResponseCacherMock{}, + time.Nanosecond, + ) + + genericResponse, err := nodesStatusProc.GetEnableRoundsMetrics() + require.Nil(t, err) + require.NotNil(t, genericResponse) + + metricsMap, ok := genericResponse.Data.(map[string]interface{}) + require.True(t, ok) + + actualValue, ok := metricsMap[key] + require.True(t, ok) + require.Equal(t, expectedValue, actualValue) +} + func TestNodeStatusProcessor_GetRatingsConfigGetAllObserversShouldFail(t *testing.T) { t.Parallel()