From 132992f9b1c2993e95e156d35eeb81b1b1715a22 Mon Sep 17 00:00:00 2001 From: Iulian Pascalau Date: Fri, 27 Dec 2024 16:20:10 +0200 Subject: [PATCH] - finished the refactor for the SC executor (part 3) --- cmd/scCallsExecutor/config/config.toml | 2 +- executors/multiversx/common.go | 81 +++ executors/multiversx/module/interface.go | 1 - executors/multiversx/module/scCallsModule.go | 204 +++++-- .../multiversx/module/scCallsModule_test.go | 39 +- executors/multiversx/refundExecutor.go | 165 ++++++ executors/multiversx/refundExecutor_test.go | 505 ++++++++++++++++++ executors/multiversx/scCallsExecutor.go | 115 +--- executors/multiversx/scCallsExecutor_test.go | 58 +- executors/multiversx/transactionExecutor.go | 13 +- .../relayers/slowTests/edgeCases_test.go | 1 + .../slowTests/framework/multiversxHandler.go | 49 -- .../relayers/slowTests/framework/testSetup.go | 9 + .../refundWithChainSimulator_test.go | 4 + .../relayers/slowTests/testFlow.go | 3 - 15 files changed, 993 insertions(+), 256 deletions(-) create mode 100644 executors/multiversx/common.go create mode 100644 executors/multiversx/refundExecutor.go create mode 100644 executors/multiversx/refundExecutor_test.go diff --git a/cmd/scCallsExecutor/config/config.toml b/cmd/scCallsExecutor/config/config.toml index 7b13c18d..c9304182 100644 --- a/cmd/scCallsExecutor/config/config.toml +++ b/cmd/scCallsExecutor/config/config.toml @@ -17,7 +17,7 @@ PollingIntervalInMillis = 6000 [RefundExecutor] - GasToExecute = 20000000 + GasToExecute = 30000000 PollingIntervalInMillis = 6000 [Filter] diff --git a/executors/multiversx/common.go b/executors/multiversx/common.go new file mode 100644 index 00000000..9ab48473 --- /dev/null +++ b/executors/multiversx/common.go @@ -0,0 +1,81 @@ +package multiversx + +import ( + "context" + "fmt" + "strings" + + bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" + "github.com/multiversx/mx-chain-core-go/core/check" + logger "github.com/multiversx/mx-chain-logger-go" + "github.com/multiversx/mx-sdk-go/data" +) + +type baseExecutor struct { + scProxyBech32Addresses []string + proxy Proxy + transactionExecutor TransactionExecutor + codec Codec + filter ScCallsExecuteFilter + log logger.Logger +} + +func (executor *baseExecutor) checkBaseComponents() error { + if check.IfNil(executor.proxy) { + return errNilProxy + } + if check.IfNil(executor.transactionExecutor) { + return errNilTransactionExecutor + } + if check.IfNil(executor.codec) { + return errNilCodec + } + if check.IfNil(executor.filter) { + return errNilFilter + } + if check.IfNil(executor.log) { + return errNilLogger + } + + if len(executor.scProxyBech32Addresses) == 0 { + return errEmptyListOfBridgeSCProxy + } + + for _, scProxyAddress := range executor.scProxyBech32Addresses { + _, err := data.NewAddressFromBech32String(scProxyAddress) + if err != nil { + return fmt.Errorf("%w for address %s", err, scProxyAddress) + } + } + + return nil +} + +func (executor *baseExecutor) executeOnAllScProxyAddress(ctx context.Context, handler func(ctx context.Context, address string) error) error { + errorStrings := make([]string, 0) + for _, scProxyAddress := range executor.scProxyBech32Addresses { + err := handler(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 *baseExecutor) filterOperations(component string, pendingOperations map[uint64]bridgeCore.ProxySCCompleteCallData) map[uint64]bridgeCore.ProxySCCompleteCallData { + result := make(map[uint64]bridgeCore.ProxySCCompleteCallData) + for id, callData := range pendingOperations { + if executor.filter.ShouldExecute(callData) { + result[id] = callData + } + } + + executor.log.Debug(component, "input pending ops", len(pendingOperations), "result pending ops", len(result)) + + return result +} diff --git a/executors/multiversx/module/interface.go b/executors/multiversx/module/interface.go index 69fab4b3..2bf28151 100644 --- a/executors/multiversx/module/interface.go +++ b/executors/multiversx/module/interface.go @@ -22,6 +22,5 @@ type pollingHandler interface { type executor interface { Execute(ctx context.Context) error - GetNumSentTransaction() uint32 IsInterfaceNil() bool } diff --git a/executors/multiversx/module/scCallsModule.go b/executors/multiversx/module/scCallsModule.go index e753c47b..f0edb3c8 100644 --- a/executors/multiversx/module/scCallsModule.go +++ b/executors/multiversx/module/scCallsModule.go @@ -23,118 +23,218 @@ var keyGen = signing.NewKeyGenerator(suite) var singleSigner = &singlesig.Ed25519Signer{} type scCallsModule struct { - nonceTxsHandler nonceTransactionsHandler - pollingHandler pollingHandler - executorInstance executor + cfg config.ScCallsModuleConfig + log logger.Logger + filter multiversx.ScCallsExecuteFilter + proxy multiversx.Proxy + nonceTxsHandler nonceTransactionsHandler + txExecutor multiversx.TransactionExecutor + + pollingHandlers []pollingHandler + executors []executor } // NewScCallsModule creates a starts a new scCallsModule instance func NewScCallsModule(cfg config.ScCallsModuleConfig, log logger.Logger, chCloseApp chan struct{}) (*scCallsModule, error) { - filter, err := filters.NewPendingOperationFilter(cfg.Filter, log) + module := &scCallsModule{ + cfg: cfg, + log: log, + } + + err := module.createFilter() if err != nil { return nil, err } - argsProxy := blockchain.ArgsProxy{ - ProxyURL: cfg.General.NetworkAddress, - SameScState: false, - ShouldBeSynced: false, - FinalityCheck: cfg.General.ProxyFinalityCheck, - AllowedDeltaToFinal: cfg.General.ProxyMaxNoncesDelta, - CacheExpirationTime: time.Second * time.Duration(cfg.General.ProxyCacherExpirationSeconds), - EntityType: sdkCore.RestAPIEntityType(cfg.General.ProxyRestAPIEntityType), + err = module.createProxy() + if err != nil { + return nil, err } - proxy, err := blockchain.NewProxy(argsProxy) + err = module.createNonceTxHandler() if err != nil { return nil, err } - module := &scCallsModule{} + err = module.createTransactionExecutor(chCloseApp) + if err != nil { + return nil, err + } - argNonceHandler := nonceHandlerV2.ArgsNonceTransactionsHandlerV2{ - Proxy: proxy, - IntervalToResend: time.Second * time.Duration(cfg.General.IntervalToResendTxsInSeconds), + err = module.createScCallsExecutor() + if err != nil { + return nil, err } - module.nonceTxsHandler, err = nonceHandlerV2.NewNonceTransactionHandlerV2(argNonceHandler) + + err = module.createRefundExecutor() if err != nil { return nil, err } + return module, nil +} + +func (module *scCallsModule) createFilter() error { + var err error + module.filter, err = filters.NewPendingOperationFilter(module.cfg.Filter, module.log) + + return err +} + +func (module *scCallsModule) createProxy() error { + argsProxy := blockchain.ArgsProxy{ + ProxyURL: module.cfg.General.NetworkAddress, + SameScState: false, + ShouldBeSynced: false, + FinalityCheck: module.cfg.General.ProxyFinalityCheck, + AllowedDeltaToFinal: module.cfg.General.ProxyMaxNoncesDelta, + CacheExpirationTime: time.Second * time.Duration(module.cfg.General.ProxyCacherExpirationSeconds), + EntityType: sdkCore.RestAPIEntityType(module.cfg.General.ProxyRestAPIEntityType), + } + + var err error + module.proxy, err = blockchain.NewProxy(argsProxy) + + return err +} + +func (module *scCallsModule) createNonceTxHandler() error { + argNonceHandler := nonceHandlerV2.ArgsNonceTransactionsHandlerV2{ + Proxy: module.proxy, + IntervalToResend: time.Second * time.Duration(module.cfg.General.IntervalToResendTxsInSeconds), + } + + var err error + module.nonceTxsHandler, err = nonceHandlerV2.NewNonceTransactionHandlerV2(argNonceHandler) + + return err +} + +func (module *scCallsModule) createTransactionExecutor(chCloseApp chan struct{}) error { wallet := interactors.NewWallet() - multiversXPrivateKeyBytes, err := wallet.LoadPrivateKeyFromPemFile(cfg.General.PrivateKeyFile) + multiversXPrivateKeyBytes, err := wallet.LoadPrivateKeyFromPemFile(module.cfg.General.PrivateKeyFile) if err != nil { - return nil, err + return err } privateKey, err := keyGen.PrivateKeyFromByteArray(multiversXPrivateKeyBytes) if err != nil { - return nil, err + return err } argsTxExecutor := multiversx.ArgsTransactionExecutor{ - Proxy: proxy, - Log: log, + Proxy: module.proxy, + Log: module.log, NonceTxHandler: module.nonceTxsHandler, PrivateKey: privateKey, SingleSigner: singleSigner, - TransactionChecks: cfg.TransactionChecks, + TransactionChecks: module.cfg.TransactionChecks, CloseAppChan: chCloseApp, } - transactionExecutor, err := multiversx.NewTransactionExecutor(argsTxExecutor) - if err != nil { - return nil, err - } + module.txExecutor, err = multiversx.NewTransactionExecutor(argsTxExecutor) + + return err +} +func (module *scCallsModule) createScCallsExecutor() error { argsExecutor := multiversx.ArgsScCallExecutor{ - ScProxyBech32Addresses: cfg.General.ScProxyBech32Addresses, - TransactionExecutor: transactionExecutor, - Proxy: proxy, + ScProxyBech32Addresses: module.cfg.General.ScProxyBech32Addresses, + TransactionExecutor: module.txExecutor, + Proxy: module.proxy, Codec: &parsers.MultiversxCodec{}, - Filter: filter, - Log: log, - ExecutorConfig: cfg.ScCallsExecutor, - TransactionChecks: cfg.TransactionChecks, + Filter: module.filter, + Log: module.log, + ExecutorConfig: module.cfg.ScCallsExecutor, } - module.executorInstance, err = multiversx.NewScCallExecutor(argsExecutor) + + executorInstance, err := multiversx.NewScCallExecutor(argsExecutor) if err != nil { - return nil, err + return err } + module.executors = append(module.executors, executorInstance) argsPollingHandler := polling.ArgsPollingHandler{ - Log: log, + Log: module.log, Name: "MultiversX SC calls", - PollingInterval: time.Duration(cfg.ScCallsExecutor.PollingIntervalInMillis) * time.Millisecond, - PollingWhenError: time.Duration(cfg.ScCallsExecutor.PollingIntervalInMillis) * time.Millisecond, - Executor: module.executorInstance, + PollingInterval: time.Duration(module.cfg.ScCallsExecutor.PollingIntervalInMillis) * time.Millisecond, + PollingWhenError: time.Duration(module.cfg.ScCallsExecutor.PollingIntervalInMillis) * time.Millisecond, + Executor: executorInstance, } - module.pollingHandler, err = polling.NewPollingHandler(argsPollingHandler) + pollingHandlerInstance, err := polling.NewPollingHandler(argsPollingHandler) if err != nil { - return nil, err + return err } - err = module.pollingHandler.StartProcessingLoop() + err = pollingHandlerInstance.StartProcessingLoop() if err != nil { - return nil, err + return err } + module.pollingHandlers = append(module.pollingHandlers, pollingHandlerInstance) - return module, nil + return nil +} + +func (module *scCallsModule) createRefundExecutor() error { + argsExecutor := multiversx.ArgsRefundExecutor{ + ScProxyBech32Addresses: module.cfg.General.ScProxyBech32Addresses, + TransactionExecutor: module.txExecutor, + Proxy: module.proxy, + Codec: &parsers.MultiversxCodec{}, + Filter: module.filter, + Log: module.log, + GasToExecute: module.cfg.RefundExecutor.GasToExecute, + } + + executorInstance, err := multiversx.NewRefundExecutor(argsExecutor) + if err != nil { + return err + } + module.executors = append(module.executors, executorInstance) + + argsPollingHandler := polling.ArgsPollingHandler{ + Log: module.log, + Name: "MultiversX refund executor", + PollingInterval: time.Duration(module.cfg.RefundExecutor.PollingIntervalInMillis) * time.Millisecond, + PollingWhenError: time.Duration(module.cfg.RefundExecutor.PollingIntervalInMillis) * time.Millisecond, + Executor: executorInstance, + } + + pollingHandlerInstance, err := polling.NewPollingHandler(argsPollingHandler) + if err != nil { + return err + } + + err = pollingHandlerInstance.StartProcessingLoop() + if err != nil { + return err + } + module.pollingHandlers = append(module.pollingHandlers, pollingHandlerInstance) + + return nil } // GetNumSentTransaction returns the total sent transactions func (module *scCallsModule) GetNumSentTransaction() uint32 { - return module.executorInstance.GetNumSentTransaction() + return module.txExecutor.GetNumSentTransaction() } // Close closes any components started func (module *scCallsModule) Close() error { - errPollingHandler := module.pollingHandler.Close() - errNonceTxsHandler := module.nonceTxsHandler.Close() + var lastError error - if errPollingHandler != nil { - return errPollingHandler + for _, handlers := range module.pollingHandlers { + err := handlers.Close() + if err != nil { + lastError = err + } } - return errNonceTxsHandler + + err := module.nonceTxsHandler.Close() + if err != nil { + lastError = err + } + + return lastError } diff --git a/executors/multiversx/module/scCallsModule_test.go b/executors/multiversx/module/scCallsModule_test.go index 097efeca..282a01ae 100644 --- a/executors/multiversx/module/scCallsModule_test.go +++ b/executors/multiversx/module/scCallsModule_test.go @@ -29,6 +29,10 @@ func createTestConfigs() config.ScCallsModuleConfig { GasLimitForOutOfGasTransactions: 30000000, PollingIntervalInMillis: 10000, }, + RefundExecutor: config.RefundExecutorConfig{ + GasToExecute: 30000000, + PollingIntervalInMillis: 10000, + }, Filter: config.PendingOperationsFilterConfig{ DeniedEthAddresses: nil, AllowedEthAddresses: []string{"*"}, @@ -86,7 +90,7 @@ func TestNewScCallsModule(t *testing.T) { assert.NotNil(t, err) assert.Nil(t, module) }) - t.Run("invalid polling interval should error", func(t *testing.T) { + t.Run("invalid polling interval for SC calls should error", func(t *testing.T) { t.Parallel() cfg := createTestConfigs() @@ -97,6 +101,39 @@ func TestNewScCallsModule(t *testing.T) { assert.Contains(t, err.Error(), "invalid value for PollingInterval") assert.Nil(t, module) }) + t.Run("invalid max gas to execute for SC calls should error", func(t *testing.T) { + t.Parallel() + + cfg := createTestConfigs() + cfg.ScCallsExecutor.MaxGasLimitToUse = 1 + + module, err := NewScCallsModule(cfg, &testsCommon.LoggerStub{}, nil) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "provided gas limit is less than absolute minimum required for MaxGasLimitToUse") + assert.Nil(t, module) + }) + t.Run("invalid polling interval for refunds should error", func(t *testing.T) { + t.Parallel() + + cfg := createTestConfigs() + cfg.RefundExecutor.PollingIntervalInMillis = 0 + + module, err := NewScCallsModule(cfg, &testsCommon.LoggerStub{}, nil) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "invalid value for PollingInterval") + assert.Nil(t, module) + }) + t.Run("invalid gas to execute for refunds should error", func(t *testing.T) { + t.Parallel() + + cfg := createTestConfigs() + cfg.RefundExecutor.GasToExecute = 0 + + module, err := NewScCallsModule(cfg, &testsCommon.LoggerStub{}, nil) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "provided gas limit is less than absolute minimum required for GasToExecute") + assert.Nil(t, module) + }) t.Run("should work with nil close app chan", func(t *testing.T) { t.Parallel() diff --git a/executors/multiversx/refundExecutor.go b/executors/multiversx/refundExecutor.go new file mode 100644 index 00000000..da08d79e --- /dev/null +++ b/executors/multiversx/refundExecutor.go @@ -0,0 +1,165 @@ +package multiversx + +import ( + "context" + "fmt" + "math/big" + + bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" + "github.com/multiversx/mx-bridge-eth-go/errors" + logger "github.com/multiversx/mx-chain-logger-go" + "github.com/multiversx/mx-sdk-go/builders" + "github.com/multiversx/mx-sdk-go/data" +) + +const ( + getRefundTransactionsFunction = "getRefundTransactions" + executeRefundTransactionFunction = "executeRefundTransaction" + refundTxType = "refund" +) + +// ArgsRefundExecutor represents the DTO struct for creating a new instance of type refundExecutor +type ArgsRefundExecutor struct { + TransactionExecutor TransactionExecutor + ScProxyBech32Addresses []string + Proxy Proxy + Codec Codec + Filter ScCallsExecuteFilter + Log logger.Logger + GasToExecute uint64 +} + +type refundExecutor struct { + *baseExecutor + gasToExecute uint64 +} + +// NewRefundExecutor creates a new instance of type refundExecutor +func NewRefundExecutor(args ArgsRefundExecutor) (*refundExecutor, error) { + if args.GasToExecute < minGasToExecuteSCCalls { + return nil, fmt.Errorf("%w for GasToExecute: provided: %d, absolute minimum required: %d", errGasLimitIsLessThanAbsoluteMinimum, args.GasToExecute, minGasToExecuteSCCalls) + } + + executor := &refundExecutor{ + baseExecutor: &baseExecutor{ + scProxyBech32Addresses: args.ScProxyBech32Addresses, + proxy: args.Proxy, + transactionExecutor: args.TransactionExecutor, + codec: args.Codec, + filter: args.Filter, + log: args.Log, + }, + gasToExecute: args.GasToExecute, + } + + err := executor.checkBaseComponents() + if err != nil { + return nil, err + } + + return executor, nil +} + +// Execute will execute one step: get all pending operations, call the filter and send execution transactions +func (executor *refundExecutor) Execute(ctx context.Context) error { + return executor.executeOnAllScProxyAddress(ctx, executor.executeRefundForScProxyAddress) +} + +func (executor *refundExecutor) executeRefundForScProxyAddress(ctx context.Context, scProxyAddress string) error { + executor.log.Info("Executing refunds for the SC proxy address", "address", scProxyAddress) + + pendingOperations, err := executor.getPendingRefunds(ctx, scProxyAddress) + if err != nil { + return err + } + + filteredPendingOperations := executor.filterOperations("refundExecutor", pendingOperations) + + return executor.executeRefunds(ctx, filteredPendingOperations, scProxyAddress) +} + +func (executor *refundExecutor) getPendingRefunds(ctx context.Context, scProxyAddress string) (map[uint64]bridgeCore.ProxySCCompleteCallData, error) { + request := &data.VmValueRequest{ + Address: scProxyAddress, + FuncName: getRefundTransactionsFunction, + } + + response, err := executor.proxy.ExecuteVMQuery(ctx, request) + if err != nil { + executor.log.Error("got error on VMQuery", "FuncName", request.FuncName, + "Args", request.Args, "SC address", request.Address, "Caller", request.CallerAddr, "error", err) + return nil, err + } + if response.Data.ReturnCode != okCodeAfterExecution { + return nil, errors.NewQueryResponseError( + response.Data.ReturnCode, + response.Data.ReturnMessage, + request.FuncName, + request.Address, + request.Args..., + ) + } + + return executor.parseResponse(response) +} + +func (executor *refundExecutor) parseResponse(response *data.VmValuesResponseData) (map[uint64]bridgeCore.ProxySCCompleteCallData, error) { + numResponseLines := len(response.Data.ReturnData) + if numResponseLines%2 != 0 { + return nil, fmt.Errorf("%w: expected an even number, got %d", errInvalidNumberOfResponseLines, numResponseLines) + } + + result := make(map[uint64]bridgeCore.ProxySCCompleteCallData, numResponseLines/2) + + for i := 0; i < numResponseLines; i += 2 { + pendingOperationID := big.NewInt(0).SetBytes(response.Data.ReturnData[i]) + callData, err := executor.codec.DecodeProxySCCompleteCallData(response.Data.ReturnData[i+1]) + if err != nil { + return nil, fmt.Errorf("%w for ReturnData at index %d", err, i+1) + } + + result[pendingOperationID.Uint64()] = callData + } + + return result, nil +} + +func (executor *refundExecutor) executeRefunds(ctx context.Context, refundIDs map[uint64]bridgeCore.ProxySCCompleteCallData, scProxyAddress string) error { + networkConfig, err := executor.proxy.GetNetworkConfig(ctx) + if err != nil { + return fmt.Errorf("%w while fetching network configs", err) + } + + for id := range refundIDs { + executor.log.Debug("refundExecutor.executeRefunds", "executing refund ID", id) + err = executor.executeOperation(context.Background(), id, networkConfig, scProxyAddress) + + if err != nil { + return fmt.Errorf("%w for refund ID: %d", err, id) + } + } + + return nil +} + +func (executor *refundExecutor) executeOperation( + ctx context.Context, + id uint64, + networkConfig *data.NetworkConfig, + scProxyAddress string, +) error { + txBuilder := builders.NewTxDataBuilder() + txBuilder.Function(executeRefundTransactionFunction).ArgInt64(int64(id)) + + dataBytes, err := txBuilder.ToDataBytes() + if err != nil { + return err + } + + return executor.transactionExecutor.ExecuteTransaction(ctx, networkConfig, scProxyAddress, refundTxType, executor.gasToExecute, dataBytes) +} + +// IsInterfaceNil returns true if there is no value under the interface +func (executor *refundExecutor) IsInterfaceNil() bool { + return executor == nil +} diff --git a/executors/multiversx/refundExecutor_test.go b/executors/multiversx/refundExecutor_test.go new file mode 100644 index 00000000..b7364cb4 --- /dev/null +++ b/executors/multiversx/refundExecutor_test.go @@ -0,0 +1,505 @@ +package multiversx + +import ( + "bytes" + "context" + "errors" + "testing" + + bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" + "github.com/multiversx/mx-bridge-eth-go/testsCommon" + "github.com/multiversx/mx-bridge-eth-go/testsCommon/interactors" + "github.com/multiversx/mx-chain-core-go/data/vm" + "github.com/multiversx/mx-sdk-go/data" + "github.com/stretchr/testify/assert" +) + +func createMockArgsRefundExecutor() ArgsRefundExecutor { + return ArgsRefundExecutor{ + ScProxyBech32Addresses: []string{ + "erd1qqqqqqqqqqqqqpgqk839entmk46ykukvhpn90g6knskju3dtanaq20f66e", + }, + Proxy: &interactors.ProxyStub{}, + TransactionExecutor: &testsCommon.TransactionExecutorStub{}, + Codec: &testsCommon.MultiversxCodecStub{}, + Filter: &testsCommon.ScCallsExecuteFilterStub{}, + Log: &testsCommon.LoggerStub{}, + GasToExecute: minGasToExecuteSCCalls, + } +} + +func TestNewRefundExecutor(t *testing.T) { + t.Parallel() + + t.Run("nil proxy should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsRefundExecutor() + args.Proxy = nil + + executor, err := NewRefundExecutor(args) + assert.Nil(t, executor) + assert.Equal(t, errNilProxy, err) + }) + t.Run("nil transaction executor should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsRefundExecutor() + args.TransactionExecutor = nil + + executor, err := NewRefundExecutor(args) + assert.Nil(t, executor) + assert.Equal(t, errNilTransactionExecutor, err) + }) + t.Run("nil codec should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsRefundExecutor() + args.Codec = nil + + executor, err := NewRefundExecutor(args) + assert.Nil(t, executor) + assert.Equal(t, errNilCodec, err) + }) + t.Run("nil filter should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsRefundExecutor() + args.Filter = nil + + executor, err := NewRefundExecutor(args) + assert.Nil(t, executor) + assert.Equal(t, errNilFilter, err) + }) + t.Run("nil logger should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsRefundExecutor() + args.Log = nil + + executor, err := NewRefundExecutor(args) + assert.Nil(t, executor) + assert.Equal(t, errNilLogger, err) + }) + t.Run("empty list of sc proxy bech32 addresses should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsRefundExecutor() + args.ScProxyBech32Addresses = nil + + executor, err := NewRefundExecutor(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 := createMockArgsRefundExecutor() + args.ScProxyBech32Addresses = append(args.ScProxyBech32Addresses, "not a valid bech32 address") + + executor, err := NewRefundExecutor(args) + assert.Nil(t, executor) + assert.NotNil(t, err) + }) + t.Run("invalid GasToExecute should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsRefundExecutor() + args.GasToExecute = minGasToExecuteSCCalls - 1 + + executor, err := NewRefundExecutor(args) + assert.Nil(t, executor) + assert.ErrorIs(t, err, errGasLimitIsLessThanAbsoluteMinimum) + assert.Contains(t, err.Error(), "provided: 2009999, absolute minimum required: 2010000") + assert.Contains(t, err.Error(), "GasToExecute") + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + args := createMockArgsRefundExecutor() + + executor, err := NewRefundExecutor(args) + assert.NotNil(t, executor) + assert.Nil(t, err) + }) +} + +func TestRefundExecutor_IsInterfaceNil(t *testing.T) { + t.Parallel() + + var instance *refundExecutor + assert.True(t, instance.IsInterfaceNil()) + + instance = &refundExecutor{} + assert.False(t, instance.IsInterfaceNil()) +} + +func TestRefundExecutor_Execute(t *testing.T) { + t.Parallel() + + runError := errors.New("run error") + expectedError := errors.New("expected error") + + argsForErrors := createMockArgsRefundExecutor() + argsForErrors.TransactionExecutor = &testsCommon.TransactionExecutorStub{ + ExecuteTransactionCalled: func(ctx context.Context, networkConfig *data.NetworkConfig, receiver string, transactionType string, gasLimit uint64, dataBytes []byte) error { + assert.Fail(t, "should have not called ExecuteTransactionCalled") + return runError + }, + } + + t.Run("get pending errors, should error", func(t *testing.T) { + t.Parallel() + + args := argsForErrors // value copy + args.Proxy = &interactors.ProxyStub{ + ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { + return nil, expectedError + }, + } + + executor, _ := NewRefundExecutor(args) + err := executor.Execute(context.Background()) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), expectedError.Error()) + assert.Contains(t, err.Error(), "errors found during execution") + }) + t.Run("get pending returns a not ok status, should error", func(t *testing.T) { + t.Parallel() + + args := argsForErrors // value copy + args.Proxy = &interactors.ProxyStub{ + ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { + return &data.VmValuesResponseData{ + Data: &vm.VMOutputApi{ + ReturnCode: "NOT OK", + }, + }, nil + }, + } + + executor, _ := NewRefundExecutor(args) + err := executor.Execute(context.Background()) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "got response code 'NOT OK'") + }) + t.Run("get pending returns an odd number of lines, should error", func(t *testing.T) { + t.Parallel() + + args := argsForErrors // value copy + args.Proxy = &interactors.ProxyStub{ + ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { + return &data.VmValuesResponseData{ + Data: &vm.VMOutputApi{ + ReturnCode: okCodeAfterExecution, + ReturnData: [][]byte{ + {0x01}, + }, + }, + }, nil + }, + } + + executor, _ := NewRefundExecutor(args) + err := executor.Execute(context.Background()) + 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") + }) + t.Run("decoder errors, should error", func(t *testing.T) { + t.Parallel() + + args := argsForErrors // value copy + args.Proxy = &interactors.ProxyStub{ + ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { + return &data.VmValuesResponseData{ + Data: &vm.VMOutputApi{ + ReturnCode: okCodeAfterExecution, + ReturnData: [][]byte{ + {0x01}, + {0x03, 0x04}, + }, + }, + }, nil + }, + } + args.Codec = &testsCommon.MultiversxCodecStub{ + DecodeProxySCCompleteCallDataCalled: func(buff []byte) (bridgeCore.ProxySCCompleteCallData, error) { + assert.Equal(t, []byte{0x03, 0x04}, buff) + + return bridgeCore.ProxySCCompleteCallData{ + To: data.NewAddressFromBytes(bytes.Repeat([]byte{1}, 32)), + }, expectedError + }, + } + + executor, _ := NewRefundExecutor(args) + err := executor.Execute(context.Background()) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), expectedError.Error()) + assert.Contains(t, err.Error(), "errors found during execution") + }) + t.Run("get network configs errors, should error", func(t *testing.T) { + t.Parallel() + + args := argsForErrors // value copy + args.Proxy = &interactors.ProxyStub{ + ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { + return &data.VmValuesResponseData{ + Data: &vm.VMOutputApi{ + ReturnCode: okCodeAfterExecution, + ReturnData: [][]byte{ + {0x01}, + {0x03, 0x04}, + }, + }, + }, nil + }, + GetNetworkConfigCalled: func(ctx context.Context) (*data.NetworkConfig, error) { + return nil, expectedError + }, + } + args.Codec = &testsCommon.MultiversxCodecStub{ + DecodeProxySCCompleteCallDataCalled: func(buff []byte) (bridgeCore.ProxySCCompleteCallData, error) { + assert.Equal(t, []byte{0x03, 0x04}, buff) + + return bridgeCore.ProxySCCompleteCallData{ + To: data.NewAddressFromBytes(bytes.Repeat([]byte{1}, 32)), + }, nil + }, + } + + executor, _ := NewRefundExecutor(args) + err := executor.Execute(context.Background()) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), expectedError.Error()) + assert.Contains(t, err.Error(), "errors found during execution") + }) + t.Run("SendTransaction errors, should error", func(t *testing.T) { + t.Parallel() + + args := createMockArgsRefundExecutor() + args.TransactionExecutor = &testsCommon.TransactionExecutorStub{ + ExecuteTransactionCalled: func(ctx context.Context, networkConfig *data.NetworkConfig, receiver string, transactionType string, gasLimit uint64, dataBytes []byte) error { + return expectedError + }, + } + args.Proxy = &interactors.ProxyStub{ + ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { + return &data.VmValuesResponseData{ + Data: &vm.VMOutputApi{ + ReturnCode: okCodeAfterExecution, + ReturnData: [][]byte{ + {0x01}, + {0x03, 0x04}, + }, + }, + }, nil + }, + GetNetworkConfigCalled: func(ctx context.Context) (*data.NetworkConfig, error) { + return &data.NetworkConfig{}, nil + }, + } + args.Codec = &testsCommon.MultiversxCodecStub{ + DecodeProxySCCompleteCallDataCalled: func(buff []byte) (bridgeCore.ProxySCCompleteCallData, error) { + assert.Equal(t, []byte{0x03, 0x04}, buff) + + return bridgeCore.ProxySCCompleteCallData{ + To: data.NewAddressFromBytes(bytes.Repeat([]byte{1}, 32)), + }, nil + }, + } + + executor, _ := NewRefundExecutor(args) + err := executor.Execute(context.Background()) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), expectedError.Error()) + assert.Contains(t, err.Error(), "errors found during execution") + }) + t.Run("should work", func(t *testing.T) { + t.Parallel() + + args := createMockArgsRefundExecutor() + args.GasToExecute = 250000000 + sendWasCalled := false + + args.Proxy = &interactors.ProxyStub{ + ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { + assert.Equal(t, args.ScProxyBech32Addresses[0], vmRequest.Address) + assert.Equal(t, getRefundTransactionsFunction, vmRequest.FuncName) + + return &data.VmValuesResponseData{ + Data: &vm.VMOutputApi{ + ReturnCode: okCodeAfterExecution, + ReturnData: [][]byte{ + {0x01}, + []byte("ProxySCCompleteCallData 1"), + {0x02}, + []byte("ProxySCCompleteCallData 2"), + }, + }, + }, nil + }, + GetNetworkConfigCalled: func(ctx context.Context) (*data.NetworkConfig, error) { + return &data.NetworkConfig{ + ChainID: "TEST", + MinTransactionVersion: 111, + }, nil + }, + } + args.Codec = &testsCommon.MultiversxCodecStub{ + DecodeProxySCCompleteCallDataCalled: func(buff []byte) (bridgeCore.ProxySCCompleteCallData, error) { + if string(buff) == "ProxySCCompleteCallData 1" { + return createTestProxySCCompleteCallData("tkn1"), nil + } + if string(buff) == "ProxySCCompleteCallData 2" { + return createTestProxySCCompleteCallData("tkn2"), nil + } + + return bridgeCore.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 bridgeCore.ProxySCCompleteCallData) bool { + return callData.Token == "tkn2" + }, + } + args.TransactionExecutor = &testsCommon.TransactionExecutorStub{ + ExecuteTransactionCalled: func(ctx context.Context, networkConfig *data.NetworkConfig, receiver string, transactionType string, gasLimit uint64, dataBytes []byte) error { + assert.Equal(t, "TEST", networkConfig.ChainID) + assert.Equal(t, uint32(111), networkConfig.MinTransactionVersion) + assert.Equal(t, args.GasToExecute, gasLimit) + assert.Equal(t, "erd1qqqqqqqqqqqqqpgqk839entmk46ykukvhpn90g6knskju3dtanaq20f66e", receiver) + assert.Equal(t, refundTxType, transactionType) + + expectedData := executeRefundTransactionFunction + "@02" + assert.Equal(t, expectedData, string(dataBytes)) + + sendWasCalled = true + + return nil + }, + } + + executor, _ := NewRefundExecutor(args) + + err := executor.Execute(context.Background()) + assert.Nil(t, err) + assert.True(t, sendWasCalled) + }) + t.Run("should work with one two proxy address", func(t *testing.T) { + t.Parallel() + + args := createMockArgsRefundExecutor() + args.ScProxyBech32Addresses = append(args.ScProxyBech32Addresses, "erd1qqqqqqqqqqqqqpgqzyuaqg3dl7rqlkudrsnm5ek0j3a97qevd8sszj0glf") + args.GasToExecute = 250000000 + + args.Proxy = &interactors.ProxyStub{ + ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { + assert.Equal(t, getRefundTransactionsFunction, 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 + }, + } + args.Codec = &testsCommon.MultiversxCodecStub{ + DecodeProxySCCompleteCallDataCalled: func(buff []byte) (bridgeCore.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 bridgeCore.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 bridgeCore.ProxySCCompleteCallData) bool { + return callData.Token == "tkn2" || callData.Token == "tkn4" + }, + } + + type sentTxValues struct { + receiver string + transactionType string + gasLimit uint64 + dataBytes []byte + } + sentTransactions := make([]*sentTxValues, 0) + args.TransactionExecutor = &testsCommon.TransactionExecutorStub{ + ExecuteTransactionCalled: func(ctx context.Context, networkConfig *data.NetworkConfig, receiver string, transactionType string, gasLimit uint64, dataBytes []byte) error { + tx := &sentTxValues{ + receiver: receiver, + transactionType: transactionType, + gasLimit: gasLimit, + dataBytes: dataBytes, + } + sentTransactions = append(sentTransactions, tx) + + return nil + }, + } + + expectedSentTransactions := []*sentTxValues{ + { + receiver: "erd1qqqqqqqqqqqqqpgqk839entmk46ykukvhpn90g6knskju3dtanaq20f66e", + transactionType: refundTxType, + gasLimit: args.GasToExecute, + dataBytes: []byte(executeRefundTransactionFunction + "@02"), + }, + { + receiver: "erd1qqqqqqqqqqqqqpgqzyuaqg3dl7rqlkudrsnm5ek0j3a97qevd8sszj0glf", + transactionType: refundTxType, + gasLimit: args.GasToExecute, + dataBytes: []byte(executeRefundTransactionFunction + "@04"), + }, + } + + executor, _ := NewRefundExecutor(args) + + err := executor.Execute(context.Background()) + assert.Nil(t, err) + assert.Equal(t, expectedSentTransactions, sentTransactions) + }) +} diff --git a/executors/multiversx/scCallsExecutor.go b/executors/multiversx/scCallsExecutor.go index 3c732d6f..e6f4040e 100644 --- a/executors/multiversx/scCallsExecutor.go +++ b/executors/multiversx/scCallsExecutor.go @@ -4,13 +4,10 @@ import ( "context" "fmt" "math/big" - "strings" - "time" "github.com/multiversx/mx-bridge-eth-go/config" bridgeCore "github.com/multiversx/mx-bridge-eth-go/core" "github.com/multiversx/mx-bridge-eth-go/errors" - "github.com/multiversx/mx-chain-core-go/core/check" logger "github.com/multiversx/mx-chain-logger-go" "github.com/multiversx/mx-sdk-go/builders" "github.com/multiversx/mx-sdk-go/data" @@ -33,20 +30,13 @@ type ArgsScCallExecutor struct { Filter ScCallsExecuteFilter Log logger.Logger ExecutorConfig config.ScCallsExecutorConfig - TransactionChecks config.TransactionChecksConfig } type scCallExecutor struct { - scProxyBech32Addresses []string - proxy Proxy - transactionExecutor TransactionExecutor - codec Codec - filter ScCallsExecuteFilter - log logger.Logger + *baseExecutor extraGasToExecute uint64 maxGasLimitToUse uint64 gasLimitForOutOfGasTransactions uint64 - executionTimeout time.Duration } // NewScCallExecutor creates a new instance of type scCallExecutor @@ -56,86 +46,53 @@ func NewScCallExecutor(args ArgsScCallExecutor) (*scCallExecutor, error) { return nil, err } - return &scCallExecutor{ - scProxyBech32Addresses: args.ScProxyBech32Addresses, - proxy: args.Proxy, - transactionExecutor: args.TransactionExecutor, - codec: args.Codec, - filter: args.Filter, - log: args.Log, + executor := &scCallExecutor{ + baseExecutor: &baseExecutor{ + scProxyBech32Addresses: args.ScProxyBech32Addresses, + proxy: args.Proxy, + transactionExecutor: args.TransactionExecutor, + codec: args.Codec, + filter: args.Filter, + log: args.Log, + }, extraGasToExecute: args.ExecutorConfig.ExtraGasToExecute, maxGasLimitToUse: args.ExecutorConfig.MaxGasLimitToUse, gasLimitForOutOfGasTransactions: args.ExecutorConfig.GasLimitForOutOfGasTransactions, - executionTimeout: time.Second * time.Duration(args.TransactionChecks.ExecutionTimeoutInSeconds), - }, nil + } + + err = executor.checkBaseComponents() + if err != nil { + return nil, err + } + + return executor, nil } func checkScCallExecutorArgs(args ArgsScCallExecutor) error { - if check.IfNil(args.Proxy) { - return errNilProxy - } - if check.IfNil(args.TransactionExecutor) { - return errNilTransactionExecutor - } - if check.IfNil(args.Codec) { - return errNilCodec - } - if check.IfNil(args.Filter) { - return errNilFilter - } - if check.IfNil(args.Log) { - return errNilLogger - } if args.ExecutorConfig.MaxGasLimitToUse < minGasToExecuteSCCalls { return fmt.Errorf("%w for MaxGasLimitToUse: provided: %d, absolute minimum required: %d", errGasLimitIsLessThanAbsoluteMinimum, args.ExecutorConfig.MaxGasLimitToUse, minGasToExecuteSCCalls) } if args.ExecutorConfig.GasLimitForOutOfGasTransactions < minGasToExecuteSCCalls { return fmt.Errorf("%w for GasLimitForOutOfGasTransactions: provided: %d, absolute minimum required: %d", errGasLimitIsLessThanAbsoluteMinimum, args.ExecutorConfig.GasLimitForOutOfGasTransactions, minGasToExecuteSCCalls) } - err := checkTransactionChecksConfig(args.TransactionChecks, args.Log) - if err != nil { - return err - } - - if len(args.ScProxyBech32Addresses) == 0 { - return errEmptyListOfBridgeSCProxy - } - - for _, scProxyAddress := range args.ScProxyBech32Addresses { - _, err = data.NewAddressFromBech32String(scProxyAddress) - if err != nil { - return fmt.Errorf("%w for address %s", err, scProxyAddress) - } - } return nil } // Execute will execute one step: get all pending operations, call the filter and send execution transactions func (executor *scCallExecutor) Execute(ctx context.Context) error { - 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")) + return executor.executeOnAllScProxyAddress(ctx, executor.executeScCallForScProxyAddress) } -func (executor *scCallExecutor) executeForScProxyAddress(ctx context.Context, scProxyAddress string) error { - executor.log.Info("Executing for the SC proxy address", "address", scProxyAddress) +func (executor *scCallExecutor) executeScCallForScProxyAddress(ctx context.Context, scProxyAddress string) error { + executor.log.Info("Executing SC calls for the SC proxy address", "address", scProxyAddress) + pendingOperations, err := executor.getPendingOperations(ctx, scProxyAddress) if err != nil { return err } - filteredPendingOperations := executor.filterOperations(pendingOperations) + filteredPendingOperations := executor.filterOperations("scCallExecutor", pendingOperations) return executor.executeOperations(ctx, filteredPendingOperations, scProxyAddress) } @@ -186,19 +143,6 @@ func (executor *scCallExecutor) parseResponse(response *data.VmValuesResponseDat return result, nil } -func (executor *scCallExecutor) filterOperations(pendingOperations map[uint64]bridgeCore.ProxySCCompleteCallData) map[uint64]bridgeCore.ProxySCCompleteCallData { - result := make(map[uint64]bridgeCore.ProxySCCompleteCallData) - for id, callData := range pendingOperations { - if executor.filter.ShouldExecute(callData) { - result[id] = callData - } - } - - executor.log.Debug("scCallExecutor.filterOperations", "input pending ops", len(pendingOperations), "result pending ops", len(result)) - - return result -} - func (executor *scCallExecutor) executeOperations( ctx context.Context, pendingOperations map[uint64]bridgeCore.ProxySCCompleteCallData, @@ -210,12 +154,8 @@ func (executor *scCallExecutor) executeOperations( } for id, callData := range pendingOperations { - workingCtx, cancel := context.WithTimeout(ctx, executor.executionTimeout) - - executor.log.Debug("scCallExecutor.executeOperations", "executing ID", id, "call data", callData, - "maximum timeout", executor.executionTimeout) - err = executor.executeOperation(workingCtx, id, callData, networkConfig, scProxyAddress) - cancel() + executor.log.Debug("scCallExecutor.executeOperations", "executing ID", id, "call data", callData) + err = executor.executeOperation(context.Background(), id, callData, networkConfig, scProxyAddress) if err != nil { return fmt.Errorf("%w for call data: %s", err, callData) @@ -281,11 +221,6 @@ func (executor *scCallExecutor) executeOperation( return executor.transactionExecutor.ExecuteTransaction(ctx, networkConfig, scProxyAddress, scCallTxType, txGasLimit, dataBytes) } -// GetNumSentTransaction returns the total sent transactions -func (executor *scCallExecutor) GetNumSentTransaction() uint32 { - return executor.transactionExecutor.GetNumSentTransaction() -} - // IsInterfaceNil returns true if there is no value under the interface func (executor *scCallExecutor) IsInterfaceNil() bool { return executor == nil diff --git a/executors/multiversx/scCallsExecutor_test.go b/executors/multiversx/scCallsExecutor_test.go index 2c028c5a..2789220c 100644 --- a/executors/multiversx/scCallsExecutor_test.go +++ b/executors/multiversx/scCallsExecutor_test.go @@ -130,35 +130,10 @@ func TestNewScCallExecutor(t *testing.T) { assert.Nil(t, executor) assert.NotNil(t, err) }) - t.Run("invalid value for TimeInSecondsBetweenChecks should error", func(t *testing.T) { - t.Parallel() - - args := createMockArgsScCallExecutor() - args.TransactionChecks = createMockCheckConfigs() - args.TransactionChecks.TimeInSecondsBetweenChecks = 0 - - executor, err := NewScCallExecutor(args) - assert.Nil(t, executor) - assert.ErrorIs(t, err, errInvalidValue) - assert.Contains(t, err.Error(), "for TransactionChecks.TimeInSecondsBetweenChecks, minimum: 1, got: 0") - }) - t.Run("invalid value for ExecutionTimeoutInSeconds should error", func(t *testing.T) { - t.Parallel() - - args := createMockArgsScCallExecutor() - args.TransactionChecks = createMockCheckConfigs() - args.TransactionChecks.ExecutionTimeoutInSeconds = 0 - - executor, err := NewScCallExecutor(args) - assert.Nil(t, executor) - assert.ErrorIs(t, err, errInvalidValue) - assert.Contains(t, err.Error(), "for TransactionChecks.ExecutionTimeoutInSeconds, minimum: 1, got: 0") - }) t.Run("invalid MaxGasLimitToUse should error", func(t *testing.T) { t.Parallel() args := createMockArgsScCallExecutor() - args.TransactionChecks = createMockCheckConfigs() args.ExecutorConfig.MaxGasLimitToUse = minGasToExecuteSCCalls - 1 executor, err := NewScCallExecutor(args) @@ -171,7 +146,6 @@ func TestNewScCallExecutor(t *testing.T) { t.Parallel() args := createMockArgsScCallExecutor() - args.TransactionChecks = createMockCheckConfigs() args.ExecutorConfig.GasLimitForOutOfGasTransactions = minGasToExecuteSCCalls - 1 executor, err := NewScCallExecutor(args) @@ -180,20 +154,10 @@ func TestNewScCallExecutor(t *testing.T) { assert.Contains(t, err.Error(), "provided: 2009999, absolute minimum required: 2010000") assert.Contains(t, err.Error(), "GasLimitForOutOfGasTransactions") }) - t.Run("should work without transaction checks", func(t *testing.T) { - t.Parallel() - - args := createMockArgsScCallExecutor() - - executor, err := NewScCallExecutor(args) - assert.NotNil(t, executor) - assert.Nil(t, err) - }) - t.Run("should work with transaction checks", func(t *testing.T) { + t.Run("should work", func(t *testing.T) { t.Parallel() args := createMockArgsScCallExecutor() - args.TransactionChecks = createMockCheckConfigs() executor, err := NewScCallExecutor(args) assert.NotNil(t, executor) @@ -399,8 +363,6 @@ func TestScCallExecutor_Execute(t *testing.T) { args := createMockArgsScCallExecutor() args.ExecutorConfig.MaxGasLimitToUse = 5000000 - args.TransactionChecks = createMockCheckConfigs() - args.TransactionChecks.TimeInSecondsBetweenChecks = 1 args.Proxy = &interactors.ProxyStub{ ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { @@ -449,8 +411,6 @@ func TestScCallExecutor_Execute(t *testing.T) { args := createMockArgsScCallExecutor() args.ExecutorConfig.MaxGasLimitToUse = 250000000 - args.TransactionChecks = createMockCheckConfigs() - args.TransactionChecks.TimeInSecondsBetweenChecks = 1 sendWasCalled := false args.Proxy = &interactors.ProxyStub{ @@ -528,8 +488,6 @@ func TestScCallExecutor_Execute(t *testing.T) { args := createMockArgsScCallExecutor() args.ScProxyBech32Addresses = append(args.ScProxyBech32Addresses, "erd1qqqqqqqqqqqqqpgqzyuaqg3dl7rqlkudrsnm5ek0j3a97qevd8sszj0glf") args.ExecutorConfig.MaxGasLimitToUse = 250000000 - args.TransactionChecks = createMockCheckConfigs() - args.TransactionChecks.TimeInSecondsBetweenChecks = 1 args.Proxy = &interactors.ProxyStub{ ExecuteVMQueryCalled: func(ctx context.Context, vmRequest *data.VmValueRequest) (*data.VmValuesResponseData, error) { @@ -780,17 +738,3 @@ func TestScCallExecutor_Execute(t *testing.T) { assert.True(t, sendWasCalled) }) } - -func TestScCallExecutor_GetNumSentTransaction(t *testing.T) { - t.Parallel() - - args := createMockArgsScCallExecutor() - args.TransactionExecutor = &testsCommon.TransactionExecutorStub{ - GetNumSentTransactionCalled: func() uint32 { - return 37 - }, - } - - executor, _ := NewScCallExecutor(args) - assert.Equal(t, uint32(37), executor.GetNumSentTransaction()) -} diff --git a/executors/multiversx/transactionExecutor.go b/executors/multiversx/transactionExecutor.go index f8d5ceed..58a2e258 100644 --- a/executors/multiversx/transactionExecutor.go +++ b/executors/multiversx/transactionExecutor.go @@ -47,6 +47,7 @@ type transactionExecutor struct { timeBetweenChecks time.Duration closeAppOnError bool extraDelayOnError time.Duration + executionTimeout time.Duration closeAppChan chan struct{} checkTransactionResults bool mutCriticalSection sync.Mutex @@ -77,6 +78,7 @@ func NewTransactionExecutor(args ArgsTransactionExecutor) (*transactionExecutor, timeBetweenChecks: time.Second * time.Duration(args.TransactionChecks.TimeInSecondsBetweenChecks), closeAppOnError: args.TransactionChecks.CloseAppOnError, extraDelayOnError: time.Second * time.Duration(args.TransactionChecks.ExtraDelayInSecondsOnError), + executionTimeout: time.Second * time.Duration(args.TransactionChecks.ExecutionTimeoutInSeconds), closeAppChan: args.CloseAppChan, }, nil } @@ -161,7 +163,14 @@ func (executor *transactionExecutor) ExecuteTransaction( Value: "0", } - hash, err := executor.executeAsCriticalSection(ctx, tx) + workingCtx := ctx + if executor.checkTransactionResults { + var cancel func() + workingCtx, cancel = context.WithTimeout(ctx, executor.executionTimeout) + defer cancel() + } + + hash, err := executor.executeAsCriticalSection(workingCtx, tx) if err != nil { return err } @@ -176,7 +185,7 @@ func (executor *transactionExecutor) ExecuteTransaction( atomic.AddUint32(&executor.numSentTransactions, 1) - return executor.handleResults(ctx, hash) + return executor.handleResults(workingCtx, hash) } func (executor *transactionExecutor) executeAsCriticalSection(ctx context.Context, tx *transaction.FrontendTransaction) (string, error) { diff --git a/integrationTests/relayers/slowTests/edgeCases_test.go b/integrationTests/relayers/slowTests/edgeCases_test.go index bccd3751..51502af6 100644 --- a/integrationTests/relayers/slowTests/edgeCases_test.go +++ b/integrationTests/relayers/slowTests/edgeCases_test.go @@ -173,6 +173,7 @@ func TestRelayerShouldExecuteMultipleSwapsWithLargeData(t *testing.T) { ValueToTransferToMvx: big.NewInt(50), ValueToSendFromMvX: nil, MvxSCCallData: scCallData, + MvxFaultySCCall: true, } usdcToken.TestOperations = append(usdcToken.TestOperations, tokenOperation) } diff --git a/integrationTests/relayers/slowTests/framework/multiversxHandler.go b/integrationTests/relayers/slowTests/framework/multiversxHandler.go index afafd946..ebf5402c 100644 --- a/integrationTests/relayers/slowTests/framework/multiversxHandler.go +++ b/integrationTests/relayers/slowTests/framework/multiversxHandler.go @@ -77,8 +77,6 @@ const ( getBurnBalancesFunction = "getBurnBalances" getTotalBalancesFunction = "getTotalBalances" getTokenLiquidityFunction = "getTokenLiquidity" - getRefundTransactions = "getRefundTransactions" - executeRefundTransaction = "executeRefundTransaction" ) var ( @@ -1118,53 +1116,6 @@ func (handler *MultiversxHandler) scCallAndCheckTx( return hash, txResult } -// RefundAllFromScBridgeProxy will refund transactions from the bridge proxy, if existing -func (handler *MultiversxHandler) RefundAllFromScBridgeProxy(ctx context.Context) { - refundIDs := handler.getAllRefundIDsFromScBridgeProxy(ctx) - if len(refundIDs) == 0 { - return - } - - for _, refundID := range refundIDs { - handler.refundTransactionInScBridgeProxy(ctx, refundID) - } -} - -func (handler *MultiversxHandler) getAllRefundIDsFromScBridgeProxy(ctx context.Context) []uint64 { - responseBytes := handler.ChainSimulator.ExecuteVMQuery( - ctx, - handler.ScProxyAddress, - getRefundTransactions, - make([]string, 0), - ) - - numResponseLines := len(responseBytes) - require.Equal(handler, 0, numResponseLines%2, "expected an even number on response") - - refundIDs := make([]uint64, 0, numResponseLines/2) - for i := 0; i < numResponseLines; i += 2 { - refundID := big.NewInt(0).SetBytes(responseBytes[i]) - refundIDs = append(refundIDs, refundID.Uint64()) - } - - return refundIDs -} - -func (handler *MultiversxHandler) refundTransactionInScBridgeProxy(ctx context.Context, refundID uint64) { - log.Info("sending refund transaction in SC bridge proxy", "refund ID", refundID) - handler.scCallAndCheckTx( - ctx, - handler.SCExecutorKeys, // anyone can call this, for example, the sc executor - handler.ScProxyAddress, - "0", - generalSCCallGasLimit, - executeRefundTransaction, - []string{ - hex.EncodeToString(big.NewInt(0).SetUint64(refundID).Bytes()), - }, - ) -} - func getHexBool(input bool) string { if input { return hexTrue diff --git a/integrationTests/relayers/slowTests/framework/testSetup.go b/integrationTests/relayers/slowTests/framework/testSetup.go index ff951ede..2daf9f6e 100644 --- a/integrationTests/relayers/slowTests/framework/testSetup.go +++ b/integrationTests/relayers/slowTests/framework/testSetup.go @@ -134,11 +134,16 @@ func (setup *TestSetup) startScCallerModule() { GasLimitForOutOfGasTransactions: 30_000_000, // gas to use when a higher than max allowed is encountered PollingIntervalInMillis: 1000, // 1 second }, + RefundExecutor: config.RefundExecutorConfig{ + GasToExecute: 30_000_000, + PollingIntervalInMillis: 1000, + }, Filter: config.PendingOperationsFilterConfig{ AllowedEthAddresses: []string{"*"}, AllowedMvxAddresses: []string{"*"}, AllowedTokens: []string{"*"}, }, + Logs: config.LogsConfig{}, TransactionChecks: config.TransactionChecksConfig{ CheckTransactionResults: true, CloseAppOnError: false, @@ -235,6 +240,10 @@ func (setup *TestSetup) processNumScCallsOperations(token TestTokenParams) { for _, op := range token.TestOperations { if len(op.MvxSCCallData) > 0 || op.MvxForceSCCall { atomic.AddUint32(&setup.numScCallsInTest, 1) + if op.MvxFaultySCCall { + // one more call for the refund operation + atomic.AddUint32(&setup.numScCallsInTest, 1) + } } } } diff --git a/integrationTests/relayers/slowTests/refundWithChainSimulator_test.go b/integrationTests/relayers/slowTests/refundWithChainSimulator_test.go index 3503c4ec..cf76e252 100644 --- a/integrationTests/relayers/slowTests/refundWithChainSimulator_test.go +++ b/integrationTests/relayers/slowTests/refundWithChainSimulator_test.go @@ -610,11 +610,13 @@ func TestRelayersShouldExecuteTransfersWithRefund(t *testing.T) { ValueToTransferToMvx: big.NewInt(500), ValueToSendFromMvX: nil, MvxSCCallData: callData, + MvxFaultySCCall: true, }, { ValueToTransferToMvx: big.NewInt(600), ValueToSendFromMvX: nil, MvxSCCallData: callData, + MvxFaultySCCall: true, }, } usdcToken.DeltaBalances = map[framework.HalfBridgeIdentifier]framework.DeltaBalancesOnKeys{ @@ -691,11 +693,13 @@ func TestRelayersShouldExecuteTransfersWithRefund(t *testing.T) { ValueToTransferToMvx: big.NewInt(500), ValueToSendFromMvX: nil, MvxSCCallData: callData, + MvxFaultySCCall: true, }, { ValueToTransferToMvx: big.NewInt(600), ValueToSendFromMvX: nil, MvxSCCallData: callData, + MvxFaultySCCall: true, }, } usdcToken.DeltaBalances = map[framework.HalfBridgeIdentifier]framework.DeltaBalancesOnKeys{ diff --git a/integrationTests/relayers/slowTests/testFlow.go b/integrationTests/relayers/slowTests/testFlow.go index 671d9517..8fc816d7 100644 --- a/integrationTests/relayers/slowTests/testFlow.go +++ b/integrationTests/relayers/slowTests/testFlow.go @@ -53,9 +53,6 @@ func (flow *testFlow) process() (finished bool) { flow.setup.MultiversxHandler.MoveRefundBatchToSafe(flow.setup.Ctx) } - //TODO: move this logic into the SC calls executor - flow.setup.MultiversxHandler.RefundAllFromScBridgeProxy(flow.setup.Ctx) - transferDoneForSecondHalf := flow.setup.AreAllTransfersCompleted(framework.SecondHalfBridge, flow.tokens...) if !flow.secondHalfBridgeDone && transferDoneForSecondHalf { flow.setup.CheckCorrectnessOnMintBurnTokens(flow.tokens...)