diff --git a/api/groups/baseNodeGroup.go b/api/groups/baseNodeGroup.go index c822fa96..aecd4c85 100644 --- a/api/groups/baseNodeGroup.go +++ b/api/groups/baseNodeGroup.go @@ -29,6 +29,7 @@ func NewNodeGroup(facadeHandler data.FacadeHandler) (*nodeGroup, error) { baseRoutesHandlers := []*data.EndpointHandlerData{ {Path: "/heartbeatstatus", Handler: ng.getHeartbeatData, Method: http.MethodGet}, {Path: "/old-storage-token/:token/nonce/:nonce", Handler: ng.isOldStorageForToken, Method: http.MethodGet}, + {Path: "/waiting-epochs-left/:key", Handler: ng.waitingEpochsLeft, Method: http.MethodGet}, } ng.baseGroup.endpoints = baseRoutesHandlers @@ -69,3 +70,14 @@ func (group *nodeGroup) isOldStorageForToken(c *gin.Context) { shared.RespondWith(c, http.StatusOK, gin.H{"isOldStorage": isOldStorage}, "", data.ReturnCodeSuccess) } + +func (group *nodeGroup) waitingEpochsLeft(c *gin.Context) { + publicKey := c.Param("key") + response, err := group.facade.GetWaitingEpochsLeftForPublicKey(publicKey) + if err != nil { + shared.RespondWith(c, http.StatusInternalServerError, nil, err.Error(), data.ReturnCodeInternalError) + return + } + + shared.RespondWith(c, http.StatusOK, response.Data, "", data.ReturnCodeSuccess) +} diff --git a/api/groups/baseNodeGroup_test.go b/api/groups/baseNodeGroup_test.go index 25083b58..402516d0 100644 --- a/api/groups/baseNodeGroup_test.go +++ b/api/groups/baseNodeGroup_test.go @@ -152,3 +152,53 @@ func TestNodeGroup_IsOldStorageToken(t *testing.T) { assert.Equal(t, "map[isOldStorage:true]", fmt.Sprintf("%v", result.Data)) }) } + +func TestBaseNodeGroup_GetWaitingEpochsLeftForPublicKey(t *testing.T) { + t.Parallel() + + t.Run("facade returns bad request", func(t *testing.T) { + t.Parallel() + + facade := &mock.FacadeStub{ + GetWaitingEpochsLeftForPublicKeyCalled: func(publicKey string) (*data.WaitingEpochsLeftApiResponse, error) { + return nil, errors.New("bad request") + }, + } + nodeGroup, err := groups.NewNodeGroup(facade) + require.NoError(t, err) + ws := startProxyServer(nodeGroup, nodePath) + + req, _ := http.NewRequest("GET", "/node/waiting-epochs-left/key", nil) + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + + assert.Equal(t, http.StatusInternalServerError, resp.Code) + }) + t.Run("facade returns bad request", func(t *testing.T) { + t.Parallel() + + providedData := data.WaitingEpochsLeftResponse{ + EpochsLeft: 10, + } + facade := &mock.FacadeStub{ + GetWaitingEpochsLeftForPublicKeyCalled: func(publicKey string) (*data.WaitingEpochsLeftApiResponse, error) { + return &data.WaitingEpochsLeftApiResponse{ + Data: providedData, + }, nil + }, + } + nodeGroup, err := groups.NewNodeGroup(facade) + require.NoError(t, err) + ws := startProxyServer(nodeGroup, nodePath) + + req, _ := http.NewRequest("GET", "/node/waiting-epochs-left/key", nil) + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + + assert.Equal(t, http.StatusOK, resp.Code) + + var result data.WaitingEpochsLeftApiResponse + loadResponse(resp.Body, &result) + assert.Equal(t, providedData, result.Data) + }) +} diff --git a/api/groups/interface.go b/api/groups/interface.go index 253a3327..0557468b 100644 --- a/api/groups/interface.go +++ b/api/groups/interface.go @@ -81,6 +81,7 @@ type NetworkFacadeHandler interface { type NodeFacadeHandler interface { GetHeartbeatData() (*data.HeartbeatResponse, error) IsOldStorageForToken(tokenID string, nonce uint64) (bool, error) + GetWaitingEpochsLeftForPublicKey(publicKey string) (*data.WaitingEpochsLeftApiResponse, error) } // StatusFacadeHandler interface defines methods that can be used from the facade diff --git a/api/mock/facadeStub.go b/api/mock/facadeStub.go index abf744a8..89143c40 100644 --- a/api/mock/facadeStub.go +++ b/api/mock/facadeStub.go @@ -81,6 +81,7 @@ type FacadeStub struct { GetCodeHashCalled func(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) GetGuardianDataCalled func(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) IsDataTrieMigratedCalled func(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) + GetWaitingEpochsLeftForPublicKeyCalled func(publicKey string) (*data.WaitingEpochsLeftApiResponse, error) } // GetProof - @@ -554,6 +555,14 @@ func (f *FacadeStub) IsDataTrieMigrated(address string, options common.AccountQu return &data.GenericAPIResponse{}, nil } +// GetWaitingEpochsLeftForPublicKey - +func (f *FacadeStub) GetWaitingEpochsLeftForPublicKey(publicKey string) (*data.WaitingEpochsLeftApiResponse, error) { + if f.GetWaitingEpochsLeftForPublicKeyCalled != nil { + return f.GetWaitingEpochsLeftForPublicKeyCalled(publicKey) + } + return &data.WaitingEpochsLeftApiResponse{}, nil +} + // WrongFacade is a struct that can be used as a wrong implementation of the node router handler type WrongFacade struct { } diff --git a/cmd/proxy/config/apiConfig/v1_0.toml b/cmd/proxy/config/apiConfig/v1_0.toml index b8cea0ed..71a13e46 100644 --- a/cmd/proxy/config/apiConfig/v1_0.toml +++ b/cmd/proxy/config/apiConfig/v1_0.toml @@ -24,7 +24,8 @@ Routes = [ [APIPackages.node] Routes = [ { Name = "/heartbeatstatus", Open = true, Secured = false, RateLimit = 0 }, - { Name = "/old-storage-token/:token/nonce/:nonce", Open = true, Secured = false, RateLimit = 0} + { Name = "/old-storage-token/:token/nonce/:nonce", Open = true, Secured = false, RateLimit = 0}, + { Name = "/waiting-epochs-left/:key", Open = true, Secured = false, RateLimit = 0} ] [APIPackages.address] diff --git a/cmd/proxy/config/apiConfig/v_next.toml b/cmd/proxy/config/apiConfig/v_next.toml index c1e6a0e6..292772b1 100644 --- a/cmd/proxy/config/apiConfig/v_next.toml +++ b/cmd/proxy/config/apiConfig/v_next.toml @@ -24,7 +24,8 @@ Routes = [ [APIPackages.node] Routes = [ { Name = "/heartbeatstatus", Open = true, Secured = false, RateLimit = 0 }, - { Name = "/old-storage-token/:token/nonce/:nonce", Open = true, Secured = false, RateLimit = 0} + { Name = "/old-storage-token/:token/nonce/:nonce", Open = true, Secured = false, RateLimit = 0}, + { Name = "/waiting-epochs-left/:key", Open = true, Secured = false, RateLimit = 0} ] [APIPackages.address] diff --git a/cmd/proxy/config/swagger/openapi.json b/cmd/proxy/config/swagger/openapi.json index 41539fe3..2abd81ee 100644 --- a/cmd/proxy/config/swagger/openapi.json +++ b/cmd/proxy/config/swagger/openapi.json @@ -1406,6 +1406,40 @@ } } }, + "/node/waiting-epochs-left/{key}": { + "get": { + "tags": [ + "node" + ], + "summary": "will return the number of epochs left for the public key until it becomes eligible", + "parameters": [ + { + "name": "key", + "in": "path", + "description": "the public key to look after", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericResponse" + } + } + } + }, + "400": { + "description": "validation error" + } + } + } + }, "/proof/root-hash/{roothash}/address/{address}": { "get": { "tags": [ diff --git a/data/nodeStatus.go b/data/nodeStatus.go index 11e44884..ed8e0cd5 100644 --- a/data/nodeStatus.go +++ b/data/nodeStatus.go @@ -69,3 +69,15 @@ type TrieStatisticsAPIResponse struct { Error string `json:"error"` Code string `json:"code"` } + +// WaitingEpochsLeftResponse matches the output structure the data field for a waiting epochs left response +type WaitingEpochsLeftResponse struct { + EpochsLeft uint32 `json:"epochsLeft"` +} + +// WaitingEpochsLeftApiResponse matches the output of an observer's waiting epochs left endpoint +type WaitingEpochsLeftApiResponse struct { + Data WaitingEpochsLeftResponse `json:"data"` + Error string `json:"error"` + Code string `json:"code"` +} diff --git a/facade/baseFacade.go b/facade/baseFacade.go index 3a72611a..2cacf673 100644 --- a/facade/baseFacade.go +++ b/facade/baseFacade.go @@ -531,6 +531,11 @@ func (epf *ProxyFacade) GetInternalStartOfEpochValidatorsInfo(epoch uint32) (*da return epf.blockProc.GetInternalStartOfEpochValidatorsInfo(epoch) } +// GetWaitingEpochsLeftForPublicKey returns the number of epochs left for the public key until it becomes eligible +func (epf *ProxyFacade) GetWaitingEpochsLeftForPublicKey(publicKey string) (*data.WaitingEpochsLeftApiResponse, error) { + return epf.nodeGroupProc.GetWaitingEpochsLeftForPublicKey(publicKey) +} + // IsDataTrieMigrated returns true if the data trie for the given address is migrated func (epf *ProxyFacade) IsDataTrieMigrated(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) { return epf.accountProc.IsDataTrieMigrated(address, options) diff --git a/facade/baseFacade_test.go b/facade/baseFacade_test.go index 4e136fa2..77430a75 100644 --- a/facade/baseFacade_test.go +++ b/facade/baseFacade_test.go @@ -1061,6 +1061,41 @@ func TestProxyFacade_GetGasConfigs(t *testing.T) { assert.Equal(t, expectedResult, actualResult) } +func TestProxyFacade_GetWaitingEpochsLeftForPublicKey(t *testing.T) { + t.Parallel() + + expectedResults := &data.WaitingEpochsLeftApiResponse{ + Data: data.WaitingEpochsLeftResponse{ + EpochsLeft: 10, + }, + } + epf, _ := facade.NewProxyFacade( + &mock.ActionsProcessorStub{}, + &mock.AccountProcessorStub{}, + &mock.TransactionProcessorStub{}, + &mock.SCQueryServiceStub{}, + &mock.NodeGroupProcessorStub{ + GetWaitingEpochsLeftForPublicKeyCalled: func(publicKey string) (*data.WaitingEpochsLeftApiResponse, error) { + return expectedResults, nil + }, + }, + &mock.ValidatorStatisticsProcessorStub{}, + &mock.FaucetProcessorStub{}, + &mock.NodeStatusProcessorStub{}, + &mock.BlockProcessorStub{}, + &mock.BlocksProcessorStub{}, + &mock.ProofProcessorStub{}, + publicKeyConverter, + &mock.ESDTSuppliesProcessorStub{}, + &mock.StatusProcessorStub{}, + &mock.AboutInfoProcessorStub{}, + ) + + actualResult, _ := epf.GetWaitingEpochsLeftForPublicKey("key") + + assert.Equal(t, expectedResults, actualResult) +} + func getPrivKey() crypto.PrivateKey { keyGen := signing.NewKeyGenerator(ed25519.NewEd25519()) sk, _ := keyGen.GeneratePair() diff --git a/facade/interface.go b/facade/interface.go index ceb04241..d06a049c 100644 --- a/facade/interface.go +++ b/facade/interface.go @@ -69,6 +69,7 @@ type SCQueryService interface { type NodeGroupProcessor interface { GetHeartbeatData() (*data.HeartbeatResponse, error) IsOldStorageForToken(tokenID string, nonce uint64) (bool, error) + GetWaitingEpochsLeftForPublicKey(publicKey string) (*data.WaitingEpochsLeftApiResponse, error) } // ValidatorStatisticsProcessor defines what a validator statistics processor should do diff --git a/facade/mock/nodeGroupProcessorStub.go b/facade/mock/nodeGroupProcessorStub.go index a65f3b01..eb0aa106 100644 --- a/facade/mock/nodeGroupProcessorStub.go +++ b/facade/mock/nodeGroupProcessorStub.go @@ -4,8 +4,9 @@ import "github.com/multiversx/mx-chain-proxy-go/data" // NodeGroupProcessorStub represents a stub implementation of a NodeGroupProcessor type NodeGroupProcessorStub struct { - GetHeartbeatDataCalled func() (*data.HeartbeatResponse, error) - IsOldStorageForTokenCalled func(tokenID string, nonce uint64) (bool, error) + GetHeartbeatDataCalled func() (*data.HeartbeatResponse, error) + IsOldStorageForTokenCalled func(tokenID string, nonce uint64) (bool, error) + GetWaitingEpochsLeftForPublicKeyCalled func(publicKey string) (*data.WaitingEpochsLeftApiResponse, error) } // IsOldStorageForToken - @@ -17,3 +18,11 @@ func (hbps *NodeGroupProcessorStub) IsOldStorageForToken(tokenID string, nonce u func (hbps *NodeGroupProcessorStub) GetHeartbeatData() (*data.HeartbeatResponse, error) { return hbps.GetHeartbeatDataCalled() } + +// GetWaitingEpochsLeftForPublicKey - +func (hbps *NodeGroupProcessorStub) GetWaitingEpochsLeftForPublicKey(publicKey string) (*data.WaitingEpochsLeftApiResponse, error) { + if hbps.GetWaitingEpochsLeftForPublicKeyCalled != nil { + return hbps.GetWaitingEpochsLeftForPublicKeyCalled(publicKey) + } + return &data.WaitingEpochsLeftApiResponse{}, nil +} diff --git a/process/errors.go b/process/errors.go index e5f1ddbc..65b03271 100644 --- a/process/errors.go +++ b/process/errors.go @@ -109,3 +109,6 @@ var ErrEmptyAppVersionString = errors.New("empty app version string") // ErrEmptyCommitString signals than an empty commit id string has been provided var ErrEmptyCommitString = errors.New("empty commit id string") + +// ErrEmptyPubKey signals that an empty public key has been provided +var ErrEmptyPubKey = errors.New("public key is empty") diff --git a/process/nodeGroupProcessor.go b/process/nodeGroupProcessor.go index 6f4088c6..caf4bf92 100644 --- a/process/nodeGroupProcessor.go +++ b/process/nodeGroupProcessor.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "errors" + "fmt" "math/big" "net/http" "sort" @@ -14,10 +15,13 @@ import ( "github.com/multiversx/mx-chain-proxy-go/data" ) -// HeartBeatPath represents the path where an observer exposes his heartbeat status -const HeartBeatPath = "/node/heartbeatstatus" - -const systemAccountAddress = "erd1lllllllllllllllllllllllllllllllllllllllllllllllllllsckry7t" +const ( + // heartbeatPath represents the path where an observer exposes his heartbeat status + heartbeatPath = "/node/heartbeatstatus" + // waitingEpochsLeftPath represents the path where an observer the number of epochs left in waiting state for a key + waitingEpochsLeftPath = "/node/waiting-epochs-left/%s" + systemAccountAddress = "erd1lllllllllllllllllllllllllllllllllllllllllllllllllllsckry7t" +) // NodeGroupProcessor is able to process transaction requests type NodeGroupProcessor struct { @@ -42,18 +46,18 @@ func NewNodeGroupProcessor( if cacheValidityDuration <= 0 { return nil, ErrInvalidCacheValidityDuration } - hbp := &NodeGroupProcessor{ + ngp := &NodeGroupProcessor{ proc: proc, cacher: cacher, cacheValidityDuration: cacheValidityDuration, } - return hbp, nil + return ngp, nil } // IsOldStorageForToken returns true if the token is stored in the old fashion -func (hbp *NodeGroupProcessor) IsOldStorageForToken(tokenID string, nonce uint64) (bool, error) { - observers, err := hbp.proc.GetAllObservers() +func (ngp *NodeGroupProcessor) IsOldStorageForToken(tokenID string, nonce uint64) (bool, error) { + observers, err := ngp.proc.GetAllObservers() if err != nil { return false, err } @@ -67,7 +71,7 @@ func (hbp *NodeGroupProcessor) IsOldStorageForToken(tokenID string, nonce uint64 apiResponse := data.AccountKeyValueResponse{} apiPath := addressPath + systemAccountAddress + "/key/" + tokenStorageKey - respCode, err := hbp.proc.CallGetRestEndPoint(observer.Address, apiPath, &apiResponse) + respCode, err := ngp.proc.CallGetRestEndPoint(observer.Address, apiPath, &apiResponse) if err == nil || respCode == http.StatusBadRequest || respCode == http.StatusInternalServerError { log.Info("account value for key request", "address", systemAccountAddress, @@ -104,23 +108,23 @@ func computeTokenStorageKey(tokenID string, nonce uint64) string { } // GetHeartbeatData will simply forward the heartbeat status from an observer -func (hbp *NodeGroupProcessor) GetHeartbeatData() (*data.HeartbeatResponse, error) { - heartbeatsToReturn, err := hbp.cacher.LoadHeartbeats() +func (ngp *NodeGroupProcessor) GetHeartbeatData() (*data.HeartbeatResponse, error) { + heartbeatsToReturn, err := ngp.cacher.LoadHeartbeats() if err == nil { return heartbeatsToReturn, nil } log.Info("heartbeat: cannot get from cache. Will fetch from API", "error", err.Error()) - return hbp.getHeartbeatsFromApi() + return ngp.getHeartbeatsFromApi() } -func (hbp *NodeGroupProcessor) getHeartbeatsFromApi() (*data.HeartbeatResponse, error) { - shardIDs := hbp.proc.GetShardIDs() +func (ngp *NodeGroupProcessor) getHeartbeatsFromApi() (*data.HeartbeatResponse, error) { + shardIDs := ngp.proc.GetShardIDs() responseMap := make(map[string]data.PubKeyHeartbeat) for _, shard := range shardIDs { - observers, err := hbp.proc.GetObservers(shard) + observers, err := ngp.proc.GetObservers(shard) if err != nil { log.Error("could not get observers", "shard", shard, "error", err.Error()) continue @@ -129,10 +133,10 @@ func (hbp *NodeGroupProcessor) getHeartbeatsFromApi() (*data.HeartbeatResponse, errorsCount := 0 var response data.HeartbeatApiResponse for _, observer := range observers { - _, err = hbp.proc.CallGetRestEndPoint(observer.Address, HeartBeatPath, &response) + _, err = ngp.proc.CallGetRestEndPoint(observer.Address, heartbeatPath, &response) heartbeats := response.Data.Heartbeats if err == nil && len(heartbeats) > 0 { - hbp.addMessagesToMap(responseMap, heartbeats, shard) + ngp.addMessagesToMap(responseMap, heartbeats, shard) break } @@ -155,10 +159,10 @@ func (hbp *NodeGroupProcessor) getHeartbeatsFromApi() (*data.HeartbeatResponse, return nil, ErrHeartbeatNotAvailable } - return hbp.mapToResponse(responseMap), nil + return ngp.mapToResponse(responseMap), nil } -func (hbp *NodeGroupProcessor) addMessagesToMap(responseMap map[string]data.PubKeyHeartbeat, heartbeats []data.PubKeyHeartbeat, observerShard uint32) { +func (ngp *NodeGroupProcessor) addMessagesToMap(responseMap map[string]data.PubKeyHeartbeat, heartbeats []data.PubKeyHeartbeat, observerShard uint32) { for _, heartbeatMessage := range heartbeats { isMessageFromCurrentShard := heartbeatMessage.ComputedShardID == observerShard isMessageFromShardAfterShuffleOut := heartbeatMessage.ReceivedShardID == observerShard @@ -179,7 +183,7 @@ func (hbp *NodeGroupProcessor) addMessagesToMap(responseMap map[string]data.PubK } } -func (hbp *NodeGroupProcessor) mapToResponse(responseMap map[string]data.PubKeyHeartbeat) *data.HeartbeatResponse { +func (ngp *NodeGroupProcessor) mapToResponse(responseMap map[string]data.PubKeyHeartbeat) *data.HeartbeatResponse { heartbeats := make([]data.PubKeyHeartbeat, 0) for _, heartbeatMessage := range responseMap { heartbeats = append(heartbeats, heartbeatMessage) @@ -195,27 +199,27 @@ func (hbp *NodeGroupProcessor) mapToResponse(responseMap map[string]data.PubKeyH } // StartCacheUpdate will start the updating of the cache from the API at a given period -func (hbp *NodeGroupProcessor) StartCacheUpdate() { - if hbp.cancelFunc != nil { +func (ngp *NodeGroupProcessor) StartCacheUpdate() { + if ngp.cancelFunc != nil { log.Error("NodeGroupProcessor - cache update already started") return } var ctx context.Context - ctx, hbp.cancelFunc = context.WithCancel(context.Background()) + ctx, ngp.cancelFunc = context.WithCancel(context.Background()) go func(ctx context.Context) { - timer := time.NewTimer(hbp.cacheValidityDuration) + timer := time.NewTimer(ngp.cacheValidityDuration) defer timer.Stop() - hbp.handleHeartbeatCacheUpdate() + ngp.handleHeartbeatCacheUpdate() for { - timer.Reset(hbp.cacheValidityDuration) + timer.Reset(ngp.cacheValidityDuration) select { case <-timer.C: - hbp.handleHeartbeatCacheUpdate() + ngp.handleHeartbeatCacheUpdate() case <-ctx.Done(): log.Debug("finishing NodeGroupProcessor cache update...") return @@ -224,24 +228,53 @@ func (hbp *NodeGroupProcessor) StartCacheUpdate() { }(ctx) } -func (hbp *NodeGroupProcessor) handleHeartbeatCacheUpdate() { - hbts, err := hbp.getHeartbeatsFromApi() +func (ngp *NodeGroupProcessor) handleHeartbeatCacheUpdate() { + hbts, err := ngp.getHeartbeatsFromApi() if err != nil { log.Warn("heartbeat: get from API", "error", err.Error()) } if hbts != nil { - err = hbp.cacher.StoreHeartbeats(hbts) + err = ngp.cacher.StoreHeartbeats(hbts) if err != nil { log.Warn("heartbeat: store in cache", "error", err.Error()) } } } +// GetWaitingEpochsLeftForPublicKey returns the number of epochs left for the public key until it becomes eligible +func (ngp *NodeGroupProcessor) GetWaitingEpochsLeftForPublicKey(publicKey string) (*data.WaitingEpochsLeftApiResponse, error) { + if len(publicKey) == 0 { + return nil, ErrEmptyPubKey + } + + observers, err := ngp.proc.GetAllObservers() + if err != nil { + return nil, err + } + + var lastErr error + var responseWaitingEpochsLeft data.WaitingEpochsLeftApiResponse + path := fmt.Sprintf(waitingEpochsLeftPath, publicKey) + for _, observer := range observers { + _, lastErr = ngp.proc.CallGetRestEndPoint(observer.Address, path, &responseWaitingEpochsLeft) + if lastErr != nil { + log.Error("waiting epochs left request", "observer", observer.Address, "public key", publicKey, "error", lastErr.Error()) + continue + } + + log.Info("waiting epochs left request", "shard ID", observer.ShardId, "observer", observer.Address, "public key", publicKey) + return &responseWaitingEpochsLeft, nil + + } + + return nil, fmt.Errorf("%w, %s", ErrSendingRequest, responseWaitingEpochsLeft.Error) +} + // Close will handle the closing of the cache update go routine -func (hbp *NodeGroupProcessor) Close() error { - if hbp.cancelFunc != nil { - hbp.cancelFunc() +func (ngp *NodeGroupProcessor) Close() error { + if ngp.cancelFunc != nil { + ngp.cancelFunc() } return nil diff --git a/process/nodeGroupProcessor_test.go b/process/nodeGroupProcessor_test.go index 918a8564..0c960bf5 100644 --- a/process/nodeGroupProcessor_test.go +++ b/process/nodeGroupProcessor_test.go @@ -632,3 +632,88 @@ func TestComputeTokenStorageKey(t *testing.T) { expectedKey := append(append([]byte(core.ProtectedKeyPrefix+"esdt"), []byte(testTokenID)...), big.NewInt(int64(testNonce)).Bytes()...) require.Equal(t, hex.EncodeToString(expectedKey), process.ComputeTokenStorageKey(testTokenID, testNonce)) } + +func TestNodeGroupProcessor_GetWaitingEpochsLeftForPublicKey(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("expected error") + t.Run("empty pub key should error", func(t *testing.T) { + t.Parallel() + + proc, _ := process.NewNodeGroupProcessor( + &mock.ProcessorStub{}, + &mock.HeartbeatCacherMock{}, + 10, + ) + + response, err := proc.GetWaitingEpochsLeftForPublicKey("") + require.Nil(t, response) + require.Equal(t, process.ErrEmptyPubKey, err) + }) + t.Run("GetAllObservers returns error should error", func(t *testing.T) { + t.Parallel() + + proc, _ := process.NewNodeGroupProcessor( + &mock.ProcessorStub{ + GetAllObserversCalled: func() ([]*data.NodeData, error) { + return nil, expectedErr + }, + }, + &mock.HeartbeatCacherMock{}, + 10, + ) + + response, err := proc.GetWaitingEpochsLeftForPublicKey("key") + require.Nil(t, response) + require.Equal(t, expectedErr, err) + }) + t.Run("all observers return error should error", func(t *testing.T) { + t.Parallel() + + proc, _ := process.NewNodeGroupProcessor( + &mock.ProcessorStub{ + GetAllObserversCalled: func() ([]*data.NodeData, error) { + return []*data.NodeData{ + {Address: "addr0", ShardId: 0}, + {Address: "addr1", ShardId: 1}, + }, nil + }, + CallGetRestEndPointCalled: func(address string, path string, value interface{}) (int, error) { + return 0, expectedErr + }, + }, + &mock.HeartbeatCacherMock{}, + 10, + ) + + response, err := proc.GetWaitingEpochsLeftForPublicKey("key") + require.Nil(t, response) + require.True(t, errors.Is(err, process.ErrSendingRequest)) + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + providedEpochsLeft := uint32(10) + proc, _ := process.NewNodeGroupProcessor( + &mock.ProcessorStub{ + GetAllObserversCalled: func() ([]*data.NodeData, error) { + return []*data.NodeData{ + {Address: "addr0", ShardId: 0}, + {Address: "addr1", ShardId: 1}, + }, nil + }, + CallGetRestEndPointCalled: func(address string, path string, value interface{}) (int, error) { + valResponse := value.(*data.WaitingEpochsLeftApiResponse) + valResponse.Data.EpochsLeft = providedEpochsLeft + return 0, nil + }, + }, + &mock.HeartbeatCacherMock{}, + 10, + ) + + response, err := proc.GetWaitingEpochsLeftForPublicKey("key") + require.NoError(t, err) + require.Equal(t, providedEpochsLeft, response.Data.EpochsLeft) + }) +}