diff --git a/app/app.go b/app/app.go index 577f325f47..23c1727c71 100644 --- a/app/app.go +++ b/app/app.go @@ -556,9 +556,6 @@ func New( ) evmSs := app.GetSubspace(evmtypes.ModuleName) - // Get gas config for stateful contracts. - gasConfig := storetypes.TransientGasConfig() - app.EvmKeeper = evmkeeper.NewKeeper( appCodec, keys[evmtypes.StoreKey], @@ -573,7 +570,7 @@ func New( precompiles.StatefulContracts( app.FungibleKeeper, appCodec, - gasConfig, + storetypes.TransientGasConfig(), ), app.ConsensusParamsKeeper, aggregateAllKeys(keys, tKeys, memKeys), diff --git a/changelog.md b/changelog.md index bef780d4a2..7ef55517f7 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ * [2634](https://github.com/zeta-chain/node/pull/2634) - add support for EIP-1559 gas fees * [2597](https://github.com/zeta-chain/node/pull/2597) - Add generic rpc metrics to zetaclient * [2538](https://github.com/zeta-chain/node/pull/2538) - add background worker routines to shutdown zetaclientd when needed for tss migration +* [2633](https://github.com/zeta-chain/node/pull/2633) - support for stateful precompiled contracts. ### Refactor @@ -57,7 +58,6 @@ * [2524](https://github.com/zeta-chain/node/pull/2524) - add inscription envelop parsing * [2560](https://github.com/zeta-chain/node/pull/2560) - add support for Solana SOL token withdraw * [2533](https://github.com/zeta-chain/node/pull/2533) - parse memo from both OP_RETURN and inscription -* [2633](https://github.com/zeta-chain/node/pull/2633) - support for stateful precompiled contracts. ### Refactor @@ -115,6 +115,7 @@ * [2415](https://github.com/zeta-chain/node/pull/2415) - add e2e test for upgrade and test admin functionalities * [2440](https://github.com/zeta-chain/node/pull/2440) - Add e2e test for TSS migration * [2473](https://github.com/zeta-chain/node/pull/2473) - add e2e tests for most used admin transactions +* [2703](https://github.com/zeta-chain/node/pull/2703) - add e2e tests for stateful precompiled contracts ### Fixes diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index cb24096859..2732d8daf6 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -229,6 +229,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { e2etests.TestERC20DepositRestrictedName, } zetaTests := []string{ + e2etests.TestZetaPrecompilesName, e2etests.TestZetaWithdrawName, e2etests.TestMessagePassingExternalChainsName, e2etests.TestMessagePassingRevertFailExternalChainsName, diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 402eed367e..2b44003a21 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -15,6 +15,7 @@ const ( TestZetaDepositRestrictedName = "zeta_deposit_restricted" TestZetaWithdrawName = "zeta_withdraw" TestZetaWithdrawBTCRevertName = "zeta_withdraw_btc_revert" // #nosec G101 - not a hardcoded password + TestZetaPrecompilesName = "zeta_precompiles" /* Message passing tests @@ -123,6 +124,12 @@ var AllE2ETests = []runner.E2ETest{ /* ZETA tests */ + runner.NewE2ETest( + TestZetaPrecompilesName, + "call Regular stateful precompiled contract", + []runner.ArgDefinition{}, + TestPrecompilesRegular, + ), runner.NewE2ETest( TestZetaDepositName, "deposit ZETA from Ethereum to ZEVM", diff --git a/e2e/e2etests/test_precompiles_regular.go b/e2e/e2etests/test_precompiles_regular.go new file mode 100644 index 0000000000..e15ae529b3 --- /dev/null +++ b/e2e/e2etests/test_precompiles_regular.go @@ -0,0 +1,27 @@ +package e2etests + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/zetacore/e2e/runner" + "github.com/zeta-chain/zetacore/precompiles/regular" +) + +func TestPrecompilesRegular(r *runner.E2ERunner, args []string) { + require.Len(r, args, 0, "No arguments expected") + + dummyBech32Addr := "1h8duy2dltz9xz0qqhm5wvcnj02upy887fyn43u" + + // Call the Regular contract in the static precompile address. + contract, err := regular.NewRegularCaller(regular.ContractAddress, r.EVMClient) + require.NoError(r, err, "Failed to create Regular contract caller") + + addr, err := contract.Bech32ToHexAddr( + nil, + common.HexToAddress("0xB9Dbc229Bf588A613C00BEE8e662727AB8121cfE").String(), + ) + require.NoError(r, err, "Failed to call Bech32ToHexAddr in Regular precompiled contract") + + require.Equal(r, dummyBech32Addr, addr.String(), "Expected address %s, got %s", dummyBech32Addr, addr.String()) +} diff --git a/precompiles/regular/src/IRegular.sol b/precompiles/regular/IRegular.sol similarity index 100% rename from precompiles/regular/src/IRegular.sol rename to precompiles/regular/IRegular.sol diff --git a/precompiles/regular/IRegularABI.go b/precompiles/regular/IRegularABI.go new file mode 100644 index 0000000000..582d9f6b83 --- /dev/null +++ b/precompiles/regular/IRegularABI.go @@ -0,0 +1,264 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package regular + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// RegularMetaData contains all meta data concerning the Regular contract. +var RegularMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"string\",\"name\":\"bech32\",\"type\":\"string\"}],\"name\":\"bech32ToHexAddr\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"prefix\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"bech32ify\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"bech32\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"string\",\"name\":\"method\",\"type\":\"string\"},{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"regularCall\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"result\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]", +} + +// RegularABI is the input ABI used to generate the binding from. +// Deprecated: Use RegularMetaData.ABI instead. +var RegularABI = RegularMetaData.ABI + +// Regular is an auto generated Go binding around an Ethereum contract. +type Regular struct { + RegularCaller // Read-only binding to the contract + RegularTransactor // Write-only binding to the contract + RegularFilterer // Log filterer for contract events +} + +// RegularCaller is an auto generated read-only Go binding around an Ethereum contract. +type RegularCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// RegularTransactor is an auto generated write-only Go binding around an Ethereum contract. +type RegularTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// RegularFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type RegularFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// RegularSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type RegularSession struct { + Contract *Regular // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// RegularCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type RegularCallerSession struct { + Contract *RegularCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// RegularTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type RegularTransactorSession struct { + Contract *RegularTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// RegularRaw is an auto generated low-level Go binding around an Ethereum contract. +type RegularRaw struct { + Contract *Regular // Generic contract binding to access the raw methods on +} + +// RegularCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type RegularCallerRaw struct { + Contract *RegularCaller // Generic read-only contract binding to access the raw methods on +} + +// RegularTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type RegularTransactorRaw struct { + Contract *RegularTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewRegular creates a new instance of Regular, bound to a specific deployed contract. +func NewRegular(address common.Address, backend bind.ContractBackend) (*Regular, error) { + contract, err := bindRegular(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Regular{RegularCaller: RegularCaller{contract: contract}, RegularTransactor: RegularTransactor{contract: contract}, RegularFilterer: RegularFilterer{contract: contract}}, nil +} + +// NewRegularCaller creates a new read-only instance of Regular, bound to a specific deployed contract. +func NewRegularCaller(address common.Address, caller bind.ContractCaller) (*RegularCaller, error) { + contract, err := bindRegular(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &RegularCaller{contract: contract}, nil +} + +// NewRegularTransactor creates a new write-only instance of Regular, bound to a specific deployed contract. +func NewRegularTransactor(address common.Address, transactor bind.ContractTransactor) (*RegularTransactor, error) { + contract, err := bindRegular(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &RegularTransactor{contract: contract}, nil +} + +// NewRegularFilterer creates a new log filterer instance of Regular, bound to a specific deployed contract. +func NewRegularFilterer(address common.Address, filterer bind.ContractFilterer) (*RegularFilterer, error) { + contract, err := bindRegular(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &RegularFilterer{contract: contract}, nil +} + +// bindRegular binds a generic wrapper to an already deployed contract. +func bindRegular(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := RegularMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Regular *RegularRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Regular.Contract.RegularCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Regular *RegularRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Regular.Contract.RegularTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Regular *RegularRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Regular.Contract.RegularTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Regular *RegularCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Regular.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Regular *RegularTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Regular.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Regular *RegularTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Regular.Contract.contract.Transact(opts, method, params...) +} + +// Bech32ToHexAddr is a free data retrieval call binding the contract method 0xe4e2a4ec. +// +// Solidity: function bech32ToHexAddr(string bech32) view returns(address addr) +func (_Regular *RegularCaller) Bech32ToHexAddr(opts *bind.CallOpts, bech32 string) (common.Address, error) { + var out []interface{} + err := _Regular.contract.Call(opts, &out, "bech32ToHexAddr", bech32) + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// Bech32ToHexAddr is a free data retrieval call binding the contract method 0xe4e2a4ec. +// +// Solidity: function bech32ToHexAddr(string bech32) view returns(address addr) +func (_Regular *RegularSession) Bech32ToHexAddr(bech32 string) (common.Address, error) { + return _Regular.Contract.Bech32ToHexAddr(&_Regular.CallOpts, bech32) +} + +// Bech32ToHexAddr is a free data retrieval call binding the contract method 0xe4e2a4ec. +// +// Solidity: function bech32ToHexAddr(string bech32) view returns(address addr) +func (_Regular *RegularCallerSession) Bech32ToHexAddr(bech32 string) (common.Address, error) { + return _Regular.Contract.Bech32ToHexAddr(&_Regular.CallOpts, bech32) +} + +// Bech32ify is a free data retrieval call binding the contract method 0x0615b74e. +// +// Solidity: function bech32ify(string prefix, address addr) view returns(string bech32) +func (_Regular *RegularCaller) Bech32ify(opts *bind.CallOpts, prefix string, addr common.Address) (string, error) { + var out []interface{} + err := _Regular.contract.Call(opts, &out, "bech32ify", prefix, addr) + + if err != nil { + return *new(string), err + } + + out0 := *abi.ConvertType(out[0], new(string)).(*string) + + return out0, err + +} + +// Bech32ify is a free data retrieval call binding the contract method 0x0615b74e. +// +// Solidity: function bech32ify(string prefix, address addr) view returns(string bech32) +func (_Regular *RegularSession) Bech32ify(prefix string, addr common.Address) (string, error) { + return _Regular.Contract.Bech32ify(&_Regular.CallOpts, prefix, addr) +} + +// Bech32ify is a free data retrieval call binding the contract method 0x0615b74e. +// +// Solidity: function bech32ify(string prefix, address addr) view returns(string bech32) +func (_Regular *RegularCallerSession) Bech32ify(prefix string, addr common.Address) (string, error) { + return _Regular.Contract.Bech32ify(&_Regular.CallOpts, prefix, addr) +} + +// RegularCall is a paid mutator transaction binding the contract method 0x93e3663d. +// +// Solidity: function regularCall(string method, address addr) returns(uint256 result) +func (_Regular *RegularTransactor) RegularCall(opts *bind.TransactOpts, method string, addr common.Address) (*types.Transaction, error) { + return _Regular.contract.Transact(opts, "regularCall", method, addr) +} + +// RegularCall is a paid mutator transaction binding the contract method 0x93e3663d. +// +// Solidity: function regularCall(string method, address addr) returns(uint256 result) +func (_Regular *RegularSession) RegularCall(method string, addr common.Address) (*types.Transaction, error) { + return _Regular.Contract.RegularCall(&_Regular.TransactOpts, method, addr) +} + +// RegularCall is a paid mutator transaction binding the contract method 0x93e3663d. +// +// Solidity: function regularCall(string method, address addr) returns(uint256 result) +func (_Regular *RegularTransactorSession) RegularCall(method string, addr common.Address) (*types.Transaction, error) { + return _Regular.Contract.RegularCall(&_Regular.TransactOpts, method, addr) +} diff --git a/precompiles/regular/src/IRegularABI.json b/precompiles/regular/IRegularABI.json similarity index 100% rename from precompiles/regular/src/IRegularABI.json rename to precompiles/regular/IRegularABI.json diff --git a/precompiles/regular/regular.go b/precompiles/regular/regular.go index ece46b6b43..608ea03546 100644 --- a/precompiles/regular/regular.go +++ b/precompiles/regular/regular.go @@ -98,12 +98,15 @@ func (rc *Contract) Abi() abi.ABI { func (rc *Contract) RequiredGas(input []byte) uint64 { // base cost to prevent large input size baseCost := uint64(len(input)) * rc.kvGasConfig.WriteCostPerByte + + // get methodID (first 4 bytes) var methodID [4]byte copy(methodID[:], input[:4]) - requiredGas, ok := GasRequiredByMethod[methodID] - if ok { + + if requiredGas, ok := GasRequiredByMethod[methodID]; ok { return requiredGas + baseCost } + return baseCost } @@ -239,8 +242,6 @@ func (rc *Contract) Run(evm *vm.EVM, contract *vm.Contract, _ bool) ([]byte, err return rc.Bech32ToHexAddr(method, args) case Bech32ifyMethodName: return rc.Bech32ify(method, args) - // case OtherMethods: - // .. default: return nil, errors.New("unknown method") } diff --git a/precompiles/testutil/errors.go b/precompiles/testutil/errors.go new file mode 100644 index 0000000000..030f32276e --- /dev/null +++ b/precompiles/testutil/errors.go @@ -0,0 +1,39 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package testutil + +import ( + "fmt" + "strings" + + abci "github.com/cometbft/cometbft/abci/types" + evmtypes "github.com/zeta-chain/ethermint/x/evm/types" +) + +// CheckVMError is a helper function used to check if the transaction is reverted with the expected error message +// in the VmError field of the MsgEthereumResponse struct. +func CheckVMError(res abci.ResponseDeliverTx, expErrMsg string, args ...interface{}) error { + if !res.IsOK() { + return fmt.Errorf("code 0 was expected on response but got code %d", res.Code) + } + ethRes, err := evmtypes.DecodeTxResponse(res.Data) + if err != nil { + return fmt.Errorf("error occurred while decoding the TxResponse. %s", err) + } + expMsg := fmt.Sprintf(expErrMsg, args...) + if !strings.Contains(ethRes.VmError, expMsg) { + return fmt.Errorf( + "unexpected VmError on response. expected error to contain: %s, received: %s", + expMsg, + ethRes.VmError, + ) + } + return nil +} + +// CheckEthereumTxFailed checks if there is a VM error in the transaction response and returns the reason. +func CheckEthereumTxFailed(ethRes *evmtypes.MsgEthereumTxResponse) (string, bool) { + reason := ethRes.VmError + return reason, reason != "" +} diff --git a/precompiles/testutil/events.go b/precompiles/testutil/events.go new file mode 100644 index 0000000000..3689c55d0c --- /dev/null +++ b/precompiles/testutil/events.go @@ -0,0 +1,34 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package testutil + +import ( + "fmt" + "strings" + + //nolint:stylecheck,revive // it's common practice to use the global imports for Ginkgo and Gomega + "github.com/ethereum/go-ethereum/accounts/abi" +) + +// validateEvents checks if the provided event names are included as keys in the contract events. +func validateEvents(contractEvents map[string]abi.Event, events []string) ([]abi.Event, error) { + expEvents := make([]abi.Event, 0, len(events)) + for _, eventStr := range events { + event, found := contractEvents[eventStr] + if !found { + availableABIEvents := make([]string, 0, len(contractEvents)) + for event := range contractEvents { + availableABIEvents = append(availableABIEvents, event) + } + availableABIEventsStr := strings.Join(availableABIEvents, ", ") + return nil, fmt.Errorf( + "unknown event %q is not contained in given ABI events:\n%s", + eventStr, + availableABIEventsStr, + ) + } + expEvents = append(expEvents, event) + } + return expEvents, nil +} diff --git a/precompiles/testutil/logs.go b/precompiles/testutil/logs.go new file mode 100644 index 0000000000..fe16a64ed5 --- /dev/null +++ b/precompiles/testutil/logs.go @@ -0,0 +1,121 @@ +// Copyright Tharsis Labs Ltd.(Evmos) +// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE) + +package testutil + +import ( + "fmt" + + abci "github.com/cometbft/cometbft/abci/types" + "github.com/ethereum/go-ethereum/accounts/abi" + evmtypes "github.com/zeta-chain/ethermint/x/evm/types" + "golang.org/x/exp/slices" +) + +// CheckLogs checks the logs for the given events and whether the transaction was successful or not. +func CheckLogs(logArgs LogCheckArgs) error { + if len(logArgs.ExpEvents) != 0 && len(logArgs.ABIEvents) == 0 { + return fmt.Errorf("no ABI events provided in log check arguments, but expected events are present") + } + + expABIEvents, err := validateEvents(logArgs.ABIEvents, logArgs.ExpEvents) + if err != nil { + return err + } + + ethRes, err := evmtypes.DecodeTxResponse(logArgs.Res.Data) + if err != nil { + return fmt.Errorf("error while decoding ethereum tx response: %v", err) + } + + reason, failed := CheckEthereumTxFailed(ethRes) + if failed != !logArgs.ExpPass { + return fmt.Errorf( + "expected vm error found to be %t; got: %t (reason: %s)\nGas usage: %d/%d (~%d %%)", + !logArgs.ExpPass, + failed, + reason, + logArgs.Res.GasUsed, + logArgs.Res.GasWanted, + int64(float64(logArgs.Res.GasUsed)/float64(logArgs.Res.GasWanted)*100), + ) + } + + if err := CheckVMError(logArgs.Res, logArgs.ErrContains); err != nil { + return err + } + + if len(ethRes.Logs) != len(logArgs.ExpEvents) { + return fmt.Errorf("expected %d events in Ethereum response; got: %d", len(logArgs.ExpEvents), len(ethRes.Logs)) + } + + // Check if expected events are present in Ethereum response + availableEventIDs := make([]string, 0, len(ethRes.Logs)) + for _, log := range ethRes.Logs { + availableEventIDs = append(availableEventIDs, log.Topics[0]) + } + + expEventIDs := make([]string, 0, len(expABIEvents)) + for _, event := range expABIEvents { + expEventIDs = append(expEventIDs, event.ID.String()) + } + + for _, eventID := range expEventIDs { + if !slices.Contains(availableEventIDs, eventID) { + return fmt.Errorf("expected event with ID %v not found in Ethereum response", eventID) + } + } + + return nil +} + +// LogCheckArgs is a struct that contains configuration for the log checking. +type LogCheckArgs struct { + // ABIEvents is a map of available abi.Event corresponding to the corresponding event names, + // which are available in the contract ABI. + ABIEvents map[string]abi.Event + // ErrContains is the error message that is expected to be contained in the transaction response. + ErrContains string + // ExpEvents are the events which are expected to be emitted. + ExpEvents []string + // ExpPass is whether the transaction is expected to pass or not. + ExpPass bool + // Res is the response of the transaction. + // + // NOTE: This does not have to be set when using contracts.CallContractAndCheckLogs. + Res abci.ResponseDeliverTx +} + +// WithABIEvents sets the ABIEvents field of LogCheckArgs. +func (l LogCheckArgs) WithABIEvents(abiEvents map[string]abi.Event) LogCheckArgs { + l.ABIEvents = abiEvents + return l +} + +// WithErrContains sets the ErrContains field of LogCheckArgs. +// If any printArgs are provided, they are used to format the error message. +func (l LogCheckArgs) WithErrContains(errContains string, printArgs ...interface{}) LogCheckArgs { + if len(printArgs) > 0 { + errContains = fmt.Sprintf(errContains, printArgs...) + } + l.ErrContains = errContains + return l +} + +// WithExpEvents sets the ExpEvents field of LogCheckArgs. +func (l LogCheckArgs) WithExpEvents(expEvents ...string) LogCheckArgs { + l.ExpEvents = expEvents + return l +} + +// WithExpPass sets the ExpPass field of LogCheckArgs. +func (l LogCheckArgs) WithExpPass(expPass bool) LogCheckArgs { + l.ExpPass = expPass + return l +} + +// WithRes sets the Res field of LogCheckArgs. +func (l LogCheckArgs) WithRes(res abci.ResponseDeliverTx) LogCheckArgs { + l.Res = res + return l +}