diff --git a/api/errors/errors.go b/api/errors/errors.go index 1ad359a6..04889abd 100644 --- a/api/errors/errors.go +++ b/api/errors/errors.go @@ -125,6 +125,9 @@ var ErrInvalidFields = errors.New("invalid fields") // ErrOperationNotAllowed signals that the operation is not allowed var ErrOperationNotAllowed = errors.New("operation not allowed") +// ErrIsDataTrieMigrated signals that an error occurred while trying to verify the migration status of the data trie +var ErrIsDataTrieMigrated = errors.New("could not verify the migration status of the data trie") + // ErrInvalidTxFields signals that one or more field of a transaction are invalid type ErrInvalidTxFields struct { Message string diff --git a/api/groups/baseAccountsGroup.go b/api/groups/baseAccountsGroup.go index 5703c13b..5ce1ab58 100644 --- a/api/groups/baseAccountsGroup.go +++ b/api/groups/baseAccountsGroup.go @@ -43,6 +43,7 @@ func NewAccountsGroup(facadeHandler data.FacadeHandler) (*accountsGroup, error) {Path: "/:address/esdts/roles", Handler: ag.getESDTsRoles, Method: http.MethodGet}, {Path: "/:address/registered-nfts", Handler: ag.getRegisteredNFTs, Method: http.MethodGet}, {Path: "/:address/nft/:tokenIdentifier/nonce/:nonce", Handler: ag.getESDTNftTokenData, Method: http.MethodGet}, + {Path: "/:address/is-data-trie-migrated", Handler: ag.isDataTrieMigrated, Method: http.MethodGet}, } ag.baseGroup.endpoints = baseRoutesHandlers @@ -370,3 +371,25 @@ func (group *accountsGroup) getESDTTokens(c *gin.Context) { c.JSON(http.StatusOK, tokens) } + +func (group *accountsGroup) isDataTrieMigrated(c *gin.Context) { + addr := c.Param("address") + if addr == "" { + shared.RespondWithValidationError(c, errors.ErrIsDataTrieMigrated, errors.ErrEmptyAddress) + return + } + + options, err := parseAccountQueryOptions(c) + if err != nil { + shared.RespondWithValidationError(c, errors.ErrIsDataTrieMigrated, err) + return + } + + isMigrated, err := group.facade.IsDataTrieMigrated(addr, options) + if err != nil { + shared.RespondWithInternalError(c, errors.ErrIsDataTrieMigrated, err) + return + } + + c.JSON(http.StatusOK, isMigrated) +} diff --git a/api/groups/baseAccountsGroup_test.go b/api/groups/baseAccountsGroup_test.go index 5439dce1..951628f8 100644 --- a/api/groups/baseAccountsGroup_test.go +++ b/api/groups/baseAccountsGroup_test.go @@ -841,3 +841,64 @@ func TestGetCodeHash_ReturnsSuccessfully(t *testing.T) { assert.Equal(t, expectedResponse, actualResponse) assert.Empty(t, actualResponse.Error) } + +func TestAccountsGroup_IsDataTrieMigrated(t *testing.T) { + t.Parallel() + + t.Run("should return error when facade returns error", func(t *testing.T) { + t.Parallel() + + expectedErr := errors.New("internal err") + facade := &mock.FacadeStub{ + IsDataTrieMigratedCalled: func(_ string, _ common.AccountQueryOptions) (*data.GenericAPIResponse, error) { + return nil, expectedErr + }, + } + addressGroup, err := groups.NewAccountsGroup(facade) + require.NoError(t, err) + ws := startProxyServer(addressGroup, addressPath) + + reqAddress := "test" + req, _ := http.NewRequest("GET", fmt.Sprintf("/address/%s/is-data-trie-migrated", reqAddress), nil) + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + + response := &data.GenericAPIResponse{} + loadResponse(resp.Body, &response) + + assert.Equal(t, http.StatusInternalServerError, resp.Code) + assert.True(t, strings.Contains(response.Error, expectedErr.Error())) + }) + + t.Run("should return successfully", func(t *testing.T) { + t.Parallel() + + expectedResponse := &data.GenericAPIResponse{ + Data: map[string]interface{}{ + "isMigrated": "true", + }, + Error: "", + Code: data.ReturnCodeSuccess, + } + facade := &mock.FacadeStub{ + IsDataTrieMigratedCalled: func(_ string, _ common.AccountQueryOptions) (*data.GenericAPIResponse, error) { + return expectedResponse, nil + }, + } + addressGroup, err := groups.NewAccountsGroup(facade) + require.NoError(t, err) + ws := startProxyServer(addressGroup, addressPath) + + reqAddress := "test" + req, _ := http.NewRequest("GET", fmt.Sprintf("/address/%s/is-data-trie-migrated", reqAddress), nil) + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + + actualResponse := &data.GenericAPIResponse{} + loadResponse(resp.Body, &actualResponse) + + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, expectedResponse, actualResponse) + assert.Empty(t, actualResponse.Error) + }) +} diff --git a/api/groups/interface.go b/api/groups/interface.go index 1af0fac5..36f0438b 100644 --- a/api/groups/interface.go +++ b/api/groups/interface.go @@ -23,6 +23,7 @@ type AccountsFacadeHandler interface { GetESDTsRoles(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) GetESDTNftTokenData(address string, key string, nonce uint64, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) GetNFTTokenIDsRegisteredByAddress(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) + IsDataTrieMigrated(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) } // BlockFacadeHandler interface defines methods that can be used from the facade diff --git a/api/mock/facadeStub.go b/api/mock/facadeStub.go index c8c1355c..e0ecefc3 100644 --- a/api/mock/facadeStub.go +++ b/api/mock/facadeStub.go @@ -77,6 +77,7 @@ type FacadeStub struct { GetTriesStatisticsCalled func(shardID uint32) (*data.TrieStatisticsAPIResponse, error) GetEpochStartDataCalled func(epoch uint32, shardID uint32) (*data.GenericAPIResponse, error) GetCodeHashCalled func(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) + IsDataTrieMigratedCalled func(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) } // GetProof - @@ -526,6 +527,15 @@ func (f *FacadeStub) GetCodeHash(address string, options common.AccountQueryOpti return f.GetCodeHashCalled(address, options) } +// IsDataTrieMigrated - +func (f *FacadeStub) IsDataTrieMigrated(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) { + if f.IsDataTrieMigratedCalled != nil { + return f.IsDataTrieMigratedCalled(address, options) + } + + return &data.GenericAPIResponse{}, 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 89d384d2..8a640c1e 100644 --- a/cmd/proxy/config/apiConfig/v1_0.toml +++ b/cmd/proxy/config/apiConfig/v1_0.toml @@ -42,7 +42,8 @@ Routes = [ { Name = "/:address/registered-nfts", Open = true, Secured = false, RateLimit = 0 }, { Name = "/:address/nft/:tokenIdentifier/nonce/:nonce", Open = true, Secured = false, RateLimit = 0 }, { Name = "/:address/shard", Open = true, Secured = false, RateLimit = 0 }, - { Name = "/:address/transactions", Open = true, Secured = false, RateLimit = 0 } + { Name = "/:address/transactions", Open = true, Secured = false, RateLimit = 0 }, + { Name = "/:address/is-data-trie-migrated", Open = true, Secured = false, RateLimit = 0 } ] [APIPackages.hyperblock] diff --git a/cmd/proxy/config/apiConfig/v_next.toml b/cmd/proxy/config/apiConfig/v_next.toml index d18d693c..34129485 100644 --- a/cmd/proxy/config/apiConfig/v_next.toml +++ b/cmd/proxy/config/apiConfig/v_next.toml @@ -42,7 +42,8 @@ Routes = [ { Name = "/:address/registered-nfts", Open = true, Secured = false, RateLimit = 0 }, { Name = "/:address/nft/:tokenIdentifier/nonce/:nonce", Open = true, Secured = false, RateLimit = 0 }, { Name = "/:address/shard", Open = true, Secured = false, RateLimit = 0 }, - { Name = "/:address/transactions", Open = true, Secured = false, RateLimit = 0 } + { Name = "/:address/transactions", Open = true, Secured = false, RateLimit = 0 }, + { Name = "/:address/is-data-trie-migrated", Open = true, Secured = false, RateLimit = 0 } ] [APIPackages.hyperblock] diff --git a/facade/baseFacade.go b/facade/baseFacade.go index 131cf5f9..ad9f606c 100644 --- a/facade/baseFacade.go +++ b/facade/baseFacade.go @@ -515,3 +515,8 @@ func (epf *ProxyFacade) GetEpochStartData(epoch uint32, shardID uint32) (*data.G func (epf *ProxyFacade) GetInternalStartOfEpochValidatorsInfo(epoch uint32) (*data.ValidatorsInfoApiResponse, error) { return epf.blockProc.GetInternalStartOfEpochValidatorsInfo(epoch) } + +// 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/interface.go b/facade/interface.go index 7de8652d..48ab54c0 100644 --- a/facade/interface.go +++ b/facade/interface.go @@ -30,6 +30,7 @@ type AccountProcessor interface { GetESDTNftTokenData(address string, key string, nonce uint64, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) GetNFTTokenIDsRegisteredByAddress(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) GetCodeHash(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) + IsDataTrieMigrated(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) } // TransactionProcessor defines what a transaction request processor should do diff --git a/facade/mock/accountProccessorStub.go b/facade/mock/accountProccessorStub.go index 20b2bb28..a2f35211 100644 --- a/facade/mock/accountProccessorStub.go +++ b/facade/mock/accountProccessorStub.go @@ -20,6 +20,7 @@ type AccountProcessorStub struct { GetKeyValuePairsCalled func(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) GetESDTsRolesCalled func(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) GetCodeHashCalled func(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) + IsDataTrieMigratedCalled func(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) } // GetKeyValuePairs - @@ -90,3 +91,12 @@ func (aps *AccountProcessorStub) GetCodeHash(address string, options common.Acco func (aps *AccountProcessorStub) ValidatorStatistics() (map[string]*data.ValidatorApiResponse, error) { return aps.ValidatorStatisticsCalled() } + +// IsDataTrieMigrated -- +func (aps *AccountProcessorStub) IsDataTrieMigrated(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) { + if aps.IsDataTrieMigratedCalled != nil { + return aps.IsDataTrieMigratedCalled(address, options) + } + + return &data.GenericAPIResponse{}, nil +} diff --git a/process/accountProcessor.go b/process/accountProcessor.go index 296c3d2f..9efcd1d1 100644 --- a/process/accountProcessor.go +++ b/process/accountProcessor.go @@ -390,3 +390,34 @@ func (ap *AccountProcessor) getObserversForAddress(address string) ([]*data.Node func (ap *AccountProcessor) GetBaseProcessor() Processor { return ap.proc } + +// IsDataTrieMigrated returns true if the data trie for the given address is migrated +func (ap *AccountProcessor) IsDataTrieMigrated(address string, options common.AccountQueryOptions) (*data.GenericAPIResponse, error) { + observers, err := ap.getObserversForAddress(address) + if err != nil { + return nil, err + } + + for _, observer := range observers { + apiResponse := data.GenericAPIResponse{} + apiPath := AddressPath + address + "/is-data-trie-migrated" + apiPath = common.BuildUrlWithAccountQueryOptions(apiPath, options) + respCode, err := ap.proc.CallGetRestEndPoint(observer.Address, apiPath, &apiResponse) + if err == nil || respCode == http.StatusBadRequest || respCode == http.StatusInternalServerError { + log.Info("is data trie migrated", + "address", address, + "shard ID", observer.ShardId, + "observer", observer.Address, + "http code", respCode) + if apiResponse.Error != "" { + return nil, errors.New(apiResponse.Error) + } + + return &apiResponse, nil + } + + log.Error("account is data trie migrated", "observer", observer.Address, "address", address, "error", err.Error()) + } + + return nil, ErrSendingRequest +} diff --git a/process/accountProcessor_test.go b/process/accountProcessor_test.go index a23d48b9..eeef7123 100644 --- a/process/accountProcessor_test.go +++ b/process/accountProcessor_test.go @@ -486,3 +486,81 @@ func TestAccountProcessor_GetCodeHash(t *testing.T) { require.NoError(t, err) require.Equal(t, "code-hash", response.Data.([]string)[0]) } + +func TestAccountProcessor_IsDataTrieMigrated(t *testing.T) { + t.Parallel() + + t.Run("should return error when cannot get observers", func(t *testing.T) { + ap, _ := process.NewAccountProcessor( + &mock.ProcessorStub{ + GetObserversCalled: func(_ uint32) ([]*data.NodeData, error) { + return nil, errors.New("cannot get observers") + }, + }, + &mock.PubKeyConverterMock{}, + &mock.ElasticSearchConnectorMock{}, + ) + + result, err := ap.IsDataTrieMigrated("address", common.AccountQueryOptions{}) + require.Error(t, err) + require.Nil(t, result) + }) + + t.Run("should return error when cannot get data trie migrated", func(t *testing.T) { + ap, _ := process.NewAccountProcessor( + &mock.ProcessorStub{ + GetObserversCalled: func(_ uint32) ([]*data.NodeData, error) { + return []*data.NodeData{ + { + Address: "observer0", + ShardId: 0, + }, + }, nil + }, + + CallGetRestEndPointCalled: func(_ string, _ string, _ interface{}) (int, error) { + return 0, errors.New("cannot get data trie migrated") + }, + ComputeShardIdCalled: func(_ []byte) (uint32, error) { + return 0, nil + }, + }, + &mock.PubKeyConverterMock{}, + &mock.ElasticSearchConnectorMock{}, + ) + + result, err := ap.IsDataTrieMigrated("DEADBEEF", common.AccountQueryOptions{}) + require.Error(t, err) + require.Nil(t, result) + }) + + t.Run("should work", func(t *testing.T) { + ap, _ := process.NewAccountProcessor( + &mock.ProcessorStub{ + GetObserversCalled: func(_ uint32) ([]*data.NodeData, error) { + return []*data.NodeData{ + { + Address: "observer0", + ShardId: 0, + }, + }, nil + }, + + CallGetRestEndPointCalled: func(_ string, _ string, value interface{}) (int, error) { + dataTrieMigratedResponse := value.(*data.GenericAPIResponse) + dataTrieMigratedResponse.Data = true + return 0, nil + }, + ComputeShardIdCalled: func(_ []byte) (uint32, error) { + return 0, nil + }, + }, + &mock.PubKeyConverterMock{}, + &mock.ElasticSearchConnectorMock{}, + ) + + result, err := ap.IsDataTrieMigrated("DEADBEEF", common.AccountQueryOptions{}) + require.NoError(t, err) + require.True(t, result.Data.(bool)) + }) +}