diff --git a/server/factory/interface.go b/server/factory/interface.go index 868e30cb..916da7fd 100644 --- a/server/factory/interface.go +++ b/server/factory/interface.go @@ -25,8 +25,7 @@ type NetworkProvider interface { GetBlockByNonce(nonce uint64) (*api.Block, error) GetBlockByHash(hash string) (*api.Block, error) GetAccount(address string) (*resources.AccountOnBlock, error) - GetAccountNativeBalance(address string, options resources.AccountQueryOptions) (*resources.AccountOnBlock, error) - GetAccountESDTBalance(address string, tokenIdentifier string, options resources.AccountQueryOptions) (*resources.AccountESDTBalance, error) + GetAccountBalance(address string, tokenIdentifier string, options resources.AccountQueryOptions) (*resources.AccountBalanceOnBlock, error) IsAddressObserved(address string) (bool, error) ComputeShardIdOfPubKey(pubkey []byte) uint32 ConvertPubKeyToAddress(pubkey []byte) string diff --git a/server/provider/accounts.go b/server/provider/accounts.go index 8b5915b1..5e4e8be5 100644 --- a/server/provider/accounts.go +++ b/server/provider/accounts.go @@ -1,15 +1,10 @@ package provider import ( - "fmt" - "strconv" - "strings" - + "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-rosetta/server/resources" ) -// TODO: Merge the methods in this file into a single method, e.g. GetAccountWithBalance(address, tokenIdentifier, options), where tokenIdentifier can be the native token or an ESDT. - // GetAccount gets an account by address func (provider *networkProvider) GetAccount(address string) (*resources.AccountOnBlock, error) { url := buildUrlGetAccount(address) @@ -24,17 +19,26 @@ func (provider *networkProvider) GetAccount(address string) (*resources.AccountO log.Trace("GetAccount()", "address", data.Account.Address, - "balance", data.Account.Balance, + "native balance", data.Account.Balance, + "nonce", data.Account.Nonce, "block", data.BlockCoordinates.Nonce, "blockHash", data.BlockCoordinates.Hash, - "blockRootHash", data.BlockCoordinates.RootHash, ) return data, nil } // GetAccountNativeBalance gets the native balance by address -func (provider *networkProvider) GetAccountNativeBalance(address string, options resources.AccountQueryOptions) (*resources.AccountOnBlock, error) { +func (provider *networkProvider) GetAccountBalance(address string, tokenIdentifier string, options resources.AccountQueryOptions) (*resources.AccountBalanceOnBlock, error) { + isNativeBalance := tokenIdentifier == provider.nativeCurrency.Symbol + if isNativeBalance { + return provider.getNativeBalance(address, options) + } + + return provider.getCustomTokenBalance(address, tokenIdentifier, options) +} + +func (provider *networkProvider) getNativeBalance(address string, options resources.AccountQueryOptions) (*resources.AccountBalanceOnBlock, error) { url := buildUrlGetAccountNativeBalance(address, options) response := &resources.AccountApiResponse{} @@ -45,31 +49,28 @@ func (provider *networkProvider) GetAccountNativeBalance(address string, options data := &response.Data - log.Trace("GetAccountNativeBalance()", + log.Trace("networkProvider.getNativeBalance()", "address", address, "balance", data.Account.Balance, "nonce", data.Account.Nonce, "block", data.BlockCoordinates.Nonce, "blockHash", data.BlockCoordinates.Hash, - "blockRootHash", data.BlockCoordinates.RootHash, ) - return data, nil + // Here, we also return the account nonce (directly available). + return &resources.AccountBalanceOnBlock{ + Balance: data.Account.Balance, + Nonce: core.OptionalUint64{Value: data.Account.Nonce, HasValue: true}, + BlockCoordinates: data.BlockCoordinates, + }, nil } -// GetAccountESDTBalance gets the ESDT balance by address and tokenIdentifier -// TODO: Return nonce for ESDT, as well (an additional request might be needed). -func (provider *networkProvider) GetAccountESDTBalance(address string, tokenIdentifier string, options resources.AccountQueryOptions) (*resources.AccountESDTBalance, error) { - tokenIdentifierParts, err := parseExtendedIdentifierParts(tokenIdentifier) +func (provider *networkProvider) getCustomTokenBalance(address string, tokenIdentifier string, options resources.AccountQueryOptions) (*resources.AccountBalanceOnBlock, error) { + url, err := decideCustomTokenBalanceUrl(address, tokenIdentifier, options) if err != nil { return nil, err } - url := buildUrlGetAccountESDTBalance(address, tokenIdentifier, options) - if tokenIdentifierParts.nonce > 0 { - url = buildUrlGetAccountNFTBalance(address, fmt.Sprintf("%s-%s", tokenIdentifierParts.ticker, tokenIdentifierParts.randomSequence), tokenIdentifierParts.nonce, options) - } - response := &resources.AccountESDTBalanceApiResponse{} err = provider.getResource(url, response) @@ -79,51 +80,31 @@ func (provider *networkProvider) GetAccountESDTBalance(address string, tokenIden data := &response.Data - log.Trace("GetAccountESDTBalance()", + log.Trace("networkProvider.getCustomTokenBalance()", "address", address, "tokenIdentifier", tokenIdentifier, "balance", data.TokenData.Balance, "block", data.BlockCoordinates.Nonce, "blockHash", data.BlockCoordinates.Hash, - "blockRootHash", data.BlockCoordinates.RootHash, ) - return &resources.AccountESDTBalance{ + // Here, we do not return the account nonce (not available without a second API call). + return &resources.AccountBalanceOnBlock{ Balance: data.TokenData.Balance, BlockCoordinates: data.BlockCoordinates, }, nil } -type tokenIdentifierParts struct { - ticker string - randomSequence string - nonce uint64 -} - -func parseExtendedIdentifierParts(tokenIdentifier string) (*tokenIdentifierParts, error) { - parts := strings.Split(tokenIdentifier, "-") - - if len(parts) == 2 { - return &tokenIdentifierParts{ - ticker: parts[0], - randomSequence: parts[1], - nonce: 0, - }, nil +func decideCustomTokenBalanceUrl(address string, tokenIdentifier string, options resources.AccountQueryOptions) (string, error) { + tokenIdentifierParts, err := parseTokenIdentifierIntoParts(tokenIdentifier) + if err != nil { + return "", err } - if len(parts) == 3 { - nonceHex := parts[2] - nonce, err := strconv.ParseUint(nonceHex, 16, 64) - if err != nil { - return nil, newErrCannotParseTokenIdentifier(tokenIdentifier, err) - } - - return &tokenIdentifierParts{ - ticker: parts[0], - randomSequence: parts[1], - nonce: nonce, - }, nil + isFungible := tokenIdentifierParts.nonce == 0 + if isFungible { + return buildUrlGetAccountFungibleTokenBalance(address, tokenIdentifier, options), nil } - return nil, newErrCannotParseTokenIdentifier(tokenIdentifier, nil) + return buildUrlGetAccountNonFungibleTokenBalance(address, tokenIdentifierParts.tickerWithRandomSequence, tokenIdentifierParts.nonce, options), nil } diff --git a/server/provider/accounts_test.go b/server/provider/accounts_test.go index f82102aa..c4adf40a 100644 --- a/server/provider/accounts_test.go +++ b/server/provider/accounts_test.go @@ -53,7 +53,7 @@ func TestNetworkProvider_GetAccount(t *testing.T) { }) } -func TestNetworkProvider_GetAccountNativeBalance(t *testing.T) { +func TestNetworkProvider_GetAccountBalance(t *testing.T) { observerFacade := testscommon.NewObserverFacadeMock() args := createDefaultArgsNewNetworkProvider() args.ObserverFacade = observerFacade @@ -64,12 +64,13 @@ func TestNetworkProvider_GetAccountNativeBalance(t *testing.T) { optionsOnFinal := resources.NewAccountQueryOptionsOnFinalBlock() - t.Run("with success", func(t *testing.T) { + t.Run("native balance, with success", func(t *testing.T) { observerFacade.MockNextError = nil observerFacade.MockGetResponse = resources.AccountApiResponse{ Data: resources.AccountOnBlock{ Account: resources.Account{ Balance: "1", + Nonce: 42, }, BlockCoordinates: resources.BlockCoordinates{ Nonce: 1000, @@ -77,38 +78,60 @@ func TestNetworkProvider_GetAccountNativeBalance(t *testing.T) { }, } - accountBalance, err := provider.GetAccountNativeBalance(testscommon.TestAddressAlice, optionsOnFinal) + accountBalance, err := provider.GetAccountBalance(testscommon.TestAddressAlice, "XeGLD", optionsOnFinal) require.Nil(t, err) - require.Equal(t, "1", accountBalance.Account.Balance) + require.Equal(t, "1", accountBalance.Balance) + require.Equal(t, uint64(42), accountBalance.Nonce.Value) require.Equal(t, uint64(1000), accountBalance.BlockCoordinates.Nonce) require.Equal(t, args.ObserverUrl, observerFacade.RecordedBaseUrl) require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th?onFinalBlock=true", observerFacade.RecordedPath) }) - t.Run("with error", func(t *testing.T) { + t.Run("native balance, with error", func(t *testing.T) { observerFacade.MockNextError = errors.New("arbitrary error") observerFacade.MockGetResponse = nil - accountBalance, err := provider.GetAccountNativeBalance(testscommon.TestAddressAlice, optionsOnFinal) + accountBalance, err := provider.GetAccountBalance(testscommon.TestAddressAlice, "XeGLD", optionsOnFinal) require.ErrorIs(t, err, errCannotGetAccount) require.Nil(t, accountBalance) require.Equal(t, args.ObserverUrl, observerFacade.RecordedBaseUrl) require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th?onFinalBlock=true", observerFacade.RecordedPath) }) -} -func TestNetworkProvider_GetAccountESDTBalance(t *testing.T) { - observerFacade := testscommon.NewObserverFacadeMock() - args := createDefaultArgsNewNetworkProvider() - args.ObserverFacade = observerFacade + t.Run("fungible token, with success", func(t *testing.T) { + observerFacade.MockNextError = nil + observerFacade.MockGetResponse = resources.AccountESDTBalanceApiResponse{ + Data: resources.AccountESDTBalanceApiResponsePayload{ + TokenData: resources.AccountESDTTokenData{ + Balance: "1", + }, + BlockCoordinates: resources.BlockCoordinates{ + Nonce: 1000, + }, + }, + } - provider, err := NewNetworkProvider(args) - require.Nil(t, err) - require.NotNil(t, provider) + accountBalance, err := provider.GetAccountBalance(testscommon.TestAddressAlice, "ABC-abcdef", optionsOnFinal) + require.Nil(t, err) + require.Equal(t, "1", accountBalance.Balance) + require.False(t, accountBalance.Nonce.HasValue) + require.Equal(t, uint64(1000), accountBalance.BlockCoordinates.Nonce) + require.Equal(t, args.ObserverUrl, observerFacade.RecordedBaseUrl) + require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/esdt/ABC-abcdef?onFinalBlock=true", observerFacade.RecordedPath) + }) - optionsOnFinal := resources.NewAccountQueryOptionsOnFinalBlock() + t.Run("fungible token, with error", func(t *testing.T) { + observerFacade.MockNextError = errors.New("arbitrary error") + observerFacade.MockGetResponse = nil - t.Run("with success", func(t *testing.T) { + accountBalance, err := provider.GetAccountBalance(testscommon.TestAddressAlice, "ABC-abcdef", optionsOnFinal) + require.ErrorIs(t, err, errCannotGetAccount) + require.Nil(t, accountBalance) + require.Equal(t, args.ObserverUrl, observerFacade.RecordedBaseUrl) + require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/esdt/ABC-abcdef?onFinalBlock=true", observerFacade.RecordedPath) + }) + + t.Run("non-fungible token, with success", func(t *testing.T) { observerFacade.MockNextError = nil observerFacade.MockGetResponse = resources.AccountESDTBalanceApiResponse{ Data: resources.AccountESDTBalanceApiResponsePayload{ @@ -121,22 +144,48 @@ func TestNetworkProvider_GetAccountESDTBalance(t *testing.T) { }, } - accountBalance, err := provider.GetAccountESDTBalance(testscommon.TestAddressAlice, "ABC-abcdef", optionsOnFinal) + accountBalance, err := provider.GetAccountBalance(testscommon.TestAddressAlice, "ABC-abcdef-0a", optionsOnFinal) require.Nil(t, err) require.Equal(t, "1", accountBalance.Balance) + require.False(t, accountBalance.Nonce.HasValue) require.Equal(t, uint64(1000), accountBalance.BlockCoordinates.Nonce) require.Equal(t, args.ObserverUrl, observerFacade.RecordedBaseUrl) - require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/esdt/ABC-abcdef?onFinalBlock=true", observerFacade.RecordedPath) + require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/nft/ABC-abcdef/nonce/10?onFinalBlock=true", observerFacade.RecordedPath) }) - t.Run("with error", func(t *testing.T) { + t.Run("non-fungible token, with error", func(t *testing.T) { observerFacade.MockNextError = errors.New("arbitrary error") observerFacade.MockGetResponse = nil - accountBalance, err := provider.GetAccountESDTBalance(testscommon.TestAddressAlice, "ABC-abcdef", optionsOnFinal) + accountBalance, err := provider.GetAccountBalance(testscommon.TestAddressAlice, "ABC-abcdef-0a", optionsOnFinal) require.ErrorIs(t, err, errCannotGetAccount) require.Nil(t, accountBalance) require.Equal(t, args.ObserverUrl, observerFacade.RecordedBaseUrl) - require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/esdt/ABC-abcdef?onFinalBlock=true", observerFacade.RecordedPath) + require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/nft/ABC-abcdef/nonce/10?onFinalBlock=true", observerFacade.RecordedPath) + }) +} + +func TestDecideCustomTokenBalanceUrl(t *testing.T) { + args := createDefaultArgsNewNetworkProvider() + provider, err := NewNetworkProvider(args) + require.Nil(t, err) + require.NotNil(t, provider) + + t.Run("for fungible", func(t *testing.T) { + url, err := decideCustomTokenBalanceUrl(testscommon.TestAddressCarol, "ABC-abcdef", resources.AccountQueryOptions{}) + require.Nil(t, err) + require.Equal(t, "/address/erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8/esdt/ABC-abcdef", url) + }) + + t.Run("for non-fungible", func(t *testing.T) { + url, err := decideCustomTokenBalanceUrl(testscommon.TestAddressCarol, "ABC-abcdef-0a", resources.AccountQueryOptions{}) + require.Nil(t, err) + require.Equal(t, "/address/erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8/nft/ABC-abcdef/nonce/10", url) + }) + + t.Run("with error", func(t *testing.T) { + url, err := decideCustomTokenBalanceUrl(testscommon.TestAddressCarol, "ABC", resources.AccountQueryOptions{}) + require.ErrorIs(t, err, errCannotParseTokenIdentifier) + require.Empty(t, url) }) } diff --git a/server/provider/tokenIdentifierParts.go b/server/provider/tokenIdentifierParts.go new file mode 100644 index 00000000..2548a132 --- /dev/null +++ b/server/provider/tokenIdentifierParts.go @@ -0,0 +1,46 @@ +package provider + +import ( + "fmt" + "strconv" + "strings" +) + +type tokenIdentifierParts struct { + ticker string + randomSequence string + tickerWithRandomSequence string + nonce uint64 +} + +func parseTokenIdentifierIntoParts(tokenIdentifier string) (*tokenIdentifierParts, error) { + parts := strings.Split(tokenIdentifier, "-") + + // Fungible tokens + if len(parts) == 2 { + return &tokenIdentifierParts{ + ticker: parts[0], + randomSequence: parts[1], + tickerWithRandomSequence: tokenIdentifier, + nonce: 0, + }, nil + } + + // Non-fungible tokens + if len(parts) == 3 { + nonceHex := parts[2] + nonce, err := strconv.ParseUint(nonceHex, 16, 64) + if err != nil { + return nil, newErrCannotParseTokenIdentifier(tokenIdentifier, err) + } + + return &tokenIdentifierParts{ + ticker: parts[0], + randomSequence: parts[1], + tickerWithRandomSequence: fmt.Sprintf("%s-%s", parts[0], parts[1]), + nonce: nonce, + }, nil + } + + return nil, newErrCannotParseTokenIdentifier(tokenIdentifier, nil) +} diff --git a/server/provider/tokenIdentifierParts_test.go b/server/provider/tokenIdentifierParts_test.go new file mode 100644 index 00000000..cd521917 --- /dev/null +++ b/server/provider/tokenIdentifierParts_test.go @@ -0,0 +1,35 @@ +package provider + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseTokenIdentifierIntoParts(t *testing.T) { + t.Run("with fungible token", func(t *testing.T) { + parts, err := parseTokenIdentifierIntoParts("ROSETTA-2c0a37") + require.Nil(t, err) + require.NotNil(t, parts) + require.Equal(t, "ROSETTA", parts.ticker) + require.Equal(t, "2c0a37", parts.randomSequence) + require.Equal(t, "ROSETTA-2c0a37", parts.tickerWithRandomSequence) + require.Equal(t, uint64(0), parts.nonce) + }) + + t.Run("with non-fungible token", func(t *testing.T) { + parts, err := parseTokenIdentifierIntoParts("EXAMPLE-453bec-0a") + require.Nil(t, err) + require.NotNil(t, parts) + require.Equal(t, "EXAMPLE", parts.ticker) + require.Equal(t, "453bec", parts.randomSequence) + require.Equal(t, "EXAMPLE-453bec", parts.tickerWithRandomSequence) + require.Equal(t, uint64(10), parts.nonce) + }) + + t.Run("with invalid custom token identifier", func(t *testing.T) { + parts, err := parseTokenIdentifierIntoParts("token") + require.ErrorIs(t, err, errCannotParseTokenIdentifier) + require.Nil(t, parts) + }) +} diff --git a/server/provider/urls.go b/server/provider/urls.go index 23cabdd8..da3d8b5d 100644 --- a/server/provider/urls.go +++ b/server/provider/urls.go @@ -15,8 +15,8 @@ var ( urlPathGetGenesisBalances = "/network/genesis-balances" urlPathGetAccount = "/address/%s" urlPathGetAccountNativeBalance = "/address/%s" - urlPathGetAccountESDTBalance = "/address/%s/esdt/%s" - urlPathGetAccountNFTBalance = "/address/%s/nft/%s/nonce/%d" + urlPathGetAccountFungibleTokenBalance = "/address/%s/esdt/%s" + urlPathGetAccountNonFungibleTokenBalance = "/address/%s/nft/%s/nonce/%d" urlParameterAccountQueryOptionsOnFinalBlock = "onFinalBlock" urlParameterAccountQueryOptionsBlockNonce = "blockNonce" urlParameterAccountQueryOptionsBlockHash = "blockHash" @@ -35,12 +35,12 @@ func buildUrlGetAccountNativeBalance(address string, options resources.AccountQu return buildUrlWithAccountQueryOptions(fmt.Sprintf(urlPathGetAccountNativeBalance, address), options) } -func buildUrlGetAccountESDTBalance(address string, tokenIdentifier string, options resources.AccountQueryOptions) string { - return buildUrlWithAccountQueryOptions(fmt.Sprintf(urlPathGetAccountESDTBalance, address, tokenIdentifier), options) +func buildUrlGetAccountFungibleTokenBalance(address string, tokenIdentifier string, options resources.AccountQueryOptions) string { + return buildUrlWithAccountQueryOptions(fmt.Sprintf(urlPathGetAccountFungibleTokenBalance, address, tokenIdentifier), options) } -func buildUrlGetAccountNFTBalance(address string, tokenIdentifier string, nonce uint64, options resources.AccountQueryOptions) string { - return buildUrlWithAccountQueryOptions(fmt.Sprintf(urlPathGetAccountNFTBalance, address, tokenIdentifier, nonce), options) +func buildUrlGetAccountNonFungibleTokenBalance(address string, tokenIdentifier string, nonce uint64, options resources.AccountQueryOptions) string { + return buildUrlWithAccountQueryOptions(fmt.Sprintf(urlPathGetAccountNonFungibleTokenBalance, address, tokenIdentifier, nonce), options) } func buildUrlWithAccountQueryOptions(path string, options resources.AccountQueryOptions) string { diff --git a/server/provider/urls_test.go b/server/provider/urls_test.go index e0cdd04f..c20521c7 100644 --- a/server/provider/urls_test.go +++ b/server/provider/urls_test.go @@ -31,20 +31,38 @@ func TestBuildUrlGetAccountNativeBalance(t *testing.T) { require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", url) } -func TestBuildUrlGetAccountESDTBalance(t *testing.T) { +func TestBuildUrlGetAccountFungibleTokenBalance(t *testing.T) { optionsOnFinal := resources.NewAccountQueryOptionsOnFinalBlock() optionsAtBlockNonce := resources.NewAccountQueryOptionsWithBlockNonce(7) optionsAtBlockHash := resources.NewAccountQueryOptionsWithBlockHash([]byte{0xaa, 0xbb, 0xcc, 0xdd}) - url := buildUrlGetAccountESDTBalance(testscommon.TestAddressAlice, "ABC-abcdef", optionsOnFinal) + url := buildUrlGetAccountFungibleTokenBalance(testscommon.TestAddressAlice, "ABC-abcdef", optionsOnFinal) require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/esdt/ABC-abcdef?onFinalBlock=true", url) - url = buildUrlGetAccountESDTBalance(testscommon.TestAddressAlice, "ABC-abcdef", optionsAtBlockNonce) + url = buildUrlGetAccountFungibleTokenBalance(testscommon.TestAddressAlice, "ABC-abcdef", optionsAtBlockNonce) require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/esdt/ABC-abcdef?blockNonce=7", url) - url = buildUrlGetAccountESDTBalance(testscommon.TestAddressAlice, "ABC-abcdef", optionsAtBlockHash) + url = buildUrlGetAccountFungibleTokenBalance(testscommon.TestAddressAlice, "ABC-abcdef", optionsAtBlockHash) require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/esdt/ABC-abcdef?blockHash=aabbccdd", url) - url = buildUrlGetAccountESDTBalance(testscommon.TestAddressAlice, "ABC-abcdef", resources.AccountQueryOptions{}) + url = buildUrlGetAccountFungibleTokenBalance(testscommon.TestAddressAlice, "ABC-abcdef", resources.AccountQueryOptions{}) require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/esdt/ABC-abcdef", url) } + +func TestBuildUrlGetAccountNonFungibleTokenBalance(t *testing.T) { + optionsOnFinal := resources.NewAccountQueryOptionsOnFinalBlock() + optionsAtBlockNonce := resources.NewAccountQueryOptionsWithBlockNonce(7) + optionsAtBlockHash := resources.NewAccountQueryOptionsWithBlockHash([]byte{0xaa, 0xbb, 0xcc, 0xdd}) + + url := buildUrlGetAccountNonFungibleTokenBalance(testscommon.TestAddressAlice, "ABC-abcdef", 10, optionsOnFinal) + require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/nft/ABC-abcdef/nonce/10?onFinalBlock=true", url) + + url = buildUrlGetAccountNonFungibleTokenBalance(testscommon.TestAddressAlice, "ABC-abcdef", 10, optionsAtBlockNonce) + require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/nft/ABC-abcdef/nonce/10?blockNonce=7", url) + + url = buildUrlGetAccountNonFungibleTokenBalance(testscommon.TestAddressAlice, "ABC-abcdef", 10, optionsAtBlockHash) + require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/nft/ABC-abcdef/nonce/10?blockHash=aabbccdd", url) + + url = buildUrlGetAccountNonFungibleTokenBalance(testscommon.TestAddressAlice, "ABC-abcdef", 10, resources.AccountQueryOptions{}) + require.Equal(t, "/address/erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th/nft/ABC-abcdef/nonce/10", url) +} diff --git a/server/resources/accounts.go b/server/resources/accounts.go index 002cff05..877a5d45 100644 --- a/server/resources/accounts.go +++ b/server/resources/accounts.go @@ -1,6 +1,10 @@ package resources -// AccountApiResponse defines an account resource +import ( + "github.com/multiversx/mx-chain-core-go/core" +) + +// AccountApiResponse is an API resource type AccountApiResponse struct { resourceApiResponse Data AccountOnBlock `json:"data"` @@ -14,13 +18,12 @@ type AccountOnBlock struct { // Account defines an account resource type Account struct { - Address string `json:"address"` - Nonce uint64 `json:"nonce"` - Balance string `json:"balance"` - Username string `json:"username"` + Address string `json:"address"` + Nonce uint64 `json:"nonce"` + Balance string `json:"balance"` } -// AccountESDTBalanceApiResponse defines an account resource +// AccountESDTBalanceApiResponse is an API resource type AccountESDTBalanceApiResponse struct { resourceApiResponse Data AccountESDTBalanceApiResponsePayload `json:"data"` @@ -32,15 +35,15 @@ type AccountESDTBalanceApiResponsePayload struct { BlockCoordinates BlockCoordinates `json:"blockInfo"` } -// AccountESDTTokenData defines an account resource +// AccountESDTTokenData is an API resource type AccountESDTTokenData struct { Identifier string `json:"tokenIdentifier"` Balance string `json:"balance"` - Properties string `json:"properties"` } -// AccountESDTBalance defines an account resource -type AccountESDTBalance struct { +// AccountBalanceOnBlock defines an account resource +type AccountBalanceOnBlock struct { Balance string + Nonce core.OptionalUint64 BlockCoordinates BlockCoordinates } diff --git a/server/resources/resources.go b/server/resources/resources.go index 1624f0fd..e2572a01 100644 --- a/server/resources/resources.go +++ b/server/resources/resources.go @@ -28,7 +28,6 @@ type Currency struct { // BlockCoordinates is an API resource type BlockCoordinates struct { - Nonce uint64 `json:"nonce"` - Hash string `json:"hash"` - RootHash string `json:"rootHash"` + Nonce uint64 `json:"nonce"` + Hash string `json:"hash"` } diff --git a/server/services/accountService.go b/server/services/accountService.go index 814280db..153d32b7 100644 --- a/server/services/accountService.go +++ b/server/services/accountService.go @@ -5,15 +5,8 @@ import ( "github.com/coinbase/rosetta-sdk-go/server" "github.com/coinbase/rosetta-sdk-go/types" - "github.com/multiversx/mx-chain-rosetta/server/resources" ) -type accountOnBlock struct { - balance *types.Amount - nonce uint64 - blockCoordinates resources.BlockCoordinates -} - type accountService struct { provider NetworkProvider extension *networkProviderExtension @@ -63,49 +56,27 @@ func (service *accountService) AccountBalance( return nil, service.errFactory.newErr(ErrNotImplemented) } - account, err := service.getAccountOnBlock(address, currencySymbol, options) + accountBalanceOnBlock, err := service.provider.GetAccountBalance(address, currencySymbol, options) if err != nil { return nil, service.errFactory.newErrWithOriginal(ErrUnableToGetAccount, err) } - response := &types.AccountBalanceResponse{ - BlockIdentifier: accountBlockCoordinatesToIdentifier(account.blockCoordinates), - Balances: []*types.Amount{account.balance}, - Metadata: objectsMap{ - "nonce": account.nonce, - }, - } - - return response, nil -} + blockIdentifier := accountBlockCoordinatesToIdentifier(accountBalanceOnBlock.BlockCoordinates) + amount := service.extension.valueToAmount(accountBalanceOnBlock.Balance, currencySymbol) + metadata := objectsMap{} -func (service *accountService) getAccountOnBlock(address string, currencySymbol string, options resources.AccountQueryOptions) (accountOnBlock, error) { - isForNative := currencySymbol == service.getNativeSymbol() - if isForNative { - accountBalance, err := service.provider.GetAccountNativeBalance(address, options) - if err != nil { - return accountOnBlock{}, err - } - - amount := service.extension.valueToNativeAmount(accountBalance.Account.Balance) - return accountOnBlock{ - balance: amount, - nonce: accountBalance.Account.Nonce, - blockCoordinates: accountBalance.BlockCoordinates, - }, nil + // Currently, "nonce" is present only for native currency requests (for simplicity). + if accountBalanceOnBlock.Nonce.HasValue { + metadata["nonce"] = accountBalanceOnBlock.Nonce.Value } - accountBalance, err := service.provider.GetAccountESDTBalance(address, currencySymbol, options) - if err != nil { - return accountOnBlock{}, err + response := &types.AccountBalanceResponse{ + BlockIdentifier: blockIdentifier, + Balances: []*types.Amount{amount}, + Metadata: metadata, } - amount := service.extension.valueToCustomAmount(accountBalance.Balance, currencySymbol) - return accountOnBlock{ - balance: amount, - // TODO: return nonce, as well. - blockCoordinates: accountBalance.BlockCoordinates, - }, nil + return response, nil } func (service *accountService) getNativeSymbol() string { diff --git a/server/services/accountService_test.go b/server/services/accountService_test.go index 8bd85e00..88cc209f 100644 --- a/server/services/accountService_test.go +++ b/server/services/accountService_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/coinbase/rosetta-sdk-go/types" + "github.com/multiversx/mx-chain-core-go/core" "github.com/multiversx/mx-chain-rosetta/server/resources" "github.com/multiversx/mx-chain-rosetta/testscommon" "github.com/stretchr/testify/require" @@ -38,8 +39,8 @@ func TestAccountService_AccountBalance(t *testing.T) { AccountIdentifier: &types.AccountIdentifier{Address: "alice"}, } - networkProvider.MockAccountsNativeBalances["alice"] = &resources.Account{ - Nonce: 7, + networkProvider.MockAccountsNativeBalances["alice"] = &resources.AccountBalanceOnBlock{ + Nonce: core.OptionalUint64{Value: 7, HasValue: true}, Balance: "100", } networkProvider.MockNextAccountBlockCoordinates.Nonce = 42 @@ -64,8 +65,8 @@ func TestAccountService_AccountBalance(t *testing.T) { }, } - networkProvider.MockAccountsNativeBalances["alice"] = &resources.Account{ - Nonce: 7, + networkProvider.MockAccountsNativeBalances["alice"] = &resources.AccountBalanceOnBlock{ + Nonce: core.OptionalUint64{Value: 7, HasValue: true}, Balance: "1000", } networkProvider.MockNextAccountBlockCoordinates.Nonce = 42 @@ -78,7 +79,7 @@ func TestAccountService_AccountBalance(t *testing.T) { require.Equal(t, uint64(7), response.Metadata["nonce"]) }) - t.Run("with one custom currency (specified)", func(t *testing.T) { + t.Run("with one custom currency (fungible, specified)", func(t *testing.T) { request := &types.AccountBalanceRequest{ AccountIdentifier: &types.AccountIdentifier{Address: "alice"}, Currencies: []*types.Currency{ @@ -89,7 +90,7 @@ func TestAccountService_AccountBalance(t *testing.T) { }, } - networkProvider.MockAccountsESDTBalances["alice_FOO-abcdef"] = &resources.AccountESDTBalance{ + networkProvider.MockAccountsCustomBalances["alice_FOO-abcdef"] = &resources.AccountBalanceOnBlock{ Balance: "500", } networkProvider.MockNextAccountBlockCoordinates.Nonce = 42 @@ -101,6 +102,28 @@ func TestAccountService_AccountBalance(t *testing.T) { require.Equal(t, "FOO-abcdef", response.Balances[0].Currency.Symbol) }) + t.Run("with one custom currency (non-fungible, specified)", func(t *testing.T) { + request := &types.AccountBalanceRequest{ + AccountIdentifier: &types.AccountIdentifier{Address: "alice"}, + Currencies: []*types.Currency{ + { + Symbol: "FOO-abcdef-0a", + }, + }, + } + + networkProvider.MockAccountsCustomBalances["alice_FOO-abcdef-0a"] = &resources.AccountBalanceOnBlock{ + Balance: "1", + } + networkProvider.MockNextAccountBlockCoordinates.Nonce = 42 + networkProvider.MockNextAccountBlockCoordinates.Hash = "abba" + + response, err := service.AccountBalance(context.Background(), request) + require.Nil(t, err) + require.Equal(t, "1", response.Balances[0].Value) + require.Equal(t, "FOO-abcdef-0a", response.Balances[0].Currency.Symbol) + }) + t.Run("with more than 1 (custom or not) currencies", func(t *testing.T) { request := &types.AccountBalanceRequest{ AccountIdentifier: &types.AccountIdentifier{Address: "alice"}, @@ -116,10 +139,10 @@ func TestAccountService_AccountBalance(t *testing.T) { }, } - networkProvider.MockAccountsESDTBalances["alice_FOO-abcdef"] = &resources.AccountESDTBalance{ + networkProvider.MockAccountsCustomBalances["alice_FOO-abcdef"] = &resources.AccountBalanceOnBlock{ Balance: "500", } - networkProvider.MockAccountsESDTBalances["alice_BAR-abcdef"] = &resources.AccountESDTBalance{ + networkProvider.MockAccountsCustomBalances["alice_BAR-abcdef"] = &resources.AccountBalanceOnBlock{ Balance: "700", } networkProvider.MockNextAccountBlockCoordinates.Nonce = 42 diff --git a/server/services/interface.go b/server/services/interface.go index ed119014..271c46ad 100644 --- a/server/services/interface.go +++ b/server/services/interface.go @@ -24,8 +24,7 @@ type NetworkProvider interface { GetBlockByNonce(nonce uint64) (*api.Block, error) GetBlockByHash(hash string) (*api.Block, error) GetAccount(address string) (*resources.AccountOnBlock, error) - GetAccountNativeBalance(address string, options resources.AccountQueryOptions) (*resources.AccountOnBlock, error) - GetAccountESDTBalance(address string, tokenIdentifier string, options resources.AccountQueryOptions) (*resources.AccountESDTBalance, error) + GetAccountBalance(address string, tokenIdentifier string, options resources.AccountQueryOptions) (*resources.AccountBalanceOnBlock, error) IsAddressObserved(address string) (bool, error) ComputeShardIdOfPubKey(pubkey []byte) uint32 ConvertPubKeyToAddress(pubkey []byte) string diff --git a/server/services/networkProviderExtension.go b/server/services/networkProviderExtension.go index cfae1930..cfff7de6 100644 --- a/server/services/networkProviderExtension.go +++ b/server/services/networkProviderExtension.go @@ -16,6 +16,14 @@ func newNetworkProviderExtension(provider NetworkProvider) *networkProviderExten } } +func (extension *networkProviderExtension) valueToAmount(value string, currencySymbol string) *types.Amount { + if extension.isNativeCurrencySymbol(currencySymbol) { + return extension.valueToNativeAmount(value) + } + + return extension.valueToCustomAmount(value, currencySymbol) +} + func (extension *networkProviderExtension) valueToNativeAmount(value string) *types.Amount { return &types.Amount{ Value: value, diff --git a/server/services/networkProviderExtension_test.go b/server/services/networkProviderExtension_test.go index 5440ddd0..f81023d3 100644 --- a/server/services/networkProviderExtension_test.go +++ b/server/services/networkProviderExtension_test.go @@ -4,39 +4,72 @@ import ( "testing" "github.com/coinbase/rosetta-sdk-go/types" + "github.com/multiversx/mx-chain-rosetta/server/resources" "github.com/multiversx/mx-chain-rosetta/testscommon" "github.com/stretchr/testify/require" ) -func TestNetworkProviderExtension_ValueToNativeAmount(t *testing.T) { +func TestNetworkProviderExtension_ValueToAmount(t *testing.T) { networkProvider := testscommon.NewNetworkProviderMock() networkProvider.MockNativeCurrencySymbol = "EGLD" - extension := newNetworkProviderExtension(networkProvider) - - amount := extension.valueToNativeAmount("1") - expectedAmount := &types.Amount{ - Value: "1", - Currency: &types.Currency{ - Symbol: "EGLD", - Decimals: 18, + networkProvider.MockCustomCurrencies = []resources.Currency{ + { + Symbol: "ABC-abcdef", + Decimals: 4, }, } - require.Equal(t, expectedAmount, amount) -} - -func TestNetworkProviderExtension_ValueToCustomAmount(t *testing.T) { - networkProvider := testscommon.NewNetworkProviderMock() extension := newNetworkProviderExtension(networkProvider) - amount := extension.valueToCustomAmount("1", "ABC-abcdef") - expectedAmount := &types.Amount{ - Value: "1", - Currency: &types.Currency{ - Symbol: "ABC-abcdef", - Decimals: 0, - }, - } + t.Run("with native currency", func(t *testing.T) { + amount := extension.valueToAmount("1", "EGLD") + expectedAmount := &types.Amount{ + Value: "1", + Currency: &types.Currency{ + Symbol: "EGLD", + Decimals: 18, + }, + } + + require.Equal(t, expectedAmount, amount) + }) + + t.Run("with native currency (explicitly)", func(t *testing.T) { + amount := extension.valueToNativeAmount("1") + expectedAmount := &types.Amount{ + Value: "1", + Currency: &types.Currency{ + Symbol: "EGLD", + Decimals: 18, + }, + } + + require.Equal(t, expectedAmount, amount) + }) + + t.Run("with custom currency", func(t *testing.T) { + amount := extension.valueToAmount("1", "ABC-abcdef") + expectedAmount := &types.Amount{ + Value: "1", + Currency: &types.Currency{ + Symbol: "ABC-abcdef", + Decimals: 4, + }, + } + + require.Equal(t, expectedAmount, amount) + }) + + t.Run("with custom currency (explicitly)", func(t *testing.T) { + amount := extension.valueToCustomAmount("1", "ABC-abcdef") + expectedAmount := &types.Amount{ + Value: "1", + Currency: &types.Currency{ + Symbol: "ABC-abcdef", + Decimals: 4, + }, + } - require.Equal(t, expectedAmount, amount) + require.Equal(t, expectedAmount, amount) + }) } diff --git a/testscommon/networkProviderMock.go b/testscommon/networkProviderMock.go index 496745f5..9d185f9a 100644 --- a/testscommon/networkProviderMock.go +++ b/testscommon/networkProviderMock.go @@ -38,8 +38,8 @@ type networkProviderMock struct { MockBlocksByHash map[string]*api.Block MockNextAccountBlockCoordinates *resources.BlockCoordinates MockAccountsByAddress map[string]*resources.Account - MockAccountsNativeBalances map[string]*resources.Account - MockAccountsESDTBalances map[string]*resources.AccountESDTBalance + MockAccountsNativeBalances map[string]*resources.AccountBalanceOnBlock + MockAccountsCustomBalances map[string]*resources.AccountBalanceOnBlock MockMempoolTransactionsByHash map[string]*transaction.ApiTransactionResult MockComputedTransactionHash string MockComputedReceiptHash string @@ -93,13 +93,12 @@ func NewNetworkProviderMock() *networkProviderMock { MockBlocksByNonce: make(map[uint64]*api.Block), MockBlocksByHash: make(map[string]*api.Block), MockNextAccountBlockCoordinates: &resources.BlockCoordinates{ - Nonce: 0, - Hash: emptyHash, - RootHash: emptyHash, + Nonce: 0, + Hash: emptyHash, }, MockAccountsByAddress: make(map[string]*resources.Account), - MockAccountsNativeBalances: make(map[string]*resources.Account), - MockAccountsESDTBalances: make(map[string]*resources.AccountESDTBalance), + MockAccountsNativeBalances: make(map[string]*resources.AccountBalanceOnBlock), + MockAccountsCustomBalances: make(map[string]*resources.AccountBalanceOnBlock), MockMempoolTransactionsByHash: make(map[string]*transaction.ApiTransactionResult), MockComputedTransactionHash: emptyHash, MockNextError: nil, @@ -228,40 +227,30 @@ func (mock *networkProviderMock) GetAccount(address string) (*resources.AccountO return nil, fmt.Errorf("account %s not found", address) } -func (mock *networkProviderMock) GetAccountNativeBalance(address string, _ resources.AccountQueryOptions) (*resources.AccountOnBlock, error) { +func (mock *networkProviderMock) GetAccountBalance(address string, tokenIdentifier string, _ resources.AccountQueryOptions) (*resources.AccountBalanceOnBlock, error) { if mock.MockNextError != nil { return nil, mock.MockNextError } - accountBalance, ok := mock.MockAccountsNativeBalances[address] - if ok { - return &resources.AccountOnBlock{ - Account: resources.Account{ - Balance: accountBalance.Balance, - Nonce: accountBalance.Nonce, - }, - BlockCoordinates: *mock.MockNextAccountBlockCoordinates, - }, nil - } - - return nil, fmt.Errorf("account %s not found", address) -} + isNativeBalance := tokenIdentifier == mock.MockNativeCurrencySymbol + if isNativeBalance { + accountBalance, ok := mock.MockAccountsNativeBalances[address] + if ok { + accountBalance.BlockCoordinates = *mock.MockNextAccountBlockCoordinates + return accountBalance, nil + } -func (mock *networkProviderMock) GetAccountESDTBalance(address string, tokenIdentifier string, _ resources.AccountQueryOptions) (*resources.AccountESDTBalance, error) { - if mock.MockNextError != nil { - return nil, mock.MockNextError + return nil, fmt.Errorf("account %s not found (for native balance)", address) } - key := fmt.Sprintf("%s_%s", address, tokenIdentifier) - accountBalance, ok := mock.MockAccountsESDTBalances[key] + customTokenBalanceKey := fmt.Sprintf("%s_%s", address, tokenIdentifier) + accountBalance, ok := mock.MockAccountsCustomBalances[customTokenBalanceKey] if ok { - return &resources.AccountESDTBalance{ - Balance: accountBalance.Balance, - BlockCoordinates: *mock.MockNextAccountBlockCoordinates, - }, nil + accountBalance.BlockCoordinates = *mock.MockNextAccountBlockCoordinates + return accountBalance, nil } - return nil, fmt.Errorf("account %s not found", address) + return nil, fmt.Errorf("account %s not found (for custom token balance)", address) } // IsAddressObserved -