diff --git a/.github/workflows/check_with_mesh_cli.yml b/.github/workflows/check_with_mesh_cli.yml index 495e9505..65661bf4 100644 --- a/.github/workflows/check_with_mesh_cli.yml +++ b/.github/workflows/check_with_mesh_cli.yml @@ -26,12 +26,18 @@ jobs: cd $GITHUB_WORKSPACE/cmd/rosetta && go build . cd $GITHUB_WORKSPACE/systemtests && go build ./proxyToObserverAdapter.go - - name: check:data + - name: check:construction (native) run: | - PYTHONPATH=. python3 ./systemtests/check_with_mesh_cli.py --mode=data --network=testnet + PYTHONPATH=. python3 ./systemtests/check_with_mesh_cli.py --mode=construction-native --network=testnet + sleep 30 + + - name: check:construction (custom) + run: | + PYTHONPATH=. python3 ./systemtests/check_with_mesh_cli.py --mode=construction-custom --network=testnet + sleep 30 - - name: check:construction + - name: check:data run: | - PYTHONPATH=. python3 ./systemtests/check_with_mesh_cli.py --mode=construction --network=testnet + PYTHONPATH=. python3 ./systemtests/check_with_mesh_cli.py --mode=data --network=testnet diff --git a/cmd/rosetta/cli.go b/cmd/rosetta/cli.go index e6a0e8c9..20b2d7c2 100644 --- a/cmd/rosetta/cli.go +++ b/cmd/rosetta/cli.go @@ -156,10 +156,10 @@ VERSION: Required: true, } - cliFlagCustomCurrenciesSymbols = cli.StringSliceFlag{ - Name: "custom-currencies", - Usage: "Specifies the symbols of enabled custom currencies (i.e. ESDT identifiers).", - Value: &cli.StringSlice{}, + cliFlagConfigFileCustomCurrencies = cli.StringFlag{ + Name: "config-custom-currencies", + Usage: "Specifies the configuration file for custom currencies.", + Required: false, } ) @@ -187,7 +187,7 @@ func getAllCliFlags() []cli.Flag { cliFlagNativeCurrencySymbol, cliFlagFirstHistoricalEpoch, cliFlagNumHistoricalEpochs, - cliFlagCustomCurrenciesSymbols, + cliFlagConfigFileCustomCurrencies, } } @@ -215,7 +215,7 @@ type parsedCliFlags struct { nativeCurrencySymbol string firstHistoricalEpoch uint32 numHistoricalEpochs uint32 - customCurrenciesSymbols []string + configFileCustomCurrencies string } func getParsedCliFlags(ctx *cli.Context) parsedCliFlags { @@ -243,6 +243,6 @@ func getParsedCliFlags(ctx *cli.Context) parsedCliFlags { nativeCurrencySymbol: ctx.GlobalString(cliFlagNativeCurrencySymbol.Name), firstHistoricalEpoch: uint32(ctx.GlobalUint(cliFlagFirstHistoricalEpoch.Name)), numHistoricalEpochs: uint32(ctx.GlobalUint(cliFlagNumHistoricalEpochs.Name)), - customCurrenciesSymbols: ctx.GlobalStringSlice(cliFlagCustomCurrenciesSymbols.Name), + configFileCustomCurrencies: ctx.GlobalString(cliFlagConfigFileCustomCurrencies.Name), } } diff --git a/cmd/rosetta/config.go b/cmd/rosetta/config.go new file mode 100644 index 00000000..9f3e3274 --- /dev/null +++ b/cmd/rosetta/config.go @@ -0,0 +1,24 @@ +package main + +import ( + "encoding/json" + "os" + + "github.com/multiversx/mx-chain-rosetta/server/resources" +) + +func loadConfigOfCustomCurrencies(configFile string) ([]resources.Currency, error) { + fileContent, err := os.ReadFile(configFile) + if err != nil { + return nil, err + } + + var customCurrencies []resources.Currency + + err = json.Unmarshal(fileContent, &customCurrencies) + if err != nil { + return nil, err + } + + return customCurrencies, nil +} diff --git a/cmd/rosetta/config_test.go b/cmd/rosetta/config_test.go new file mode 100644 index 00000000..3e407f73 --- /dev/null +++ b/cmd/rosetta/config_test.go @@ -0,0 +1,36 @@ +package main + +import ( + "testing" + + "github.com/multiversx/mx-chain-rosetta/server/resources" + "github.com/stretchr/testify/require" +) + +func TestLoadConfigOfCustomCurrencies(t *testing.T) { + t.Run("with success", func(t *testing.T) { + customCurrencies, err := loadConfigOfCustomCurrencies("testdata/custom-currencies.json") + require.NoError(t, err) + require.NoError(t, err) + require.Equal(t, []resources.Currency{ + { + Symbol: "WEGLD-bd4d79", + Decimals: 18, + }, + { + Symbol: "USDC-c76f1f", + Decimals: 6, + }, + }, customCurrencies) + }) + + t.Run("with error (missing file)", func(t *testing.T) { + _, err := loadConfigOfCustomCurrencies("testdata/missing-file.json") + require.Error(t, err) + }) + + t.Run("with error (invalid file)", func(t *testing.T) { + _, err := loadConfigOfCustomCurrencies("testdata/custom-currencies-bad.json") + require.Error(t, err) + }) +} diff --git a/cmd/rosetta/main.go b/cmd/rosetta/main.go index c710c977..fc81360c 100644 --- a/cmd/rosetta/main.go +++ b/cmd/rosetta/main.go @@ -53,6 +53,11 @@ func startRosetta(ctx *cli.Context) error { return err } + customCurrencies, err := loadConfigOfCustomCurrencies(cliFlags.configFileCustomCurrencies) + if err != nil { + return err + } + log.Info("Starting Rosetta...", "middleware", version.RosettaMiddlewareVersion, "specification", version.RosettaVersion) networkProvider, err := factory.CreateNetworkProvider(factory.ArgsCreateNetworkProvider{ @@ -72,7 +77,7 @@ func startRosetta(ctx *cli.Context) error { MinGasLimit: cliFlags.minGasLimit, ExtraGasLimitGuardedTx: cliFlags.extraGasLimitGuardedTx, NativeCurrencySymbol: cliFlags.nativeCurrencySymbol, - CustomCurrenciesSymbols: cliFlags.customCurrenciesSymbols, + CustomCurrencies: customCurrencies, GenesisBlockHash: cliFlags.genesisBlock, FirstHistoricalEpoch: cliFlags.firstHistoricalEpoch, NumHistoricalEpochs: cliFlags.numHistoricalEpochs, diff --git a/cmd/rosetta/testdata/custom-currencies-bad.json b/cmd/rosetta/testdata/custom-currencies-bad.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/cmd/rosetta/testdata/custom-currencies-bad.json @@ -0,0 +1 @@ +{} diff --git a/cmd/rosetta/testdata/custom-currencies.json b/cmd/rosetta/testdata/custom-currencies.json new file mode 100644 index 00000000..8a90dd3c --- /dev/null +++ b/cmd/rosetta/testdata/custom-currencies.json @@ -0,0 +1,10 @@ +[ + { + "symbol": "WEGLD-bd4d79", + "decimals": 18 + }, + { + "symbol": "USDC-c76f1f", + "decimals": 6 + } +] diff --git a/server/factory/provider.go b/server/factory/provider.go index bd94e421..6f15a689 100644 --- a/server/factory/provider.go +++ b/server/factory/provider.go @@ -11,6 +11,7 @@ import ( processFactory "github.com/multiversx/mx-chain-proxy-go/process/factory" "github.com/multiversx/mx-chain-rosetta/server/factory/components" "github.com/multiversx/mx-chain-rosetta/server/provider" + "github.com/multiversx/mx-chain-rosetta/server/resources" ) const ( @@ -41,7 +42,7 @@ type ArgsCreateNetworkProvider struct { MinGasLimit uint64 ExtraGasLimitGuardedTx uint64 NativeCurrencySymbol string - CustomCurrenciesSymbols []string + CustomCurrencies []resources.Currency GenesisBlockHash string GenesisTimestamp int64 FirstHistoricalEpoch uint32 @@ -131,7 +132,7 @@ func CreateNetworkProvider(args ArgsCreateNetworkProvider) (NetworkProvider, err MinGasLimit: args.MinGasLimit, ExtraGasLimitGuardedTx: args.ExtraGasLimitGuardedTx, NativeCurrencySymbol: args.NativeCurrencySymbol, - CustomCurrenciesSymbols: args.CustomCurrenciesSymbols, + CustomCurrencies: args.CustomCurrencies, GenesisBlockHash: args.GenesisBlockHash, GenesisTimestamp: args.GenesisTimestamp, FirstHistoricalEpoch: args.FirstHistoricalEpoch, diff --git a/server/provider/currenciesProvider.go b/server/provider/currenciesProvider.go index 2e5df112..b262c7dd 100644 --- a/server/provider/currenciesProvider.go +++ b/server/provider/currenciesProvider.go @@ -11,20 +11,19 @@ type currenciesProvider struct { // In the future, we might extract this to a standalone component (separate sub-package). // For the moment, we keep it as a simple structure, with unexported (future-to-be exported) member functions. -func newCurrenciesProvider(nativeCurrencySymbol string, customCurrenciesSymbols []string) *currenciesProvider { - customCurrencies := make([]resources.Currency, 0, len(customCurrenciesSymbols)) +func newCurrenciesProvider(nativeCurrencySymbol string, customCurrencies []resources.Currency) (*currenciesProvider, error) { customCurrenciesBySymbol := make(map[string]resources.Currency) + customCurrenciesSymbols := make([]string, 0, len(customCurrencies)) - for _, symbol := range customCurrenciesSymbols { - customCurrency := resources.Currency{ - Symbol: symbol, - // At the moment, for custom currencies (ESDTs), we hardcode the number of decimals to 0. - // In the future, we might fetch the actual number of decimals from the metachain observer. - Decimals: 0, + for index, customCurrency := range customCurrencies { + symbol := customCurrency.Symbol + + if len(symbol) == 0 { + return nil, newInvalidCustomCurrency(index) } - customCurrencies = append(customCurrencies, customCurrency) customCurrenciesBySymbol[symbol] = customCurrency + customCurrenciesSymbols = append(customCurrenciesSymbols, symbol) } return ¤ciesProvider{ @@ -35,7 +34,7 @@ func newCurrenciesProvider(nativeCurrencySymbol string, customCurrenciesSymbols customCurrenciesSymbols: customCurrenciesSymbols, customCurrencies: customCurrencies, customCurrenciesBySymbol: customCurrenciesBySymbol, - } + }, nil } // GetNativeCurrency gets the native currency (EGLD, 18 decimals) diff --git a/server/provider/currenciesProvider_test.go b/server/provider/currenciesProvider_test.go index acff6ec6..fb23f711 100644 --- a/server/provider/currenciesProvider_test.go +++ b/server/provider/currenciesProvider_test.go @@ -3,35 +3,88 @@ package provider import ( "testing" + "github.com/multiversx/mx-chain-rosetta/server/resources" "github.com/stretchr/testify/require" ) -func TestCurrenciesProvider(t *testing.T) { +func TestNewCurrenciesProvider(t *testing.T) { + t.Run("with success", func(t *testing.T) { + t.Parallel() + + provider, err := newCurrenciesProvider("XeGLD", []resources.Currency{ + {Symbol: "ROSETTA-3a2edf", Decimals: 2}, + {Symbol: "ROSETTA-057ab4", Decimals: 2}, + }) + + require.NoError(t, err) + require.NotNil(t, provider) + }) + + t.Run("with success (empty or nil array of custom currencies)", func(t *testing.T) { + t.Parallel() + + provider, err := newCurrenciesProvider("XeGLD", []resources.Currency{}) + require.NoError(t, err) + require.NotNil(t, provider) + + provider, err = newCurrenciesProvider("XeGLD", nil) + require.NoError(t, err) + require.NotNil(t, provider) + }) + + t.Run("with invalid custom currency symbol", func(t *testing.T) { + t.Parallel() + + _, err := newCurrenciesProvider("XeGLD", []resources.Currency{ + {Symbol: "", Decimals: 2}, + }) + + require.ErrorIs(t, err, errInvalidCustomCurrencySymbol) + require.Equal(t, "invalid custom currency symbol, index = 0", err.Error()) + }) +} + +func TestCurrenciesProvider_NativeCurrency(t *testing.T) { t.Parallel() - provider := newCurrenciesProvider("XeGLD", []string{"ROSETTA-3a2edf", "ROSETTA-057ab4"}) + provider, err := newCurrenciesProvider("XeGLD", []resources.Currency{ + {Symbol: "ROSETTA-3a2edf", Decimals: 2}, + }) + + require.NoError(t, err) - t.Run("get native", func(t *testing.T) { + nativeCurrency := provider.GetNativeCurrency() + require.Equal(t, "XeGLD", nativeCurrency.Symbol) + require.Equal(t, int32(18), nativeCurrency.Decimals) +} + +func TestCurrenciesProvider_CustomCurrencies(t *testing.T) { + provider, err := newCurrenciesProvider("XeGLD", []resources.Currency{ + {Symbol: "ROSETTA-3a2edf", Decimals: 2}, + {Symbol: "ROSETTA-057ab4", Decimals: 2}, + }) + + require.NoError(t, err) + + t.Run("check has", func(t *testing.T) { t.Parallel() - nativeCurrency := provider.GetNativeCurrency() - require.Equal(t, "XeGLD", nativeCurrency.Symbol) - require.Equal(t, int32(18), nativeCurrency.Decimals) + require.True(t, provider.HasCustomCurrency("ROSETTA-3a2edf")) + require.True(t, provider.HasCustomCurrency("ROSETTA-057ab4")) + require.False(t, provider.HasCustomCurrency("FOO-abcdef")) + require.False(t, provider.HasCustomCurrency("BAR-abcdef")) + require.False(t, provider.HasCustomCurrency("")) }) - t.Run("get custom", func(t *testing.T) { + t.Run("get all", func(t *testing.T) { t.Parallel() customCurrencies := provider.GetCustomCurrencies() require.Equal(t, 2, len(customCurrencies)) + }) - customCurrency, ok := provider.GetCustomCurrencyBySymbol("ROSETTA-3a2edf") - require.True(t, ok) - require.Equal(t, "ROSETTA-3a2edf", customCurrency.Symbol) - - customCurrency, ok = provider.GetCustomCurrencyBySymbol("ROSETTA-057ab4") - require.True(t, ok) - require.Equal(t, "ROSETTA-057ab4", customCurrency.Symbol) + t.Run("get all symbols", func(t *testing.T) { + t.Parallel() customCurrenciesSymbols := provider.GetCustomCurrenciesSymbols() require.Equal(t, 2, len(customCurrenciesSymbols)) @@ -39,12 +92,15 @@ func TestCurrenciesProvider(t *testing.T) { require.Equal(t, "ROSETTA-057ab4", customCurrenciesSymbols[1]) }) - t.Run("has custom", func(t *testing.T) { + t.Run("get by symbol", func(t *testing.T) { t.Parallel() - require.True(t, provider.HasCustomCurrency("ROSETTA-3a2edf")) - require.True(t, provider.HasCustomCurrency("ROSETTA-057ab4")) - require.False(t, provider.HasCustomCurrency("FOO-abcdef")) - require.False(t, provider.HasCustomCurrency("BAR-abcdef")) + customCurrency, ok := provider.GetCustomCurrencyBySymbol("ROSETTA-3a2edf") + require.True(t, ok) + require.Equal(t, "ROSETTA-3a2edf", customCurrency.Symbol) + + customCurrency, ok = provider.GetCustomCurrencyBySymbol("ROSETTA-057ab4") + require.True(t, ok) + require.Equal(t, "ROSETTA-057ab4", customCurrency.Symbol) }) } diff --git a/server/provider/errors.go b/server/provider/errors.go index 933f4641..ac01b120 100644 --- a/server/provider/errors.go +++ b/server/provider/errors.go @@ -11,6 +11,7 @@ var errCannotGetBlock = errors.New("cannot get block") var errCannotGetAccount = errors.New("cannot get account") var errCannotGetTransaction = errors.New("cannot get transaction") var errCannotGetLatestBlockNonce = errors.New("cannot get latest block nonce, maybe the node didn't start syncing") +var errInvalidCustomCurrencySymbol = errors.New("invalid custom currency symbol") func newErrCannotGetBlockByNonce(nonce uint64, innerError error) error { return fmt.Errorf("%w: %v, nonce = %d", errCannotGetBlock, innerError, nonce) @@ -28,6 +29,10 @@ func newErrCannotGetTransaction(hash string, innerError error) error { return fmt.Errorf("%w: %v, address = %s", errCannotGetTransaction, innerError, hash) } +func newInvalidCustomCurrency(index int) error { + return fmt.Errorf("%w, index = %d", errInvalidCustomCurrencySymbol, index) +} + // In proxy-go, the function CallGetRestEndPoint() returns an error message as the JSON content of the erroneous HTTP response. // Here, we attempt to decode that JSON and create an error with a "flat" error message. func convertStructuredApiErrToFlatErr(apiErr error) error { diff --git a/server/provider/networkProvider.go b/server/provider/networkProvider.go index 78ae136f..02f8e746 100644 --- a/server/provider/networkProvider.go +++ b/server/provider/networkProvider.go @@ -36,7 +36,7 @@ type ArgsNewNetworkProvider struct { MinGasLimit uint64 ExtraGasLimitGuardedTx uint64 NativeCurrencySymbol string - CustomCurrenciesSymbols []string + CustomCurrencies []resources.Currency GenesisBlockHash string GenesisTimestamp int64 FirstHistoricalEpoch uint32 @@ -83,7 +83,10 @@ func NewNetworkProvider(args ArgsNewNetworkProvider) (*networkProvider, error) { return nil, err } - currenciesProvider := newCurrenciesProvider(args.NativeCurrencySymbol, args.CustomCurrenciesSymbols) + currenciesProvider, err := newCurrenciesProvider(args.NativeCurrencySymbol, args.CustomCurrencies) + if err != nil { + return nil, err + } return &networkProvider{ currenciesProvider: currenciesProvider, diff --git a/server/provider/networkProvider_test.go b/server/provider/networkProvider_test.go index 54cec5ee..191813b1 100644 --- a/server/provider/networkProvider_test.go +++ b/server/provider/networkProvider_test.go @@ -31,15 +31,18 @@ func TestNewNetworkProvider(t *testing.T) { MinGasLimit: 50001, ExtraGasLimitGuardedTx: 50001, NativeCurrencySymbol: "XeGLD", - CustomCurrenciesSymbols: []string{"FOO-abcdef", "BAR-abcdef"}, - GenesisBlockHash: "aaaa", - GenesisTimestamp: 123456789, - FirstHistoricalEpoch: 1000, - NumHistoricalEpochs: 1024, - ObserverFacade: testscommon.NewObserverFacadeMock(), - Hasher: testscommon.RealWorldBlake2bHasher, - MarshalizerForHashing: testscommon.MarshalizerForHashing, - PubKeyConverter: testscommon.RealWorldBech32PubkeyConverter, + CustomCurrencies: []resources.Currency{ + {Symbol: "FOO-abcdef", Decimals: 6}, + {Symbol: "BAR-abcdef", Decimals: 18}, + }, + GenesisBlockHash: "aaaa", + GenesisTimestamp: 123456789, + FirstHistoricalEpoch: 1000, + NumHistoricalEpochs: 1024, + ObserverFacade: testscommon.NewObserverFacadeMock(), + Hasher: testscommon.RealWorldBlake2bHasher, + MarshalizerForHashing: testscommon.MarshalizerForHashing, + PubKeyConverter: testscommon.RealWorldBech32PubkeyConverter, } provider, err := NewNetworkProvider(args) @@ -60,7 +63,10 @@ func TestNewNetworkProvider(t *testing.T) { assert.Equal(t, uint64(50001), provider.GetNetworkConfig().MinGasLimit) assert.Equal(t, uint64(50001), provider.GetNetworkConfig().ExtraGasLimitGuardedTx) assert.Equal(t, "XeGLD", provider.GetNativeCurrency().Symbol) - assert.Equal(t, []resources.Currency{{Symbol: "FOO-abcdef"}, {Symbol: "BAR-abcdef"}}, provider.GetCustomCurrencies()) + assert.Equal(t, []resources.Currency{ + {Symbol: "FOO-abcdef", Decimals: 6}, + {Symbol: "BAR-abcdef", Decimals: 18}, + }, provider.GetCustomCurrencies()) assert.Equal(t, "aaaa", provider.GetGenesisBlockSummary().Hash) assert.Equal(t, int64(123456789), provider.GetGenesisTimestamp()) assert.Equal(t, uint32(1000), provider.firstHistoricalEpoch) diff --git a/server/resources/resources.go b/server/resources/resources.go index 43edf3c4..1624f0fd 100644 --- a/server/resources/resources.go +++ b/server/resources/resources.go @@ -22,8 +22,8 @@ type BlockSummary struct { // Currency is an internal resource type Currency struct { - Symbol string - Decimals int32 + Symbol string `json:"symbol"` + Decimals int32 `json:"decimals"` } // BlockCoordinates is an API resource diff --git a/server/services/constants.go b/server/services/constants.go index 9aa7dff2..16ff804c 100644 --- a/server/services/constants.go +++ b/server/services/constants.go @@ -12,7 +12,9 @@ var ( transactionProcessingTypeRelayed = "RelayedTx" transactionProcessingTypeBuiltInFunctionCall = "BuiltInFunctionCall" transactionProcessingTypeMoveBalance = "MoveBalance" + amountZero = "0" builtInFunctionClaimDeveloperRewards = core.BuiltInFunctionClaimDeveloperRewards + builtInFunctionESDTTransfer = core.BuiltInFunctionESDTTransfer refundGasMessage = "refundedGas" argumentsSeparator = "@" sendingValueToNonPayableContractDataPrefix = argumentsSeparator + hex.EncodeToString([]byte("sending value to non payable contract")) diff --git a/server/services/constructionOptions_test.go b/server/services/constructionOptions_test.go index 15cc269f..28c7a6ca 100644 --- a/server/services/constructionOptions_test.go +++ b/server/services/constructionOptions_test.go @@ -54,7 +54,7 @@ func TestConstructionOptions_Validate(t *testing.T) { Sender: "alice", Receiver: "bob", Amount: "1234", - CurrencySymbol: "FOO", + CurrencySymbol: "TEST-abcdef", Data: []byte("hello"), }).validate("XeGLD"), "for custom currencies, option 'data' must be empty") diff --git a/server/services/constructionService.go b/server/services/constructionService.go index 90513542..81efa333 100644 --- a/server/services/constructionService.go +++ b/server/services/constructionService.go @@ -5,6 +5,8 @@ import ( "encoding/hex" "encoding/json" "errors" + "fmt" + "strings" "github.com/coinbase/rosetta-sdk-go/server" "github.com/coinbase/rosetta-sdk-go/types" @@ -133,26 +135,31 @@ func (service *constructionService) ConstructionMetadata( return nil, service.errFactory.newErrWithOriginal(ErrUnableToGetAccount, err) } - computedData := service.computeData(requestOptions) - - fee, gasLimit, gasPrice, errTyped := service.computeFeeComponents(requestOptions, computedData) - if err != nil { - return nil, errTyped - } - metadata := &constructionMetadata{ Nonce: account.Account.Nonce, Sender: requestOptions.Sender, Receiver: requestOptions.Receiver, - Amount: requestOptions.Amount, CurrencySymbol: requestOptions.CurrencySymbol, - GasLimit: gasLimit, - GasPrice: gasPrice, - Data: computedData, ChainID: service.provider.GetNetworkConfig().NetworkID, Version: transactionVersion, } + if service.extension.isNativeCurrencySymbol(requestOptions.CurrencySymbol) { + metadata.Amount = requestOptions.Amount + metadata.Data = requestOptions.Data + } else { + metadata.Amount = amountZero + metadata.Data = service.computeDataForCustomCurrencyTransfer(requestOptions.CurrencySymbol, requestOptions.Amount) + } + + fee, gasLimit, gasPrice, errTyped := service.computeFeeComponents(requestOptions, metadata.Data) + if errTyped != nil { + return nil, errTyped + } + + metadata.GasLimit = gasLimit + metadata.GasPrice = gasPrice + metadataAsObjectsMap, err := toObjectsMap(metadata) if err != nil { return nil, service.errFactory.newErrWithOriginal(ErrConstruction, err) @@ -166,13 +173,10 @@ func (service *constructionService) ConstructionMetadata( }, nil } -func (service *constructionService) computeData(options *constructionOptions) []byte { - if service.extension.isNativeCurrencySymbol(options.CurrencySymbol) { - return options.Data - } - - // TODO: Handle in a future PR - return make([]byte, 0) +func (service *constructionService) computeDataForCustomCurrencyTransfer(tokenIdentifier string, amount string) []byte { + // Only fungible tokens are supported (at least, for now). + data := fmt.Sprintf("%s@%s@%s", builtInFunctionESDTTransfer, stringToHex(tokenIdentifier), amountToHex(amount)) + return []byte(data) } // ConstructionPayloads returns an unsigned transaction blob and a collection of payloads that must be signed @@ -224,29 +228,85 @@ func (service *constructionService) ConstructionParse( } } + operations, err := service.createOperationsFromPreparedTx(tx) + if err != nil { + return nil, service.errFactory.newErrWithOriginal(ErrConstruction, err) + } + return &types.ConstructionParseResponse{ - Operations: service.createOperationsFromPreparedTx(tx), + Operations: operations, AccountIdentifierSigners: signers, }, nil } -func (service *constructionService) createOperationsFromPreparedTx(tx *data.Transaction) []*types.Operation { - operations := []*types.Operation{ - { - Type: opTransfer, - Account: addressToAccountIdentifier(tx.Sender), - Amount: service.extension.valueToNativeAmount("-" + tx.Value), - }, - { - Type: opTransfer, - Account: addressToAccountIdentifier(tx.Receiver), - Amount: service.extension.valueToNativeAmount(tx.Value), - }, +func (service *constructionService) createOperationsFromPreparedTx(tx *data.Transaction) ([]*types.Operation, error) { + var operations []*types.Operation + + isCustomCurrencyTransfer := isCustomCurrencyTransfer(string(tx.Data)) + + if isCustomCurrencyTransfer { + tokenIdentifier, amount, err := parseCustomCurrencyTransfer(string(tx.Data)) + if err != nil { + return nil, err + } + + operations = []*types.Operation{ + { + Type: opCustomTransfer, + Account: addressToAccountIdentifier(tx.Sender), + Amount: service.extension.valueToCustomAmount("-"+amount, tokenIdentifier), + }, + { + Type: opCustomTransfer, + Account: addressToAccountIdentifier(tx.Receiver), + Amount: service.extension.valueToCustomAmount(amount, tokenIdentifier), + }, + } + } else { + // Native currency transfer + operations = []*types.Operation{ + { + Type: opTransfer, + Account: addressToAccountIdentifier(tx.Sender), + Amount: service.extension.valueToNativeAmount("-" + tx.Value), + }, + { + Type: opTransfer, + Account: addressToAccountIdentifier(tx.Receiver), + Amount: service.extension.valueToNativeAmount(tx.Value), + }, + } } indexOperations(operations) - return operations + return operations, nil +} + +func isCustomCurrencyTransfer(txData string) bool { + return strings.HasPrefix(txData, builtInFunctionESDTTransfer) +} + +// parseCustomCurrencyTransfer parses a single ESDT transfer. +// Other kinds of ESDT transfers are not supported, for now. +func parseCustomCurrencyTransfer(txData string) (string, string, error) { + parts := strings.Split(txData, "@") + + if len(parts) != 3 { + return "", "", errors.New("cannot parse data of custom currency transfer") + } + + tokenIdentifierBytes, err := hex.DecodeString(parts[1]) + if err != nil { + return "", "", errors.New("cannot decode custom token identifier") + } + + amount, err := hexToAmount(parts[2]) + if err != nil { + return "", "", errors.New("cannot decode custom token amount") + } + + return string(tokenIdentifierBytes), amount, nil } func getTxFromRequest(txString string) (*data.Transaction, error) { diff --git a/server/services/constructionService_test.go b/server/services/constructionService_test.go index 0d183e30..167dac11 100644 --- a/server/services/constructionService_test.go +++ b/server/services/constructionService_test.go @@ -69,7 +69,7 @@ func TestConstructionService_ConstructionPreprocess(t *testing.T) { }, } - response, err := service.ConstructionPreprocess(context.Background(), + response, errTyped := service.ConstructionPreprocess(context.Background(), &types.ConstructionPreprocessRequest{ Operations: operations, Metadata: objectsMap{ @@ -78,6 +78,8 @@ func TestConstructionService_ConstructionPreprocess(t *testing.T) { }, ) + require.Nil(t, errTyped) + expectedOptions := &constructionOptions{ Sender: testscommon.TestAddressAlice, Receiver: testscommon.TestAddressBob, @@ -86,9 +88,8 @@ func TestConstructionService_ConstructionPreprocess(t *testing.T) { } actualOptions := &constructionOptions{} - _ = fromObjectsMap(response.Options, actualOptions) - - require.Nil(t, err) + err := fromObjectsMap(response.Options, actualOptions) + require.NoError(t, err) require.Equal(t, expectedOptions, actualOptions) }) @@ -104,7 +105,7 @@ func TestConstructionService_ConstructionPreprocess(t *testing.T) { }, } - response, err := service.ConstructionPreprocess(context.Background(), + response, errTyped := service.ConstructionPreprocess(context.Background(), &types.ConstructionPreprocessRequest{ Operations: operations, Metadata: objectsMap{ @@ -114,6 +115,8 @@ func TestConstructionService_ConstructionPreprocess(t *testing.T) { }, ) + require.Nil(t, errTyped) + expectedOptions := &constructionOptions{ Sender: testscommon.TestAddressAlice, Receiver: testscommon.TestAddressBob, @@ -122,9 +125,8 @@ func TestConstructionService_ConstructionPreprocess(t *testing.T) { } actualOptions := &constructionOptions{} - _ = fromObjectsMap(response.Options, actualOptions) - - require.Nil(t, err) + err := fromObjectsMap(response.Options, actualOptions) + require.NoError(t, err) require.Equal(t, expectedOptions, actualOptions) }) @@ -154,7 +156,7 @@ func TestConstructionService_ConstructionPreprocess(t *testing.T) { t.Run("with maximal 'metadata', without 'operations'", func(t *testing.T) { t.Parallel() - response, err := service.ConstructionPreprocess(context.Background(), + response, errTyped := service.ConstructionPreprocess(context.Background(), &types.ConstructionPreprocessRequest{ Metadata: objectsMap{ "sender": testscommon.TestAddressAlice, @@ -168,6 +170,8 @@ func TestConstructionService_ConstructionPreprocess(t *testing.T) { }, ) + require.Nil(t, errTyped) + expectedOptions := &constructionOptions{ Sender: testscommon.TestAddressAlice, Receiver: testscommon.TestAddressBob, @@ -179,16 +183,15 @@ func TestConstructionService_ConstructionPreprocess(t *testing.T) { } actualOptions := &constructionOptions{} - _ = fromObjectsMap(response.Options, actualOptions) - - require.Nil(t, err) + err := fromObjectsMap(response.Options, actualOptions) + require.NoError(t, err) require.Equal(t, expectedOptions, actualOptions) }) t.Run("with incomplete 'metadata', without 'operations'", func(t *testing.T) { t.Parallel() - response, err := service.ConstructionPreprocess(context.Background(), + response, errTyped := service.ConstructionPreprocess(context.Background(), &types.ConstructionPreprocessRequest{ Metadata: objectsMap{ "sender": testscommon.TestAddressAlice, @@ -197,7 +200,7 @@ func TestConstructionService_ConstructionPreprocess(t *testing.T) { }, ) - require.Equal(t, int32(ErrConstruction), err.Code) + require.Equal(t, int32(ErrConstruction), errTyped.Code) require.Nil(t, response) }) } @@ -211,10 +214,49 @@ func TestConstructionService_ConstructionMetadata(t *testing.T) { service := NewConstructionService(networkProvider) - t.Run("with explicitly providing gas limit and price", func(t *testing.T) { + t.Run("with native currency, with explicitly providing gas limit and price", func(t *testing.T) { t.Parallel() - response, err := service.ConstructionMetadata(context.Background(), + response, errTyped := service.ConstructionMetadata(context.Background(), + &types.ConstructionMetadataRequest{ + Options: objectsMap{ + "receiver": testscommon.TestAddressBob, + "sender": testscommon.TestAddressAlice, + "amount": "1234", + "currencySymbol": "XeGLD", + "gasLimit": 100000, + "gasPrice": 1500000000, + }, + }, + ) + + require.Nil(t, errTyped) + + expectedMetadata := &constructionMetadata{ + Sender: testscommon.TestAddressAlice, + Receiver: testscommon.TestAddressBob, + Nonce: 42, + Amount: "1234", + CurrencySymbol: "XeGLD", + GasLimit: 100000, + GasPrice: 1500000000, + ChainID: "T", + Version: 1, + } + + actualMetadata := &constructionMetadata{} + err := fromObjectsMap(response.Metadata, actualMetadata) + require.NoError(t, err) + + // We are suggesting the fee by considering the refund + require.Equal(t, "75000000000000", response.SuggestedFee[0].Value) + require.Equal(t, expectedMetadata, actualMetadata) + }) + + t.Run("with native currency, with explicitly providing gas limit and price (with data)", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionMetadata(context.Background(), &types.ConstructionMetadataRequest{ Options: objectsMap{ "receiver": testscommon.TestAddressBob, @@ -222,12 +264,14 @@ func TestConstructionService_ConstructionMetadata(t *testing.T) { "amount": "1234", "currencySymbol": "XeGLD", "gasLimit": 70000, - "gasPrice": 1000000000, + "gasPrice": 1500000000, "data": []byte("hello"), }, }, ) + require.Nil(t, errTyped) + expectedMetadata := &constructionMetadata{ Sender: testscommon.TestAddressAlice, Receiver: testscommon.TestAddressBob, @@ -235,25 +279,45 @@ func TestConstructionService_ConstructionMetadata(t *testing.T) { Amount: "1234", CurrencySymbol: "XeGLD", GasLimit: 70000, - GasPrice: 1000000000, + GasPrice: 1500000000, Data: []byte("hello"), ChainID: "T", Version: 1, } actualMetadata := &constructionMetadata{} - _ = fromObjectsMap(response.Metadata, actualMetadata) + err := fromObjectsMap(response.Metadata, actualMetadata) + require.NoError(t, err) - require.Nil(t, err) // We are suggesting the fee by considering the refund - require.Equal(t, "57500000000000", response.SuggestedFee[0].Value) + require.Equal(t, "86250000000000", response.SuggestedFee[0].Value) require.Equal(t, expectedMetadata, actualMetadata) }) - t.Run("without providing gas limit and price", func(t *testing.T) { + t.Run("with native currency, with explicitly providing gas limit and price (but too little gas)", func(t *testing.T) { + t.Parallel() + + _, errTyped := service.ConstructionMetadata(context.Background(), + &types.ConstructionMetadataRequest{ + Options: objectsMap{ + "receiver": testscommon.TestAddressBob, + "sender": testscommon.TestAddressAlice, + "amount": "1234", + "currencySymbol": "XeGLD", + "gasLimit": 50000, + "gasPrice": 1000000000, + "data": []byte("hello"), + }, + }, + ) + + require.Equal(t, ErrInsufficientGasLimit, errCode(errTyped.Code)) + }) + + t.Run("with native currency, without providing gas limit and price", func(t *testing.T) { t.Parallel() - response, err := service.ConstructionMetadata(context.Background(), + response, errTyped := service.ConstructionMetadata(context.Background(), &types.ConstructionMetadataRequest{ Options: objectsMap{ "receiver": testscommon.TestAddressBob, @@ -265,6 +329,8 @@ func TestConstructionService_ConstructionMetadata(t *testing.T) { }, ) + require.Nil(t, errTyped) + expectedMetadata := &constructionMetadata{ Sender: testscommon.TestAddressAlice, Receiver: testscommon.TestAddressBob, @@ -279,12 +345,108 @@ func TestConstructionService_ConstructionMetadata(t *testing.T) { } actualMetadata := &constructionMetadata{} - _ = fromObjectsMap(response.Metadata, actualMetadata) + err := fromObjectsMap(response.Metadata, actualMetadata) + require.NoError(t, err) - require.Nil(t, err) require.Equal(t, "57500000000000", response.SuggestedFee[0].Value) require.Equal(t, expectedMetadata, actualMetadata) }) + + t.Run("with custom currency, with explicitly providing gas limit and price (but too little gas)", func(t *testing.T) { + t.Parallel() + + _, errTyped := service.ConstructionMetadata(context.Background(), + &types.ConstructionMetadataRequest{ + Options: objectsMap{ + "receiver": testscommon.TestAddressBob, + "sender": testscommon.TestAddressAlice, + "amount": "1234", + "currencySymbol": "TEST-abcdef", + "gasLimit": 50000, + "gasPrice": 1000000000, + }, + }, + ) + + require.Equal(t, ErrInsufficientGasLimit, errCode(errTyped.Code)) + }) + + t.Run("with custom currency, with explicitly providing gas limit and price", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionMetadata(context.Background(), + &types.ConstructionMetadataRequest{ + Options: objectsMap{ + "receiver": testscommon.TestAddressBob, + "sender": testscommon.TestAddressAlice, + "amount": "1234", + "currencySymbol": "TEST-abcdef", + "gasLimit": 500000, + "gasPrice": 1000000000, + }, + }, + ) + + require.Nil(t, errTyped) + + expectedMetadata := &constructionMetadata{ + Sender: testscommon.TestAddressAlice, + Receiver: testscommon.TestAddressBob, + Nonce: 42, + Amount: "0", + CurrencySymbol: "TEST-abcdef", + GasLimit: 500000, + GasPrice: 1000000000, + Data: []byte("ESDTTransfer@544553542d616263646566@04d2"), + ChainID: "T", + Version: 1, + } + + actualMetadata := &constructionMetadata{} + err := fromObjectsMap(response.Metadata, actualMetadata) + require.NoError(t, err) + + // We are suggesting the fee by considering the refund + require.Equal(t, "112000000000000", response.SuggestedFee[0].Value) + require.Equal(t, expectedMetadata, actualMetadata) + }) + + t.Run("with custom currency, without providing gas limit and price", func(t *testing.T) { + t.Parallel() + + response, errTyped := service.ConstructionMetadata(context.Background(), + &types.ConstructionMetadataRequest{ + Options: objectsMap{ + "receiver": testscommon.TestAddressBob, + "sender": testscommon.TestAddressAlice, + "amount": "1234", + "currencySymbol": "TEST-abcdef", + }, + }, + ) + + require.Nil(t, errTyped) + + expectedMetadata := &constructionMetadata{ + Sender: testscommon.TestAddressAlice, + Receiver: testscommon.TestAddressBob, + Nonce: 42, + Amount: "0", + CurrencySymbol: "TEST-abcdef", + GasLimit: 310000, + GasPrice: 1000000000, + Data: []byte("ESDTTransfer@544553542d616263646566@04d2"), + ChainID: "T", + Version: 1, + } + + actualMetadata := &constructionMetadata{} + err := fromObjectsMap(response.Metadata, actualMetadata) + require.NoError(t, err) + + require.Equal(t, "112000000000000", response.SuggestedFee[0].Value) + require.Equal(t, expectedMetadata, actualMetadata) + }) } func TestConstructionService_ConstructionPayloads(t *testing.T) { @@ -298,7 +460,7 @@ func TestConstructionService_ConstructionPayloads(t *testing.T) { service := NewConstructionService(networkProvider) - response, err := service.ConstructionPayloads(context.Background(), + response, errTyped := service.ConstructionPayloads(context.Background(), &types.ConstructionPayloadsRequest{ Metadata: objectsMap{ "sender": testscommon.TestAddressAlice, @@ -317,7 +479,7 @@ func TestConstructionService_ConstructionPayloads(t *testing.T) { expectedTxJson := `{"nonce":42,"value":"1234","receiver":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx","sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th","gasPrice":1000000000,"gasLimit":57500,"data":"aGVsbG8=","chainID":"T","version":1}` - require.Nil(t, err) + require.Nil(t, errTyped) require.Len(t, response.Payloads, 1) require.Equal(t, expectedTxJson, response.UnsignedTransaction) require.Equal(t, []byte(expectedTxJson), response.Payloads[0].Bytes) @@ -332,32 +494,65 @@ func TestConstructionService_ConstructionParse(t *testing.T) { extension := newNetworkProviderExtension(networkProvider) service := NewConstructionService(networkProvider) - notSignedTx := `{"nonce":42,"value":"1234","receiver":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx","sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th","gasPrice":1100000000,"gasLimit":57500,"data":"aGVsbG8=","chainID":"T","version":1}` + t.Run("native transfer", func(t *testing.T) { + notSignedTx := `{"nonce":42,"value":"1234","receiver":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx","sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th","gasPrice":1100000000,"gasLimit":57500,"data":"aGVsbG8=","chainID":"T","version":1}` - operations := []*types.Operation{ - { - OperationIdentifier: indexToOperationIdentifier(0), - Type: opTransfer, - Account: addressToAccountIdentifier(testscommon.TestAddressAlice), - Amount: extension.valueToNativeAmount("-1234"), - }, - { - OperationIdentifier: indexToOperationIdentifier(1), - Type: opTransfer, - Account: addressToAccountIdentifier(testscommon.TestAddressBob), - Amount: extension.valueToNativeAmount("1234"), - }, - } + operations := []*types.Operation{ + { + OperationIdentifier: indexToOperationIdentifier(0), + Type: opTransfer, + Account: addressToAccountIdentifier(testscommon.TestAddressAlice), + Amount: extension.valueToNativeAmount("-1234"), + }, + { + OperationIdentifier: indexToOperationIdentifier(1), + Type: opTransfer, + Account: addressToAccountIdentifier(testscommon.TestAddressBob), + Amount: extension.valueToNativeAmount("1234"), + }, + } - response, err := service.ConstructionParse(context.Background(), - &types.ConstructionParseRequest{ - Signed: false, - Transaction: notSignedTx, - }, - ) - require.Nil(t, err) - require.Equal(t, operations, response.Operations) - require.Nil(t, response.AccountIdentifierSigners) + response, errTyped := service.ConstructionParse(context.Background(), + &types.ConstructionParseRequest{ + Signed: false, + Transaction: notSignedTx, + }, + ) + + require.Nil(t, errTyped) + require.Equal(t, operations, response.Operations) + require.Nil(t, response.AccountIdentifierSigners) + }) + + t.Run("custom transfer", func(t *testing.T) { + notSignedTx := `{"nonce":42,"value":"1234","receiver":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx","sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th","gasPrice":1100000000,"gasLimit":57500,"data":"RVNEVFRyYW5zZmVyQDU0NDU1MzU0MmQ2MTYyNjM2NDY1NjZAMDRkMg==","chainID":"T","version":1}` + + operations := []*types.Operation{ + { + OperationIdentifier: indexToOperationIdentifier(0), + Type: opCustomTransfer, + Account: addressToAccountIdentifier(testscommon.TestAddressAlice), + Amount: extension.valueToCustomAmount("-1234", "TEST-abcdef"), + }, + { + OperationIdentifier: indexToOperationIdentifier(1), + Type: opCustomTransfer, + Account: addressToAccountIdentifier(testscommon.TestAddressBob), + Amount: extension.valueToCustomAmount("1234", "TEST-abcdef"), + }, + } + + response, errTyped := service.ConstructionParse(context.Background(), + &types.ConstructionParseRequest{ + Signed: false, + Transaction: notSignedTx, + }, + ) + + require.Nil(t, errTyped) + require.Equal(t, operations, response.Operations) + require.Nil(t, response.AccountIdentifierSigners) + }) } func TestConstructionService_ConstructionCombine(t *testing.T) { @@ -368,7 +563,7 @@ func TestConstructionService_ConstructionCombine(t *testing.T) { notSignedTx := `{"nonce":42,"value":"1234","receiver":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx","sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th","gasPrice":1100000000,"gasLimit":57500,"data":"aGVsbG8=","chainID":"T","version":1}` - response, err := service.ConstructionCombine(context.Background(), + response, errTyped := service.ConstructionCombine(context.Background(), &types.ConstructionCombineRequest{ UnsignedTransaction: notSignedTx, Signatures: []*types.Signature{ @@ -380,7 +575,8 @@ func TestConstructionService_ConstructionCombine(t *testing.T) { ) signedTx := `{"nonce":42,"value":"1234","receiver":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx","sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th","gasPrice":1100000000,"gasLimit":57500,"data":"aGVsbG8=","signature":"aabb","chainID":"T","version":1}` - require.Nil(t, err) + + require.Nil(t, errTyped) require.Equal(t, signedTx, response.SignedTransaction) } @@ -390,7 +586,7 @@ func TestConstructionService_ConstructionDerive(t *testing.T) { networkProvider := testscommon.NewNetworkProviderMock() service := NewConstructionService(networkProvider) - response, err := service.ConstructionDerive(context.Background(), + response, errTyped := service.ConstructionDerive(context.Background(), &types.ConstructionDeriveRequest{ PublicKey: &types.PublicKey{ Bytes: testscommon.TestPubKeyAlice, @@ -399,7 +595,7 @@ func TestConstructionService_ConstructionDerive(t *testing.T) { }, ) - require.Nil(t, err) + require.Nil(t, errTyped) require.Equal(t, testscommon.TestAddressAlice, response.AccountIdentifier.Address) } @@ -412,12 +608,12 @@ func TestConstructionService_ConstructionHash(t *testing.T) { signedTx := `{"nonce":42,"value":"1234","receiver":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx","sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th","gasPrice":1100000000,"gasLimit":57500,"data":"aGVsbG8=","signature":"aabb","chainID":"T","version":1}` - response, err := service.ConstructionHash(context.Background(), + response, errTyped := service.ConstructionHash(context.Background(), &types.ConstructionHashRequest{ SignedTransaction: signedTx, }, ) - require.Nil(t, err) + require.Nil(t, errTyped) require.Equal(t, "aaaa", response.TransactionIdentifier.Hash) } @@ -436,12 +632,12 @@ func TestConstructionService_ConstructionSubmit(t *testing.T) { signedTx := `{"nonce":42,"value":"1234","receiver":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx","sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th","gasPrice":1100000000,"gasLimit":57500,"data":"aGVsbG8=","signature":"aabb","chainID":"T","version":1}` - response, err := service.ConstructionSubmit(context.Background(), + response, errTyped := service.ConstructionSubmit(context.Background(), &types.ConstructionSubmitRequest{ SignedTransaction: signedTx, }, ) - require.Nil(t, err) + require.Nil(t, errTyped) require.Equal(t, "aaaa", response.TransactionIdentifier.Hash) require.Equal(t, "T", calledWithTransaction.ChainID) require.Equal(t, uint64(42), calledWithTransaction.Nonce) @@ -475,6 +671,7 @@ func TestConstructionService_CreateOperationsFromPreparedTx(t *testing.T) { }, } - operations := service.createOperationsFromPreparedTx(preparedTx) + operations, err := service.createOperationsFromPreparedTx(preparedTx) + require.Nil(t, err) require.Equal(t, expectedOperations, operations) } diff --git a/server/services/networkProviderExtension.go b/server/services/networkProviderExtension.go index 842a0652..c2648dac 100644 --- a/server/services/networkProviderExtension.go +++ b/server/services/networkProviderExtension.go @@ -3,6 +3,7 @@ package services import ( "github.com/coinbase/rosetta-sdk-go/types" "github.com/multiversx/mx-chain-core-go/core" + "github.com/multiversx/mx-chain-rosetta/server/resources" ) type networkProviderExtension struct { @@ -23,13 +24,21 @@ func (extension *networkProviderExtension) valueToNativeAmount(value string) *ty } func (extension *networkProviderExtension) valueToCustomAmount(value string, currencySymbol string) *types.Amount { + currency, ok := extension.provider.GetCustomCurrencyBySymbol(currencySymbol) + if !ok { + log.Warn("valueToCustomAmount(): unknown currency", "symbol", currencySymbol) + + currency = resources.Currency{ + Symbol: currencySymbol, + Decimals: 0, + } + } + return &types.Amount{ Value: value, Currency: &types.Currency{ - Symbol: currencySymbol, - // Currently, we hardcode numDecimals to zero for custom currencies. - // TODO: Fix this once we have the information available. - Decimals: 0, + Symbol: currency.Symbol, + Decimals: currency.Decimals, }, } } diff --git a/systemtests/check_with_mesh_cli.py b/systemtests/check_with_mesh_cli.py index ea94143c..36d040cf 100644 --- a/systemtests/check_with_mesh_cli.py +++ b/systemtests/check_with_mesh_cli.py @@ -14,7 +14,7 @@ def main() -> int: parser = ArgumentParser() - parser.add_argument("--mode", choices=["data", "construction"], required=True) + parser.add_argument("--mode", choices=["data", "construction-native", "construction-custom"], required=True) parser.add_argument("--network", choices=CONFIGURATIONS.keys(), required=True) args = parser.parse_args() @@ -66,6 +66,7 @@ def run_rosetta(configuration: Configuration): f"--network-id={configuration.network_id}", f"--network-name={configuration.network_name}", f"--native-currency={configuration.native_currency}", + f"--config-custom-currencies={configuration.config_file_custom_currencies}", f"--first-historical-epoch={current_epoch}", f"--num-historical-epochs={configuration.num_historical_epochs}", ] @@ -87,13 +88,15 @@ def run_proxy_to_observer_adapter(configuration: Configuration): def run_mesh_cli(mode: str, configuration: Configuration): if mode == "data": return run_mesh_cli_with_check_data(configuration) - elif mode == "construction": - return run_mesh_cli_with_check_construction(configuration) + elif mode == "construction-native": + return run_mesh_cli_with_check_construction_native(configuration) + elif mode == "construction-custom": + return run_mesh_cli_with_check_construction_custom(configuration) else: raise ValueError(f"Unknown mode: {mode}") -def run_mesh_cli_with_check_construction(configuration: Configuration): +def run_mesh_cli_with_check_construction_native(configuration: Configuration): """ E.g. @@ -104,7 +107,19 @@ def run_mesh_cli_with_check_construction(configuration: Configuration): command = [ "rosetta-cli", "check:construction", - f"--configuration-file={configuration.check_construction_configuration_file}", + f"--configuration-file={configuration.check_construction_native_configuration_file}", + f"--online-url=http://localhost:{constants.PORT_ROSETTA}", + f"--offline-url=http://localhost:{constants.PORT_ROSETTA}", + ] + + return subprocess.Popen(command) + + +def run_mesh_cli_with_check_construction_custom(configuration: Configuration): + command = [ + "rosetta-cli", + "check:construction", + f"--configuration-file={configuration.check_construction_custom_configuration_file}", f"--online-url=http://localhost:{constants.PORT_ROSETTA}", f"--offline-url=http://localhost:{constants.PORT_ROSETTA}", ] diff --git a/systemtests/config.py b/systemtests/config.py index 3bf17709..2a4b2a34 100644 --- a/systemtests/config.py +++ b/systemtests/config.py @@ -7,9 +7,11 @@ class Configuration: network_id: str network_name: str native_currency: str + config_file_custom_currencies: str num_historical_epochs: int proxy_url: str - check_construction_configuration_file: str + check_construction_native_configuration_file: str + check_construction_custom_configuration_file: str check_data_configuration_file: str check_data_directory: str @@ -20,9 +22,11 @@ class Configuration: network_id="D", network_name="untitled", native_currency="EGLD", + config_file_custom_currencies="systemtests/rosetta_config/devnet-custom-currencies.json", num_historical_epochs=2, proxy_url="https://devnet-gateway.multiversx.com", - check_construction_configuration_file="systemtests/mesh_cli_config/devnet-construction.json", + check_construction_native_configuration_file="systemtests/mesh_cli_config/devnet-construction-native.json", + check_construction_custom_configuration_file="systemtests/mesh_cli_config/devnet-construction-custom.json", check_data_configuration_file="systemtests/mesh_cli_config/check-data.json", check_data_directory="systemtests/devnet-data", ), @@ -31,9 +35,11 @@ class Configuration: network_id="T", network_name="untitled", native_currency="EGLD", + config_file_custom_currencies="systemtests/rosetta_config/testnet-custom-currencies.json", num_historical_epochs=2, proxy_url="https://testnet-gateway.multiversx.com", - check_construction_configuration_file="systemtests/mesh_cli_config/testnet-construction.json", + check_construction_native_configuration_file="systemtests/mesh_cli_config/testnet-construction-native.json", + check_construction_custom_configuration_file="systemtests/mesh_cli_config/testnet-construction-custom.json", check_data_configuration_file="systemtests/mesh_cli_config/check-data.json", check_data_directory="systemtests/testnet-data", ), diff --git a/systemtests/mesh_cli_config/devnet-construction-custom.json b/systemtests/mesh_cli_config/devnet-construction-custom.json new file mode 100644 index 00000000..e7400b75 --- /dev/null +++ b/systemtests/mesh_cli_config/devnet-construction-custom.json @@ -0,0 +1,46 @@ +{ + "network": { + "blockchain": "MultiversX", + "network": "untitled" + }, + "data_directory": "", + "http_timeout": 200, + "max_retries": 1, + "max_online_connections": 120, + "max_sync_concurrency": 1, + "construction": { + "end_conditions": { + "transfer": 1 + }, + "stale_depth": 10, + "broadcast_limit": 5, + "constructor_dsl_file": "devnet-construction-custom.ros", + "prefunded_accounts": [ + { + "account_identifier": { + "address": "erd1ldjsdetjvegjdnda0qw2h62kq6rpvrklkc5pw9zxm0nwulfhtyqqtyc4vq" + }, + "privkey": "3e4e89e501eb542c12403fb15c52479e8721f2f4dedc3b3ef0f3b47b37de006c", + "curve_type": "edwards25519", + "currency": { + "symbol": "ROSETTA-2c0a37", + "decimals": 2 + } + }, + { + "account_identifier": { + "address": "erd1ldjsdetjvegjdnda0qw2h62kq6rpvrklkc5pw9zxm0nwulfhtyqqtyc4vq" + }, + "privkey": "3e4e89e501eb542c12403fb15c52479e8721f2f4dedc3b3ef0f3b47b37de006c", + "curve_type": "edwards25519", + "currency": { + "symbol": "EGLD", + "decimals": 18 + } + } + ] + }, + "data": { + "inactive_discrepancy_search_disabled": true + } +} diff --git a/systemtests/mesh_cli_config/devnet-construction-custom.ros b/systemtests/mesh_cli_config/devnet-construction-custom.ros new file mode 100644 index 00000000..1abda2bd --- /dev/null +++ b/systemtests/mesh_cli_config/devnet-construction-custom.ros @@ -0,0 +1,54 @@ +transfer(1){ + transfer{ + transfer.network = {"network":"untitled", "blockchain":"MultiversX"}; + custom_currency = {"symbol":"ROSETTA-2c0a37", "decimals":2}; + sender = { + "account_identifier": { + "address": "erd1ldjsdetjvegjdnda0qw2h62kq6rpvrklkc5pw9zxm0nwulfhtyqqtyc4vq" + }, + "currency": { + "symbol": "ROSETTA-2c0a37", + "decimals": 2 + } + }; + + max_fee = "50000000000000"; + max_transfer_amount = "12345"; + recipient_amount = random_number({"minimum": "1", "maximum": {{max_transfer_amount}}}); + + print_message({"recipient_amount":{{recipient_amount}}}); + + sender_amount = 0-{{recipient_amount}}; + recipient = { + "account_identifier": { + "address": "erd1xtslmt67utuewwv8jsx729mxjxaa8dvyyzp7492hy99dl7hvcuqq30l98v" + }, + "currency": { + "symbol": "ROSETTA-2c0a37", + "decimals": 2 + } + }; + transfer.confirmation_depth = "10"; + transfer.operations = [ + { + "operation_identifier":{"index":0}, + "type":"CustomTransfer", + "account":{{sender.account_identifier}}, + "amount":{ + "value":{{sender_amount}}, + "currency":{{custom_currency}} + } + }, + { + "operation_identifier":{"index":1}, + "related_operations": [{"index": 0}], + "type":"CustomTransfer", + "account":{{recipient.account_identifier}}, + "amount":{ + "value":{{recipient_amount}}, + "currency":{{custom_currency}} + } + } + ]; + } +} diff --git a/systemtests/mesh_cli_config/devnet-construction.json b/systemtests/mesh_cli_config/devnet-construction-native.json similarity index 92% rename from systemtests/mesh_cli_config/devnet-construction.json rename to systemtests/mesh_cli_config/devnet-construction-native.json index af953b92..711b1c6a 100644 --- a/systemtests/mesh_cli_config/devnet-construction.json +++ b/systemtests/mesh_cli_config/devnet-construction-native.json @@ -14,7 +14,7 @@ }, "stale_depth": 10, "broadcast_limit": 5, - "constructor_dsl_file": "devnet-construction.ros", + "constructor_dsl_file": "devnet-construction-native.ros", "prefunded_accounts": [ { "account_identifier": { diff --git a/systemtests/mesh_cli_config/devnet-construction.ros b/systemtests/mesh_cli_config/devnet-construction-native.ros similarity index 100% rename from systemtests/mesh_cli_config/devnet-construction.ros rename to systemtests/mesh_cli_config/devnet-construction-native.ros diff --git a/systemtests/mesh_cli_config/testnet-construction-custom.json b/systemtests/mesh_cli_config/testnet-construction-custom.json new file mode 100644 index 00000000..1c93e0d5 --- /dev/null +++ b/systemtests/mesh_cli_config/testnet-construction-custom.json @@ -0,0 +1,46 @@ +{ + "network": { + "blockchain": "MultiversX", + "network": "untitled" + }, + "data_directory": "", + "http_timeout": 200, + "max_retries": 1, + "max_online_connections": 120, + "max_sync_concurrency": 1, + "construction": { + "end_conditions": { + "transfer": 1 + }, + "stale_depth": 10, + "broadcast_limit": 5, + "constructor_dsl_file": "testnet-construction-custom.ros", + "prefunded_accounts": [ + { + "account_identifier": { + "address": "erd1ldjsdetjvegjdnda0qw2h62kq6rpvrklkc5pw9zxm0nwulfhtyqqtyc4vq" + }, + "privkey": "3e4e89e501eb542c12403fb15c52479e8721f2f4dedc3b3ef0f3b47b37de006c", + "curve_type": "edwards25519", + "currency": { + "symbol": "ROSETTA-7783de", + "decimals": 4 + } + }, + { + "account_identifier": { + "address": "erd1ldjsdetjvegjdnda0qw2h62kq6rpvrklkc5pw9zxm0nwulfhtyqqtyc4vq" + }, + "privkey": "3e4e89e501eb542c12403fb15c52479e8721f2f4dedc3b3ef0f3b47b37de006c", + "curve_type": "edwards25519", + "currency": { + "symbol": "EGLD", + "decimals": 18 + } + } + ] + }, + "data": { + "inactive_discrepancy_search_disabled": true + } +} diff --git a/systemtests/mesh_cli_config/testnet-construction-custom.ros b/systemtests/mesh_cli_config/testnet-construction-custom.ros new file mode 100644 index 00000000..fa70a322 --- /dev/null +++ b/systemtests/mesh_cli_config/testnet-construction-custom.ros @@ -0,0 +1,54 @@ +transfer(1){ + transfer{ + transfer.network = {"network":"untitled", "blockchain":"MultiversX"}; + custom_currency = {"symbol":"ROSETTA-7783de", "decimals":4}; + sender = { + "account_identifier": { + "address": "erd1ldjsdetjvegjdnda0qw2h62kq6rpvrklkc5pw9zxm0nwulfhtyqqtyc4vq" + }, + "currency": { + "symbol": "ROSETTA-7783de", + "decimals": 4 + } + }; + + max_fee = "50000000000000"; + max_transfer_amount = "12345"; + recipient_amount = random_number({"minimum": "1", "maximum": {{max_transfer_amount}}}); + + print_message({"recipient_amount":{{recipient_amount}}}); + + sender_amount = 0-{{recipient_amount}}; + recipient = { + "account_identifier": { + "address": "erd1xtslmt67utuewwv8jsx729mxjxaa8dvyyzp7492hy99dl7hvcuqq30l98v" + }, + "currency": { + "symbol": "ROSETTA-7783de", + "decimals": 4 + } + }; + transfer.confirmation_depth = "10"; + transfer.operations = [ + { + "operation_identifier":{"index":0}, + "type":"CustomTransfer", + "account":{{sender.account_identifier}}, + "amount":{ + "value":{{sender_amount}}, + "currency":{{custom_currency}} + } + }, + { + "operation_identifier":{"index":1}, + "related_operations": [{"index": 0}], + "type":"CustomTransfer", + "account":{{recipient.account_identifier}}, + "amount":{ + "value":{{recipient_amount}}, + "currency":{{custom_currency}} + } + } + ]; + } +} diff --git a/systemtests/mesh_cli_config/testnet-construction.json b/systemtests/mesh_cli_config/testnet-construction-native.json similarity index 92% rename from systemtests/mesh_cli_config/testnet-construction.json rename to systemtests/mesh_cli_config/testnet-construction-native.json index 30337b39..2113da29 100644 --- a/systemtests/mesh_cli_config/testnet-construction.json +++ b/systemtests/mesh_cli_config/testnet-construction-native.json @@ -14,7 +14,7 @@ }, "stale_depth": 10, "broadcast_limit": 5, - "constructor_dsl_file": "testnet-construction.ros", + "constructor_dsl_file": "testnet-construction-native.ros", "prefunded_accounts": [ { "account_identifier": { diff --git a/systemtests/mesh_cli_config/testnet-construction.ros b/systemtests/mesh_cli_config/testnet-construction-native.ros similarity index 100% rename from systemtests/mesh_cli_config/testnet-construction.ros rename to systemtests/mesh_cli_config/testnet-construction-native.ros diff --git a/systemtests/rosetta_config/devnet-custom-currencies.json b/systemtests/rosetta_config/devnet-custom-currencies.json new file mode 100644 index 00000000..be7ce62e --- /dev/null +++ b/systemtests/rosetta_config/devnet-custom-currencies.json @@ -0,0 +1,6 @@ +[ + { + "symbol": "ROSETTA-2c0a37", + "decimals": 2 + } +] diff --git a/systemtests/rosetta_config/mainnet-custom-currencies.json b/systemtests/rosetta_config/mainnet-custom-currencies.json new file mode 100644 index 00000000..18cffb72 --- /dev/null +++ b/systemtests/rosetta_config/mainnet-custom-currencies.json @@ -0,0 +1,14 @@ +[ + { + "symbol": "WEGLD-bd4d79", + "decimals": 18 + }, + { + "symbol": "MEX-455c57", + "decimals": 18 + }, + { + "symbol": "USDC-c76f1f", + "decimals": 6 + } +] diff --git a/systemtests/rosetta_config/testnet-custom-currencies.json b/systemtests/rosetta_config/testnet-custom-currencies.json new file mode 100644 index 00000000..6939ea7f --- /dev/null +++ b/systemtests/rosetta_config/testnet-custom-currencies.json @@ -0,0 +1,6 @@ +[ + { + "symbol": "ROSETTA-7783de", + "decimals": 4 + } +]