Skip to content

Commit

Permalink
Merge branch 'spica-03' into handle-contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
andreibancioiu committed Aug 12, 2024
2 parents 8c51425 + 956de52 commit adbab56
Show file tree
Hide file tree
Showing 15 changed files with 357 additions and 204 deletions.
3 changes: 1 addition & 2 deletions server/factory/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 33 additions & 52 deletions server/provider/accounts.go
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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{}

Expand All @@ -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)
Expand All @@ -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
}
91 changes: 70 additions & 21 deletions server/provider/accounts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -64,51 +64,74 @@ 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,
},
},
}

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{
Expand All @@ -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)
})
}
46 changes: 46 additions & 0 deletions server/provider/tokenIdentifierParts.go
Original file line number Diff line number Diff line change
@@ -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)
}
35 changes: 35 additions & 0 deletions server/provider/tokenIdentifierParts_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
Loading

0 comments on commit adbab56

Please sign in to comment.