diff --git a/cmd/migration/config/config-mainnet-eth.toml b/cmd/migration/config/config-mainnet-eth.toml index a1a4d8ff..19fd556d 100644 --- a/cmd/migration/config/config-mainnet-eth.toml +++ b/cmd/migration/config/config-mainnet-eth.toml @@ -4,7 +4,7 @@ PrivateKeyFile = "keys/ethereum.sk" # the path to the file containing the relayer eth private key MultisigContractAddress = "0x1Ff78EB04d44a803E73c44FEf8790c5cAbD14596" SafeContractAddress = "0x92A26975433A61CF1134802586aa669bAB8B69f3" - GasLimitBase = 350000 + GasLimitBase = 400000 GasLimitForEach = 30000 [Eth.GasStation] Enabled = true diff --git a/cmd/migration/interface.go b/cmd/migration/interface.go index 086ccffc..7d71c7fc 100644 --- a/cmd/migration/interface.go +++ b/cmd/migration/interface.go @@ -2,7 +2,6 @@ package main import ( "context" - "math/big" "github.com/ethereum/go-ethereum/common" "github.com/multiversx/mx-bridge-eth-go/executors/ethereum" @@ -11,5 +10,5 @@ import ( // BatchCreator defines the operations implemented by an entity that can create an Ethereum batch message that can be used // in signing or transfer execution type BatchCreator interface { - CreateBatchInfo(ctx context.Context, newSafeAddress common.Address, partialMigration map[string]*big.Float) (*ethereum.BatchInfo, error) + CreateBatchInfo(ctx context.Context, newSafeAddress common.Address, partialMigration map[string]*ethereum.FloatWrapper) (*ethereum.BatchInfo, error) } diff --git a/cmd/migration/main.go b/cmd/migration/main.go index 5ff13500..bb60a9cd 100644 --- a/cmd/migration/main.go +++ b/cmd/migration/main.go @@ -6,12 +6,14 @@ import ( "encoding/hex" "encoding/json" "fmt" + "math/big" "os" "strings" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" + "github.com/multiversx/mx-bridge-eth-go/clients" ethereumClient "github.com/multiversx/mx-bridge-eth-go/clients/ethereum" "github.com/multiversx/mx-bridge-eth-go/clients/gasManagement" "github.com/multiversx/mx-bridge-eth-go/clients/gasManagement/factory" @@ -309,6 +311,11 @@ func executeTransfer(ctx *cli.Context, cfg config.MigrationToolConfig) error { return err } + err = waitForGasPrice(gs) + if err != nil { + return err + } + args := ethereum.ArgsMigrationBatchExecutor{ EthereumChainWrapper: components.ethereumChainWrapper, CryptoHandler: components.cryptoHandler, @@ -328,6 +335,28 @@ func executeTransfer(ctx *cli.Context, cfg config.MigrationToolConfig) error { return executor.ExecuteTransfer(context.Background()) } +func waitForGasPrice(gs clients.GasHandler) error { + log.Info("Fetching a gas price value. Please wait...") + numRetries := 5 + timeBetweenChecks := time.Second + + var err error + var gasPrice *big.Int + for i := 0; i < numRetries; i++ { + time.Sleep(timeBetweenChecks) + gasPrice, err = gs.GetCurrentGasPrice() + if err != nil { + log.Debug("waitForGasPrice", "error", err) + continue + } + + log.Info("Fetched the gas price", "value", gasPrice.String()) + return nil + } + + return err +} + func loadConfig(filepath string) (config.MigrationToolConfig, error) { cfg := config.MigrationToolConfig{} err := chainCore.LoadTomlFile(&cfg, filepath) diff --git a/cmd/scCallsExecutor/config/config.toml b/cmd/scCallsExecutor/config/config.toml index b9a18034..9f60880c 100644 --- a/cmd/scCallsExecutor/config/config.toml +++ b/cmd/scCallsExecutor/config/config.toml @@ -1,4 +1,6 @@ -ScProxyBech32Address = "erd1qqqqqqqqqqqqqpgqnef5f5aq32d63kljld8w5vnvz4gk5sy9hrrq2ld08s" +ScProxyBech32Addresses = [ + "erd1qqqqqqqqqqqqqpgqnef5f5aq32d63kljld8w5vnvz4gk5sy9hrrq2ld08s", +] ExtraGasToExecute = 60000000 # this value allow the SC calls without provided gas limit to be refunded MaxGasLimitToUse = 249999999 # this is a safe max gas limit to use both intra-shard & cross-shard GasLimitForOutOfGasTransactions = 30000000 # this value will be used when a transaction specified a gas limit > 249999999 diff --git a/cmd/scCallsExecutor/flags.go b/cmd/scCallsExecutor/flags.go index 658fd4eb..a5313054 100644 --- a/cmd/scCallsExecutor/flags.go +++ b/cmd/scCallsExecutor/flags.go @@ -73,11 +73,6 @@ var ( Name: "network-address", Usage: "The network address (gateway) to be used. Example: 'https://testnet-explorer.multiversx.com'", } - // scProxyBech32Address is the smart contract address used to interact with this tool - scProxyBech32Address = cli.StringFlag{ - Name: "sc-proxy-address", - Usage: "The smart contract address in bech32 format to interact with", - } // privateKeyFile is the MultiversX private key file used to issue transaction for the SC calls privateKeyFile = cli.StringFlag{ Name: "private-key-file", @@ -96,7 +91,6 @@ func getFlags() []cli.Flag { profileMode, restApiInterface, networkAddress, - scProxyBech32Address, privateKeyFile, } } diff --git a/cmd/scCallsExecutor/main.go b/cmd/scCallsExecutor/main.go index d1493199..7b929780 100644 --- a/cmd/scCallsExecutor/main.go +++ b/cmd/scCallsExecutor/main.go @@ -93,10 +93,6 @@ func startExecutor(ctx *cli.Context, version string) error { } } - if ctx.IsSet(scProxyBech32Address.Name) { - cfg.ScProxyBech32Address = ctx.GlobalString(scProxyBech32Address.Name) - log.Info("using flag-defined SC proxy address", "address", cfg.ScProxyBech32Address) - } if ctx.IsSet(networkAddress.Name) { cfg.NetworkAddress = ctx.GlobalString(networkAddress.Name) log.Info("using flag-defined network address", "address", cfg.NetworkAddress) @@ -111,7 +107,7 @@ func startExecutor(ctx *cli.Context, version string) error { } args := config.ScCallsModuleConfig{ - ScProxyBech32Address: cfg.ScProxyBech32Address, + ScProxyBech32Addresses: cfg.ScProxyBech32Addresses, ExtraGasToExecute: cfg.ExtraGasToExecute, MaxGasLimitToUse: cfg.MaxGasLimitToUse, GasLimitForOutOfGasTransactions: cfg.GasLimitForOutOfGasTransactions, diff --git a/config/config.go b/config/config.go index cce4ce7d..a91ebfda 100644 --- a/config/config.go +++ b/config/config.go @@ -192,7 +192,7 @@ type PendingOperationsFilterConfig struct { // ScCallsModuleConfig will hold the settings for the SC calls module type ScCallsModuleConfig struct { - ScProxyBech32Address string + ScProxyBech32Addresses []string ExtraGasToExecute uint64 MaxGasLimitToUse uint64 GasLimitForOutOfGasTransactions uint64 diff --git a/config/tomlConfigs_test.go b/config/tomlConfigs_test.go index 1dedb717..057ef77a 100644 --- a/config/tomlConfigs_test.go +++ b/config/tomlConfigs_test.go @@ -405,7 +405,10 @@ func TestScCallsExecutorConfigs(t *testing.T) { t.Parallel() expectedConfig := ScCallsModuleConfig{ - ScProxyBech32Address: "erd1qqqqqqqqqqqqqpgqnef5f5aq32d63kljld8w5vnvz4gk5sy9hrrq2ld08s", + ScProxyBech32Addresses: []string{ + "erd1qqqqqqqqqqqqqpgqnef5f5aq32d63kljld8w5vnvz4gk5sy9hrrq2ld08s", + "erd1qqqqqqqqqqqqqpgqzyuaqg3dl7rqlkudrsnm5ek0j3a97qevd8sszj0glf", + }, ExtraGasToExecute: 50000000, MaxGasLimitToUse: 249999999, GasLimitForOutOfGasTransactions: 30000000, @@ -436,7 +439,10 @@ func TestScCallsExecutorConfigs(t *testing.T) { } testString := ` -ScProxyBech32Address = "erd1qqqqqqqqqqqqqpgqnef5f5aq32d63kljld8w5vnvz4gk5sy9hrrq2ld08s" +ScProxyBech32Addresses = [ + "erd1qqqqqqqqqqqqqpgqnef5f5aq32d63kljld8w5vnvz4gk5sy9hrrq2ld08s", + "erd1qqqqqqqqqqqqqpgqzyuaqg3dl7rqlkudrsnm5ek0j3a97qevd8sszj0glf", +] ExtraGasToExecute = 50000000 MaxGasLimitToUse = 249999999 # this is a safe max gas limit to use both intra-shard & cross-shard GasLimitForOutOfGasTransactions = 30000000 # this value will be used when a transaction specified a gas limit > 249999999 diff --git a/executors/ethereum/common.go b/executors/ethereum/common.go index c4a93cdb..def84b8e 100644 --- a/executors/ethereum/common.go +++ b/executors/ethereum/common.go @@ -37,6 +37,14 @@ type SignatureInfo struct { Signature string `json:"Signature"` } +// FloatWrapper is a wrapper of the big.Float that supports specifying if the value is maximum +type FloatWrapper struct { + *big.Float + IsMax bool +} + +var maxValues = []string{"all", "max", "*"} + // TokensBalancesDisplayString will convert the deposit balances into a human-readable string func TokensBalancesDisplayString(batchInfo *BatchInfo) string { maxTokenLen := 0 @@ -69,12 +77,12 @@ func TokensBalancesDisplayString(batchInfo *BatchInfo) string { } // ConvertPartialMigrationStringToMap converts the partial migration string to its map representation -func ConvertPartialMigrationStringToMap(partialMigration string) (map[string]*big.Float, error) { +func ConvertPartialMigrationStringToMap(partialMigration string) (map[string]*FloatWrapper, error) { partsSeparator := "," tokenAmountSeparator := ":" parts := strings.Split(partialMigration, partsSeparator) - partialMap := make(map[string]*big.Float) + partialMap := make(map[string]*FloatWrapper) for _, part := range parts { part = strings.Trim(part, " \t\n") splt := strings.Split(part, tokenAmountSeparator) @@ -82,19 +90,48 @@ func ConvertPartialMigrationStringToMap(partialMigration string) (map[string]*bi return nil, fmt.Errorf("%w at token %s, invalid format", errInvalidPartialMigrationString, part) } + token := splt[0] + if isMaxValueString(splt[1]) { + partialMap[token] = &FloatWrapper{ + Float: big.NewFloat(0), + IsMax: true, + } + + continue + } + amount, ok := big.NewFloat(0).SetString(splt[1]) if !ok { return nil, fmt.Errorf("%w at token %s, not a number", errInvalidPartialMigrationString, part) } - token := splt[0] if partialMap[token] == nil { - partialMap[token] = big.NewFloat(0).Set(amount) + partialMap[token] = &FloatWrapper{ + Float: big.NewFloat(0).Set(amount), + IsMax: false, + } continue } - partialMap[token].Add(partialMap[token], amount) + if partialMap[token].IsMax { + // do not attempt to add something to an already max float + continue + } + + partialMap[token].Add(partialMap[token].Float, amount) } return partialMap, nil } + +func isMaxValueString(value string) bool { + value = strings.ToLower(value) + + for _, maxValue := range maxValues { + if value == maxValue { + return true + } + } + + return false +} diff --git a/executors/ethereum/common_test.go b/executors/ethereum/common_test.go index 12b75a1f..d5c55b13 100644 --- a/executors/ethereum/common_test.go +++ b/executors/ethereum/common_test.go @@ -120,11 +120,46 @@ func TestConvertPartialMigrationStringToMap(t *testing.T) { results, err := ConvertPartialMigrationStringToMap(str) assert.Nil(t, err) - expectedResults := map[string]*big.Float{ - "k": big.NewFloat(3.2), - "f": big.NewFloat(1), - "g": big.NewFloat(0.001), - "h": big.NewFloat(0), + expectedResults := map[string]*FloatWrapper{ + "k": {Float: big.NewFloat(3.2)}, + "f": {Float: big.NewFloat(1)}, + "g": {Float: big.NewFloat(0.001)}, + "h": {Float: big.NewFloat(0)}, + } + + assert.Nil(t, err) + assert.Equal(t, expectedResults, results) + }) + t.Run("should work with maximum available", func(t *testing.T) { + t.Parallel() + + str := "k:1,l:0,m:*,n:AlL,o:MaX" + results, err := ConvertPartialMigrationStringToMap(str) + assert.Nil(t, err) + + expectedResults := map[string]*FloatWrapper{ + "k": {Float: big.NewFloat(1)}, + "l": {Float: big.NewFloat(0), IsMax: false}, + "m": {Float: big.NewFloat(0), IsMax: true}, + "n": {Float: big.NewFloat(0), IsMax: true}, + "o": {Float: big.NewFloat(0), IsMax: true}, + } + + assert.Nil(t, err) + assert.Equal(t, expectedResults, results) + }) + t.Run("should not add on a token with max value", func(t *testing.T) { + t.Parallel() + + str := "k:1,l:0,m:*,k:*,n:1,k:4" + results, err := ConvertPartialMigrationStringToMap(str) + assert.Nil(t, err) + + expectedResults := map[string]*FloatWrapper{ + "k": {Float: big.NewFloat(0), IsMax: true}, + "l": {Float: big.NewFloat(0), IsMax: false}, + "m": {Float: big.NewFloat(0), IsMax: true}, + "n": {Float: big.NewFloat(1), IsMax: false}, } assert.Nil(t, err) diff --git a/executors/ethereum/migrationBatchCreator.go b/executors/ethereum/migrationBatchCreator.go index 9e88271f..f3cf7fad 100644 --- a/executors/ethereum/migrationBatchCreator.go +++ b/executors/ethereum/migrationBatchCreator.go @@ -60,7 +60,7 @@ func NewMigrationBatchCreator(args ArgsMigrationBatchCreator) (*migrationBatchCr } // CreateBatchInfo creates an instance of type BatchInfo -func (creator *migrationBatchCreator) CreateBatchInfo(ctx context.Context, newSafeAddress common.Address, partialMigration map[string]*big.Float) (*BatchInfo, error) { +func (creator *migrationBatchCreator) CreateBatchInfo(ctx context.Context, newSafeAddress common.Address, partialMigration map[string]*FloatWrapper) (*BatchInfo, error) { creator.logger.Info("started the batch creation process...") depositStart := uint64(0) // deposits inside a batch are not tracked, we can start from 0 @@ -78,7 +78,7 @@ func (creator *migrationBatchCreator) CreateBatchInfo(ctx context.Context, newSa "free batch ID", freeBatchID, "time took", endTime.Sub(startTime)) if partialMigration == nil { - partialMigration = make(map[string]*big.Float) + partialMigration = make(map[string]*FloatWrapper) } tokensList, err := creator.getTokensList(ctx, partialMigration) @@ -175,7 +175,7 @@ func (creator *migrationBatchCreator) checkAvailableBatch( return nil } -func (creator *migrationBatchCreator) getTokensList(ctx context.Context, partialMigration map[string]*big.Float) ([]string, error) { +func (creator *migrationBatchCreator) getTokensList(ctx context.Context, partialMigration map[string]*FloatWrapper) ([]string, error) { tokens, err := creator.mvxDataGetter.GetAllKnownTokens(ctx) if err != nil { return nil, err @@ -224,7 +224,7 @@ func (creator *migrationBatchCreator) fetchERC20ContractsAddresses(ctx context.C return deposits, nil } -func (creator *migrationBatchCreator) fetchBalances(ctx context.Context, deposits []*DepositInfo, partialMigration map[string]*big.Float) error { +func (creator *migrationBatchCreator) fetchBalances(ctx context.Context, deposits []*DepositInfo, partialMigration map[string]*FloatWrapper) error { for _, deposit := range deposits { balance, err := creator.erc20ContractsHolder.BalanceOf(ctx, deposit.ContractAddress, creator.safeContractAddress) if err != nil { @@ -237,9 +237,10 @@ func (creator *migrationBatchCreator) fetchBalances(ctx context.Context, deposit } deposit.Decimals = decimals - trimAmount := partialMigration[deposit.Token] - if trimAmount != nil { - denominatedTrimAmount := big.NewFloat(0).Set(trimAmount) + trimValue := partialMigration[deposit.Token] + trimIsNeeded := trimValue != nil && !trimValue.IsMax + if trimIsNeeded { + denominatedTrimAmount := big.NewFloat(0).Set(trimValue.Float) multiplier := big.NewInt(10) multiplier.Exp(multiplier, big.NewInt(int64(deposit.Decimals)), nil) denominatedTrimAmount.Mul(denominatedTrimAmount, big.NewFloat(0).SetInt(multiplier)) diff --git a/executors/ethereum/migrationBatchCreator_test.go b/executors/ethereum/migrationBatchCreator_test.go index 277aa319..7a9fe0de 100644 --- a/executors/ethereum/migrationBatchCreator_test.go +++ b/executors/ethereum/migrationBatchCreator_test.go @@ -561,11 +561,65 @@ func TestMigrationBatchCreator_CreateBatchInfo(t *testing.T) { expectedBatch.DepositsInfo[1].DenominatedAmount, _ = big.NewFloat(0).SetString("0.000000000000000020") expectedBatch.DepositsInfo[2].DenominatedAmount, _ = big.NewFloat(0).SetString("120") - partialMap := map[string]*big.Float{ - "tkn1": big.NewFloat(0.017), - "tkn3": big.NewFloat(120), + token2Value, _ := big.NewFloat(0).SetString("0.000000000000000020") + partialMap := map[string]*FloatWrapper{ + "tkn1": {Float: big.NewFloat(0.017)}, + "tkn2": {Float: token2Value}, + "tkn3": {Float: big.NewFloat(120)}, + } + + batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, partialMap) + assert.Nil(t, err) + assert.Equal(t, expectedBatch, batch) + }) + t.Run("with trim and all quantity", func(t *testing.T) { + expectedBatch := &BatchInfo{ + OldSafeContractAddress: safeContractAddress.String(), + NewSafeContractAddress: newSafeContractAddress.String(), + BatchID: firstFreeBatchId, + MessageHash: common.HexToHash("0x8c1b5bb16418a3dec1990fe8c0cd8363e9e56ca6870b6c7f0e7496f4411f60b0"), + DepositsInfo: []*DepositInfo{ + { + DepositNonce: 1, + Token: "tkn1", + ContractAddressString: common.BytesToAddress(tkn1Erc20Address).String(), + ContractAddress: common.BytesToAddress(tkn1Erc20Address), + Amount: big.NewInt(17), + AmountString: "17", + DenominatedAmountString: "0.017", + Decimals: 3, + }, + { + DepositNonce: 2, + Token: "tkn2", + ContractAddressString: common.BytesToAddress(tkn2Erc20Address).String(), + ContractAddress: common.BytesToAddress(tkn2Erc20Address), + Amount: big.NewInt(38), + AmountString: "38", + DenominatedAmountString: "0.000000000000000038", + Decimals: 18, + }, + { + DepositNonce: 3, + Token: "tkn3", + ContractAddressString: common.BytesToAddress(tkn3Erc20Address).String(), + ContractAddress: common.BytesToAddress(tkn3Erc20Address), + Amount: big.NewInt(120), + AmountString: "120", + DenominatedAmountString: "120", + Decimals: 0, + }, + }, + } + expectedBatch.DepositsInfo[0].DenominatedAmount, _ = big.NewFloat(0).SetString("0.017") + expectedBatch.DepositsInfo[1].DenominatedAmount, _ = big.NewFloat(0).SetString("0.000000000000000038") + expectedBatch.DepositsInfo[2].DenominatedAmount, _ = big.NewFloat(0).SetString("120") + + partialMap := map[string]*FloatWrapper{ + "tkn1": {Float: big.NewFloat(0.017)}, + "tkn2": {Float: big.NewFloat(0), IsMax: true}, + "tkn3": {Float: big.NewFloat(120)}, } - partialMap["tkn2"], _ = big.NewFloat(0).SetString("0.000000000000000020") batch, err := creator.CreateBatchInfo(context.Background(), newSafeContractAddress, partialMap) assert.Nil(t, err) diff --git a/executors/multiversx/errors.go b/executors/multiversx/errors.go index 4b6339e9..9c7ee755 100644 --- a/executors/multiversx/errors.go +++ b/executors/multiversx/errors.go @@ -15,4 +15,5 @@ var ( errNilCloseAppChannel = errors.New("nil close application channel") errTransactionFailed = errors.New("transaction failed") errGasLimitIsLessThanAbsoluteMinimum = errors.New("provided gas limit is less than absolute minimum required") + errEmptyListOfBridgeSCProxy = errors.New("the bridge SC proxy addresses list is empty") ) diff --git a/executors/multiversx/module/scCallsModule.go b/executors/multiversx/module/scCallsModule.go index 19d2ff1b..4d1733bd 100644 --- a/executors/multiversx/module/scCallsModule.go +++ b/executors/multiversx/module/scCallsModule.go @@ -73,7 +73,7 @@ func NewScCallsModule(cfg config.ScCallsModuleConfig, log logger.Logger, chClose } argsExecutor := multiversx.ArgsScCallExecutor{ - ScProxyBech32Address: cfg.ScProxyBech32Address, + ScProxyBech32Addresses: cfg.ScProxyBech32Addresses, Proxy: proxy, Codec: &parsers.MultiversxCodec{}, Filter: filter, diff --git a/executors/multiversx/module/scCallsModule_test.go b/executors/multiversx/module/scCallsModule_test.go index 246e25ea..cb5f7467 100644 --- a/executors/multiversx/module/scCallsModule_test.go +++ b/executors/multiversx/module/scCallsModule_test.go @@ -11,7 +11,9 @@ import ( func createTestConfigs() config.ScCallsModuleConfig { return config.ScCallsModuleConfig{ - ScProxyBech32Address: "erd1qqqqqqqqqqqqqpgqgftcwj09u0nhmskrw7xxqcqh8qmzwyexd8ss7ftcxx", + ScProxyBech32Addresses: []string{ + "erd1qqqqqqqqqqqqqpgqgftcwj09u0nhmskrw7xxqcqh8qmzwyexd8ss7ftcxx", + }, ExtraGasToExecute: 6000000, MaxGasLimitToUse: 249999999, GasLimitForOutOfGasTransactions: 30000000, @@ -104,10 +106,28 @@ func TestNewScCallsModule(t *testing.T) { err = module.Close() assert.Nil(t, err) }) - t.Run("should work with nil close app chan", func(t *testing.T) { + t.Run("should work with not nil close app chan", func(t *testing.T) { + t.Parallel() + + cfg := createTestConfigs() + cfg.TransactionChecks.CheckTransactionResults = true + cfg.TransactionChecks.TimeInSecondsBetweenChecks = 1 + cfg.TransactionChecks.ExecutionTimeoutInSeconds = 1 + cfg.TransactionChecks.CloseAppOnError = true + module, err := NewScCallsModule(cfg, &testsCommon.LoggerStub{}, make(chan struct{}, 1)) + assert.Nil(t, err) + assert.NotNil(t, module) + + assert.Zero(t, module.GetNumSentTransaction()) + + err = module.Close() + assert.Nil(t, err) + }) + t.Run("should work with not nil close app chan and 2 sc proxy addresses", func(t *testing.T) { t.Parallel() cfg := createTestConfigs() + cfg.ScProxyBech32Addresses = append(cfg.ScProxyBech32Addresses, "erd1qqqqqqqqqqqqqpgqzyuaqg3dl7rqlkudrsnm5ek0j3a97qevd8sszj0glf") cfg.TransactionChecks.CheckTransactionResults = true cfg.TransactionChecks.TimeInSecondsBetweenChecks = 1 cfg.TransactionChecks.ExecutionTimeoutInSeconds = 1 diff --git a/executors/multiversx/scCallsExecutor.go b/executors/multiversx/scCallsExecutor.go index a573c1c3..a622730a 100644 --- a/executors/multiversx/scCallsExecutor.go +++ b/executors/multiversx/scCallsExecutor.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "math/big" + "strings" "sync/atomic" "time" @@ -33,7 +34,7 @@ const ( // ArgsScCallExecutor represents the DTO struct for creating a new instance of type scCallExecutor type ArgsScCallExecutor struct { - ScProxyBech32Address string + ScProxyBech32Addresses []string Proxy Proxy Codec Codec Filter ScCallsExecuteFilter @@ -49,7 +50,7 @@ type ArgsScCallExecutor struct { } type scCallExecutor struct { - scProxyBech32Address string + scProxyBech32Addresses []string proxy Proxy codec Codec filter ScCallsExecuteFilter @@ -85,7 +86,7 @@ func NewScCallExecutor(args ArgsScCallExecutor) (*scCallExecutor, error) { senderAddress := data.NewAddressFromBytes(publicKeyBytes) return &scCallExecutor{ - scProxyBech32Address: args.ScProxyBech32Address, + scProxyBech32Addresses: args.ScProxyBech32Addresses, proxy: args.Proxy, codec: args.Codec, filter: args.Filter, @@ -139,9 +140,18 @@ func checkArgs(args ArgsScCallExecutor) error { return err } - _, err = data.NewAddressFromBech32String(args.ScProxyBech32Address) + if len(args.ScProxyBech32Addresses) == 0 { + return errEmptyListOfBridgeSCProxy + } - return err + for _, scProxyAddress := range args.ScProxyBech32Addresses { + _, err = data.NewAddressFromBech32String(scProxyAddress) + if err != nil { + return fmt.Errorf("%w for address %s", err, scProxyAddress) + } + } + + return nil } func checkTransactionChecksConfig(args ArgsScCallExecutor) error { @@ -167,19 +177,36 @@ func checkTransactionChecksConfig(args ArgsScCallExecutor) error { // Execute will execute one step: get all pending operations, call the filter and send execution transactions func (executor *scCallExecutor) Execute(ctx context.Context) error { - pendingOperations, err := executor.getPendingOperations(ctx) + errorStrings := make([]string, 0) + for _, scProxyAddress := range executor.scProxyBech32Addresses { + err := executor.executeForScProxyAddress(ctx, scProxyAddress) + if err != nil { + errorStrings = append(errorStrings, err.Error()) + } + } + + if len(errorStrings) == 0 { + return nil + } + + return fmt.Errorf("errors found during execution: %s", strings.Join(errorStrings, "\n")) +} + +func (executor *scCallExecutor) executeForScProxyAddress(ctx context.Context, scProxyAddress string) error { + executor.log.Info("Executing for the SC proxy address", "address", scProxyAddress) + pendingOperations, err := executor.getPendingOperations(ctx, scProxyAddress) if err != nil { return err } filteredPendingOperations := executor.filterOperations(pendingOperations) - return executor.executeOperations(ctx, filteredPendingOperations) + return executor.executeOperations(ctx, filteredPendingOperations, scProxyAddress) } -func (executor *scCallExecutor) getPendingOperations(ctx context.Context) (map[uint64]parsers.ProxySCCompleteCallData, error) { +func (executor *scCallExecutor) getPendingOperations(ctx context.Context, scProxyAddress string) (map[uint64]parsers.ProxySCCompleteCallData, error) { request := &data.VmValueRequest{ - Address: executor.scProxyBech32Address, + Address: scProxyAddress, FuncName: getPendingTransactionsFunction, } @@ -236,7 +263,11 @@ func (executor *scCallExecutor) filterOperations(pendingOperations map[uint64]pa return result } -func (executor *scCallExecutor) executeOperations(ctx context.Context, pendingOperations map[uint64]parsers.ProxySCCompleteCallData) error { +func (executor *scCallExecutor) executeOperations( + ctx context.Context, + pendingOperations map[uint64]parsers.ProxySCCompleteCallData, + scProxyAddress string, +) error { networkConfig, err := executor.proxy.GetNetworkConfig(ctx) if err != nil { return fmt.Errorf("%w while fetching network configs", err) @@ -247,7 +278,7 @@ func (executor *scCallExecutor) executeOperations(ctx context.Context, pendingOp executor.log.Debug("scCallExecutor.executeOperations", "executing ID", id, "call data", callData, "maximum timeout", executor.executionTimeout) - err = executor.executeOperation(workingCtx, id, callData, networkConfig) + err = executor.executeOperation(workingCtx, id, callData, networkConfig, scProxyAddress) cancel() if err != nil { @@ -263,6 +294,7 @@ func (executor *scCallExecutor) executeOperation( id uint64, callData parsers.ProxySCCompleteCallData, networkConfig *data.NetworkConfig, + scProxyAddress string, ) error { txBuilder := builders.NewTxDataBuilder() txBuilder.Function(scProxyCallFunction).ArgInt64(int64(id)) @@ -290,7 +322,7 @@ func (executor *scCallExecutor) executeOperation( GasLimit: gasLimit + executor.extraGasToExecute, Data: dataBytes, Sender: bech32Address, - Receiver: executor.scProxyBech32Address, + Receiver: scProxyAddress, Value: "0", } diff --git a/executors/multiversx/scCallsExecutor_test.go b/executors/multiversx/scCallsExecutor_test.go index bc9e8b30..e6b1b638 100644 --- a/executors/multiversx/scCallsExecutor_test.go +++ b/executors/multiversx/scCallsExecutor_test.go @@ -29,7 +29,9 @@ var testCodec = &testsCommon.TestMultiversXCodec{} func createMockArgsScCallExecutor() ArgsScCallExecutor { return ArgsScCallExecutor{ - ScProxyBech32Address: "erd1qqqqqqqqqqqqqpgqk839entmk46ykukvhpn90g6knskju3dtanaq20f66e", + ScProxyBech32Addresses: []string{ + "erd1qqqqqqqqqqqqqpgqk839entmk46ykukvhpn90g6knskju3dtanaq20f66e", + }, Proxy: &interactors.ProxyStub{}, Codec: &testsCommon.MultiversxCodecStub{}, Filter: &testsCommon.ScCallsExecuteFilterStub{}, @@ -146,11 +148,21 @@ func TestNewScCallExecutor(t *testing.T) { assert.Nil(t, executor) assert.Equal(t, errNilSingleSigner, err) }) + t.Run("empty list of sc proxy bech32 addresses should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsScCallExecutor() + args.ScProxyBech32Addresses = nil + + executor, err := NewScCallExecutor(args) + assert.Nil(t, executor) + assert.Equal(t, errEmptyListOfBridgeSCProxy, err) + }) t.Run("invalid sc proxy bech32 address should error", func(t *testing.T) { t.Parallel() args := createMockArgsScCallExecutor() - args.ScProxyBech32Address = "not a valid bech32 address" + args.ScProxyBech32Addresses = append(args.ScProxyBech32Addresses, "not a valid bech32 address") executor, err := NewScCallExecutor(args) assert.Nil(t, executor) @@ -279,7 +291,9 @@ func TestScCallExecutor_Execute(t *testing.T) { executor, _ := NewScCallExecutor(args) err := executor.Execute(context.Background()) - assert.Equal(t, expectedError, err) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), expectedError.Error()) + assert.Contains(t, err.Error(), "errors found during execution") assert.Zero(t, executor.GetNumSentTransaction()) }) t.Run("get pending returns a not ok status, should error", func(t *testing.T) { @@ -321,7 +335,9 @@ func TestScCallExecutor_Execute(t *testing.T) { executor, _ := NewScCallExecutor(args) err := executor.Execute(context.Background()) - assert.ErrorIs(t, err, errInvalidNumberOfResponseLines) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), errInvalidNumberOfResponseLines.Error()) + assert.Contains(t, err.Error(), "errors found during execution") assert.Contains(t, err.Error(), "expected an even number, got 1") assert.Zero(t, executor.GetNumSentTransaction()) }) @@ -354,7 +370,9 @@ func TestScCallExecutor_Execute(t *testing.T) { executor, _ := NewScCallExecutor(args) err := executor.Execute(context.Background()) - assert.ErrorIs(t, err, expectedError) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), expectedError.Error()) + assert.Contains(t, err.Error(), "errors found during execution") assert.Zero(t, executor.GetNumSentTransaction()) }) t.Run("get network configs errors, should error", func(t *testing.T) { @@ -389,7 +407,9 @@ func TestScCallExecutor_Execute(t *testing.T) { executor, _ := NewScCallExecutor(args) err := executor.Execute(context.Background()) - assert.ErrorIs(t, err, expectedError) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), expectedError.Error()) + assert.Contains(t, err.Error(), "errors found during execution") assert.Zero(t, executor.GetNumSentTransaction()) }) t.Run("ApplyNonceAndGasPrice errors, should error", func(t *testing.T) { @@ -433,7 +453,9 @@ func TestScCallExecutor_Execute(t *testing.T) { executor, _ := NewScCallExecutor(args) err := executor.Execute(context.Background()) - assert.ErrorIs(t, err, expectedError) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), expectedError.Error()) + assert.Contains(t, err.Error(), "errors found during execution") assert.Zero(t, executor.GetNumSentTransaction()) }) t.Run("Sign errors, should error", func(t *testing.T) { @@ -482,7 +504,9 @@ func TestScCallExecutor_Execute(t *testing.T) { executor, _ := NewScCallExecutor(args) err := executor.Execute(context.Background()) - assert.ErrorIs(t, err, expectedError) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), expectedError.Error()) + assert.Contains(t, err.Error(), "errors found during execution") assert.Zero(t, executor.GetNumSentTransaction()) }) t.Run("SendTransaction errors, should error", func(t *testing.T) { @@ -530,10 +554,12 @@ func TestScCallExecutor_Execute(t *testing.T) { executor, _ := NewScCallExecutor(args) err := executor.Execute(context.Background()) - assert.ErrorIs(t, err, expectedError) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), expectedError.Error()) + assert.Contains(t, err.Error(), "errors found during execution") assert.Equal(t, uint32(0), executor.GetNumSentTransaction()) }) - t.Run("should work", func(t *testing.T) { + t.Run("should work with one SC proxy address", func(t *testing.T) { t.Parallel() args := createMockArgsScCallExecutor() @@ -548,7 +574,7 @@ func TestScCallExecutor_Execute(t *testing.T) { args.Proxy = &interactors.ProxyStub{ ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { - assert.Equal(t, args.ScProxyBech32Address, vmRequest.Address) + assert.Equal(t, args.ScProxyBech32Addresses[0], vmRequest.Address) assert.Equal(t, getPendingTransactionsFunction, vmRequest.FuncName) return &data.VmValuesResponseData{ @@ -617,7 +643,7 @@ func TestScCallExecutor_Execute(t *testing.T) { assert.Equal(t, "erd1qqqqqqqqqqqqqpgqk839entmk46ykukvhpn90g6knskju3dtanaq20f66e", tx.Receiver) assert.Equal(t, "0", tx.Value) - // only the second pending operation gor through the filter + // only the second pending operation got through the filter expectedData := scProxyCallFunction + "@02" assert.Equal(t, expectedData, string(tx.Data)) @@ -640,6 +666,144 @@ func TestScCallExecutor_Execute(t *testing.T) { assert.Equal(t, uint32(1), executor.GetNumSentTransaction()) assert.True(t, processTransactionStatusCalled) }) + t.Run("should work with one two proxy address", func(t *testing.T) { + t.Parallel() + + args := createMockArgsScCallExecutor() + pk := args.PrivateKey.GeneratePublic() + pkBuff, _ := pk.ToByteArray() + sender := data.NewAddressFromBytes(pkBuff) + senderAddress, _ := sender.AddressAsBech32String() + + args.ScProxyBech32Addresses = append(args.ScProxyBech32Addresses, "erd1qqqqqqqqqqqqqpgqzyuaqg3dl7rqlkudrsnm5ek0j3a97qevd8sszj0glf") + args.MaxGasLimitToUse = 250000000 + args.TransactionChecks = createMockCheckConfigs() + args.TransactionChecks.TimeInSecondsBetweenChecks = 1 + txHash := "tx hash" + numProcessTransactionStatusCalled := 0 + + nonceCounter := uint64(100) + + sentTransactions := make([]*transaction.FrontendTransaction, 0) + args.Proxy = &interactors.ProxyStub{ + ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { + assert.Equal(t, getPendingTransactionsFunction, vmRequest.FuncName) + + returnData := make([][]byte, 4) + switch vmRequest.Address { + case args.ScProxyBech32Addresses[0]: + returnData[0] = []byte{0x01} + returnData[1] = []byte("ProxySCCompleteCallData 1") + returnData[2] = []byte{0x02} + returnData[3] = []byte("ProxySCCompleteCallData 2") + case args.ScProxyBech32Addresses[1]: + returnData[0] = []byte{0x03} + returnData[1] = []byte("ProxySCCompleteCallData 3") + returnData[2] = []byte{0x04} + returnData[3] = []byte("ProxySCCompleteCallData 4") + } + return &data.VmValuesResponseData{ + Data: &vm.VMOutputApi{ + ReturnCode: okCodeAfterExecution, + ReturnData: returnData, + }, + }, nil + }, + GetNetworkConfigCalled: func(ctx context.Context) (*data.NetworkConfig, error) { + return &data.NetworkConfig{ + ChainID: "TEST", + MinTransactionVersion: 111, + }, nil + }, + ProcessTransactionStatusCalled: func(ctx context.Context, hexTxHash string) (transaction.TxStatus, error) { + assert.Contains(t, hexTxHash, txHash) + numProcessTransactionStatusCalled++ + + return transaction.TxStatusSuccess, nil + }, + } + args.Codec = &testsCommon.MultiversxCodecStub{ + DecodeProxySCCompleteCallDataCalled: func(buff []byte) (parsers.ProxySCCompleteCallData, error) { + if string(buff) == "ProxySCCompleteCallData 1" { + return createTestProxySCCompleteCallData("tkn1"), nil + } + if string(buff) == "ProxySCCompleteCallData 2" { + return createTestProxySCCompleteCallData("tkn2"), nil + } + if string(buff) == "ProxySCCompleteCallData 3" { + return createTestProxySCCompleteCallData("tkn3"), nil + } + if string(buff) == "ProxySCCompleteCallData 4" { + return createTestProxySCCompleteCallData("tkn4"), nil + } + + return parsers.ProxySCCompleteCallData{ + To: data.NewAddressFromBytes(bytes.Repeat([]byte{1}, 32)), + }, errors.New("wrong buffer") + }, + ExtractGasLimitFromRawCallDataCalled: func(buff []byte) (uint64, error) { + return 5000000, nil + }, + } + args.Filter = &testsCommon.ScCallsExecuteFilterStub{ + ShouldExecuteCalled: func(callData parsers.ProxySCCompleteCallData) bool { + return callData.Token == "tkn2" || callData.Token == "tkn4" + }, + } + args.NonceTxHandler = &testsCommon.TxNonceHandlerV2Stub{ + ApplyNonceAndGasPriceCalled: func(ctx context.Context, address core.AddressHandler, tx *transaction.FrontendTransaction) error { + tx.Nonce = nonceCounter + tx.GasPrice = 101010 + nonceCounter++ + return nil + }, + SendTransactionCalled: func(ctx context.Context, tx *transaction.FrontendTransaction) (string, error) { + sentTransactions = append(sentTransactions, tx) + + return fmt.Sprintf("%s - %d", txHash, tx.Nonce), nil + }, + } + args.SingleSigner = &testCrypto.SingleSignerStub{ + SignCalled: func(private crypto.PrivateKey, msg []byte) ([]byte, error) { + return []byte("sig"), nil + }, + } + + expectedSentTransactions := []*transaction.FrontendTransaction{ + { + Nonce: 100, + Value: "0", + Receiver: "erd1qqqqqqqqqqqqqpgqk839entmk46ykukvhpn90g6knskju3dtanaq20f66e", + Sender: senderAddress, + GasPrice: 101010, + GasLimit: args.ExtraGasToExecute + 5000000, + Data: []byte(scProxyCallFunction + "@02"), + Signature: hex.EncodeToString([]byte("sig")), + ChainID: "TEST", + Version: 111, + }, + { + Nonce: 101, + Value: "0", + Receiver: "erd1qqqqqqqqqqqqqpgqzyuaqg3dl7rqlkudrsnm5ek0j3a97qevd8sszj0glf", + Sender: senderAddress, + GasPrice: 101010, + GasLimit: args.ExtraGasToExecute + 5000000, + Data: []byte(scProxyCallFunction + "@04"), + Signature: hex.EncodeToString([]byte("sig")), + ChainID: "TEST", + Version: 111, + }, + } + + executor, _ := NewScCallExecutor(args) + + err := executor.Execute(context.Background()) + assert.Nil(t, err) + assert.Equal(t, uint32(2), executor.GetNumSentTransaction()) + assert.Equal(t, expectedSentTransactions, sentTransactions) + assert.Equal(t, 2, numProcessTransactionStatusCalled) + }) t.Run("should work even if the gas limit decode errors", func(t *testing.T) { t.Parallel() @@ -650,7 +814,7 @@ func TestScCallExecutor_Execute(t *testing.T) { args.Proxy = &interactors.ProxyStub{ ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { - assert.Equal(t, args.ScProxyBech32Address, vmRequest.Address) + assert.Equal(t, args.ScProxyBech32Addresses[0], vmRequest.Address) assert.Equal(t, getPendingTransactionsFunction, vmRequest.FuncName) return &data.VmValuesResponseData{ @@ -711,7 +875,7 @@ func TestScCallExecutor_Execute(t *testing.T) { assert.Equal(t, "erd1qqqqqqqqqqqqqpgqk839entmk46ykukvhpn90g6knskju3dtanaq20f66e", tx.Receiver) assert.Equal(t, "0", tx.Value) - // only the second pending operation gor through the filter + // only the second pending operation got through the filter expectedData := scProxyCallFunction + "@02" assert.Equal(t, expectedData, string(tx.Data)) @@ -743,7 +907,7 @@ func TestScCallExecutor_Execute(t *testing.T) { args.Proxy = &interactors.ProxyStub{ ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { - assert.Equal(t, args.ScProxyBech32Address, vmRequest.Address) + assert.Equal(t, args.ScProxyBech32Addresses[0], vmRequest.Address) assert.Equal(t, getPendingTransactionsFunction, vmRequest.FuncName) return &data.VmValuesResponseData{ @@ -804,7 +968,7 @@ func TestScCallExecutor_Execute(t *testing.T) { assert.Equal(t, "erd1qqqqqqqqqqqqqpgqk839entmk46ykukvhpn90g6knskju3dtanaq20f66e", tx.Receiver) assert.Equal(t, "0", tx.Value) - // only the second pending operation gor through the filter + // only the second pending operation got through the filter expectedData := scProxyCallFunction + "@02" assert.Equal(t, expectedData, string(tx.Data)) @@ -833,7 +997,7 @@ func TestScCallExecutor_Execute(t *testing.T) { args.Proxy = &interactors.ProxyStub{ ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { - assert.Equal(t, args.ScProxyBech32Address, vmRequest.Address) + assert.Equal(t, args.ScProxyBech32Addresses[0], vmRequest.Address) assert.Equal(t, getPendingTransactionsFunction, vmRequest.FuncName) return &data.VmValuesResponseData{ diff --git a/integrationTests/relayers/slowTests/framework/testSetup.go b/integrationTests/relayers/slowTests/framework/testSetup.go index 960c8ee5..7fb9e1c9 100644 --- a/integrationTests/relayers/slowTests/framework/testSetup.go +++ b/integrationTests/relayers/slowTests/framework/testSetup.go @@ -116,7 +116,9 @@ func (setup *TestSetup) StartRelayersAndScModule() { func (setup *TestSetup) startScCallerModule() { cfg := config.ScCallsModuleConfig{ - ScProxyBech32Address: setup.MultiversxHandler.ScProxyAddress.Bech32(), + ScProxyBech32Addresses: []string{ + setup.MultiversxHandler.ScProxyAddress.Bech32(), + }, ExtraGasToExecute: 60_000_000, // 60 million: this ensures that a SC call with 0 gas limit is refunded MaxGasLimitToUse: 249_999_999, // max cross shard limit GasLimitForOutOfGasTransactions: 30_000_000, // gas to use when a higher than max allowed is encountered