diff --git a/.github/codecov.yml b/.github/codecov.yml index 8e55c3df0..dcb3fa110 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -21,7 +21,7 @@ coverage: status: project: default: - target: 66% + target: 60% if_not_found: success threshold: 1% # Allow coverage to drop by X%, posting a success status. # removed_code_behavior: Takes values [off, removals_only, adjust_base] diff --git a/.github/workflows/e2e-evm.yml b/.github/workflows/e2e-evm.yml new file mode 100644 index 000000000..4d55fa5fb --- /dev/null +++ b/.github/workflows/e2e-evm.yml @@ -0,0 +1,83 @@ +name: EVM e2e tests + +on: + pull_request: + paths: + [ + "**.go", + "**.proto", + "go.mod", + "go.sum", + "**go.mod", + "**go.sum", + "contrib/docker/*", + "**.js", + "**.json", + ] + +# Allow concurrent runs on main/release branches but isolates other branches +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }} + cancel-in-progress: ${{ ! (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) }} + +jobs: + e2e-evm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.21 + cache: true + + # Use GitHub actions output paramters to get go paths. For more info, see + # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions + - name: "Set output variables for go cache" + id: go-cache-paths + run: | + echo "go-build-cache=$(go env GOCACHE)" >> $GITHUB_OUTPUT + echo "go-mod-cache=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT + + - name: "Go build cache" + uses: actions/cache@v4 + with: + path: ${{ steps.go-cache-paths.outputs.go-build-cache }} + key: go-build-cache-${{ hashFiles('**/go.sum') }} + + - name: "Go mod cache" + uses: actions/cache@v4 + with: + path: ${{ steps.go-cache-paths.outputs.go-mod-cache }} + key: go-mod-cache-${{ hashFiles('**/go.sum') }} + + - name: "Install just" + # casey/just: https://just.systems/man/en/chapter_6.html + # taiki-e/install-action: https://github.com/taiki-e/install-action + uses: taiki-e/install-action@just + + - name: "Build the nibid binary" + run: | + just install + + - name: Setup NodeJS with npm caching + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: NPM Install + run: npm install + working-directory: "e2e/evm" + + - name: "Launch localnet" + run: | + just localnet --no-build & + sleep 6 + + - name: Run tests + run: npm test + working-directory: "e2e/evm" + env: + JSON_RPC_ENDPOINT: http://127.0.0.1:8545 + MNEMONIC: guard cream sadness conduct invite crumble clock pudding hole grit liar hotel maid produce squeeze return argue turtle know drive eight casino maze host diff --git a/.gitignore b/.gitignore index 185bb48ff..e2638d272 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -**__pycache** +### Main Gitignore unit-test-reports vue/node_modules vue/dist @@ -19,3 +19,343 @@ temp temp* txout.json vote.json +**__pycache** + +### TypeScript and Friends + +node_modules +ui-debug.log +.firebase/ +.idea +.vscode +/public/ +.env +firebase-debug.log +**/bun.lockb +out-* +exit-status-* +.DS_Store +.npmrc + +### Node ### + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + + + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### react ### +.DS_* +*.log +logs +**/*.backup.* +**/*.back.* + +node_modules +bower_components + +*.sublime* + +psd +thumb +sketch + +.firebase/ + +public/js/ +.npmrc + +playwright/test-results/ +playwright/playwright-report/ +playwright/playwright/.cache/ +playwright/chrome-extensions/keplr/ +playwright/yarn.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 86d6aba82..e177b2cc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1869](https://github.com/NibiruChain/nibiru/pull/1869) - feat(eth): Module and start of keeper tests - [#1871](https://github.com/NibiruChain/nibiru/pull/1871) - feat(evm): app config and json-rpc - [#1873](https://github.com/NibiruChain/nibiru/pull/1873) - feat(evm): keeper collections and grpc query impls for EthAccount, NibiruAccount +- [#1883](https://github.com/NibiruChain/nibiru/pull/1883) - feat(evm): keeper logic, Ante handlers, EthCall, and EVM transactions. +- [#1887](https://github.com/NibiruChain/nibiru/pull/1887) - test(evm): eth api integration test suite +- [#1889](https://github.com/NibiruChain/nibiru/pull/1889) - feat: implemented basic evm tx methods - [#1895](https://github.com/NibiruChain/nibiru/pull/1895) - refactor(geth): Reference go-ethereum as a submodule for easier change tracking with upstream #### Dapp modules: perp, spot, oracle, etc diff --git a/app/ante.go b/app/ante.go index a0d70a1bc..21435d335 100644 --- a/app/ante.go +++ b/app/ante.go @@ -1,92 +1,153 @@ package app import ( - sdkerrors "cosmossdk.io/errors" + "fmt" + wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" - wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" - "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/errors" - sdkante "github.com/cosmos/cosmos-sdk/x/auth/ante" ibcante "github.com/cosmos/ibc-go/v7/modules/core/ante" - ibckeeper "github.com/cosmos/ibc-go/v7/modules/core/keeper" + + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" "github.com/NibiruChain/nibiru/app/ante" + "github.com/NibiruChain/nibiru/eth" devgasante "github.com/NibiruChain/nibiru/x/devgas/v1/ante" - devgaskeeper "github.com/NibiruChain/nibiru/x/devgas/v1/keeper" + "github.com/NibiruChain/nibiru/x/evm" ) -type AnteHandlerOptions struct { - sdkante.HandlerOptions - IBCKeeper *ibckeeper.Keeper - DevGasKeeper *devgaskeeper.Keeper - DevGasBankKeeper devgasante.BankKeeper - - TxCounterStoreKey types.StoreKey - WasmConfig *wasmtypes.WasmConfig -} - // NewAnteHandler returns and AnteHandler that checks and increments sequence // numbers, checks signatures and account numbers, and deducts fees from the // first signer. -func NewAnteHandler(options AnteHandlerOptions) (sdk.AnteHandler, error) { - if err := options.ValidateAndClean(); err != nil { - return nil, err +func NewAnteHandler( + keepers AppKeepers, + opts ante.AnteHandlerOptions, +) sdk.AnteHandler { + return func( + ctx sdk.Context, tx sdk.Tx, sim bool, + ) (newCtx sdk.Context, err error) { + if err := opts.ValidateAndClean(); err != nil { + return ctx, err + } + + var anteHandler sdk.AnteHandler + hasExt, typeUrl := TxHasExtensions(tx) + // TODO: handle ethereum txs + if hasExt && typeUrl != "" { + anteHandler = AnteHandlerExtendedTx(typeUrl, keepers, opts, ctx) + return anteHandler(ctx, tx, sim) + } + + switch tx.(type) { + case sdk.Tx: + anteHandler = AnteHandlerStandardTx(opts) + default: + return ctx, fmt.Errorf("invalid tx type (%T) in AnteHandler", tx) + } + return anteHandler(ctx, tx, sim) } +} +func AnteHandlerStandardTx(opts ante.AnteHandlerOptions) sdk.AnteHandler { anteDecorators := []sdk.AnteDecorator{ - sdkante.NewSetUpContextDecorator(), - wasmkeeper.NewLimitSimulationGasDecorator(options.WasmConfig.SimulationGasLimit), - wasmkeeper.NewCountTXDecorator(options.TxCounterStoreKey), - sdkante.NewExtensionOptionsDecorator(nil), - sdkante.NewValidateBasicDecorator(), - sdkante.NewTxTimeoutHeightDecorator(), - sdkante.NewValidateMemoDecorator(options.AccountKeeper), + AnteDecoratorPreventEtheruemTxMsgs{}, // reject MsgEthereumTxs + authante.NewSetUpContextDecorator(), + wasmkeeper.NewLimitSimulationGasDecorator(opts.WasmConfig.SimulationGasLimit), + wasmkeeper.NewCountTXDecorator(opts.TxCounterStoreKey), + authante.NewExtensionOptionsDecorator(opts.ExtensionOptionChecker), + authante.NewValidateBasicDecorator(), + authante.NewTxTimeoutHeightDecorator(), + authante.NewValidateMemoDecorator(opts.AccountKeeper), ante.NewPostPriceFixedPriceDecorator(), ante.AnteDecoratorStakingCommission{}, - sdkante.NewConsumeGasForTxSizeDecorator(options.AccountKeeper), + authante.NewConsumeGasForTxSizeDecorator(opts.AccountKeeper), // Replace fee ante from cosmos auth with a custom one. - sdkante.NewDeductFeeDecorator( - options.AccountKeeper, options.BankKeeper, options.FeegrantKeeper, options.TxFeeChecker), + authante.NewDeductFeeDecorator( + opts.AccountKeeper, opts.BankKeeper, opts.FeegrantKeeper, opts.TxFeeChecker), devgasante.NewDevGasPayoutDecorator( - options.DevGasBankKeeper, options.DevGasKeeper), - // SetPubKeyDecorator must be called before all signature verification decorators - sdkante.NewSetPubKeyDecorator(options.AccountKeeper), - sdkante.NewValidateSigCountDecorator(options.AccountKeeper), - sdkante.NewSigGasConsumeDecorator(options.AccountKeeper, options.SigGasConsumer), - sdkante.NewSigVerificationDecorator(options.AccountKeeper, options.SignModeHandler), - sdkante.NewIncrementSequenceDecorator(options.AccountKeeper), - ibcante.NewRedundantRelayDecorator(options.IBCKeeper), + opts.DevGasBankKeeper, opts.DevGasKeeper), + // NOTE: SetPubKeyDecorator must be called before all signature verification decorators + authante.NewSetPubKeyDecorator(opts.AccountKeeper), + authante.NewValidateSigCountDecorator(opts.AccountKeeper), + authante.NewSigGasConsumeDecorator(opts.AccountKeeper, opts.SigGasConsumer), + authante.NewSigVerificationDecorator(opts.AccountKeeper, opts.SignModeHandler), + authante.NewIncrementSequenceDecorator(opts.AccountKeeper), + ibcante.NewRedundantRelayDecorator(opts.IBCKeeper), } - return sdk.ChainAnteDecorators(anteDecorators...), nil + return sdk.ChainAnteDecorators(anteDecorators...) } -func (opts *AnteHandlerOptions) ValidateAndClean() error { - if opts.AccountKeeper == nil { - return AnteHandlerError("account keeper") - } - if opts.BankKeeper == nil { - return AnteHandlerError("bank keeper") - } - if opts.SignModeHandler == nil { - return AnteHandlerError("sign mode handler") +func TxHasExtensions(tx sdk.Tx) (hasExt bool, typeUrl string) { + extensionTx, ok := tx.(authante.HasExtensionOptionsTx) + if !ok { + return false, "" } - if opts.SigGasConsumer == nil { - opts.SigGasConsumer = sdkante.DefaultSigVerificationGasConsumer - } - if opts.WasmConfig == nil { - return AnteHandlerError("wasm config") - } - if opts.DevGasKeeper == nil { - return AnteHandlerError("devgas keeper") + + extOpts := extensionTx.GetExtensionOptions() + if len(extOpts) == 0 { + return false, "" } - if opts.IBCKeeper == nil { - return AnteHandlerError("ibc keeper") + + return true, extOpts[0].GetTypeUrl() +} + +func AnteHandlerExtendedTx( + typeUrl string, + keepers AppKeepers, + opts ante.AnteHandlerOptions, + ctx sdk.Context, +) (anteHandler sdk.AnteHandler) { + switch typeUrl { + case evm.TYPE_URL_ETHEREUM_TX: + anteHandler = NewAnteHandlerEVM(keepers, opts) + case eth.TYPE_URL_DYNAMIC_FEE_TX: + anteHandler = NewAnteHandlerNonEVM(keepers, opts) + default: + errUnsupported := fmt.Errorf( + `encountered tx with unsupported extension option, "%s"`, typeUrl) + return func( + ctx sdk.Context, tx sdk.Tx, simulate bool, + ) (newCtx sdk.Context, err error) { + return ctx, errUnsupported + } } - return nil + return anteHandler } -func AnteHandlerError(shortDesc string) error { - return sdkerrors.Wrapf(errors.ErrLogic, "%s is required for AnteHandler", shortDesc) +// NewAnteHandlerNonEVM: Default ante handler for non-EVM transactions. +func NewAnteHandlerNonEVM( + k AppKeepers, opts ante.AnteHandlerOptions, +) sdk.AnteHandler { + return sdk.ChainAnteDecorators( + AnteDecoratorPreventEtheruemTxMsgs{}, // reject MsgEthereumTxs + authante.NewSetUpContextDecorator(), + wasmkeeper.NewLimitSimulationGasDecorator(opts.WasmConfig.SimulationGasLimit), + wasmkeeper.NewCountTXDecorator(opts.TxCounterStoreKey), + // TODO: UD + // cosmosante.NewAuthzLimiterDecorator( // disable the Msg types that cannot be included on an authz.MsgExec msgs field + // sdk.MsgTypeURL(&evm.MsgEthereumTx{}), + // sdk.MsgTypeURL(&sdkvesting.MsgCreateVestingAccount{}), + // ), + authante.NewExtensionOptionsDecorator(opts.ExtensionOptionChecker), + authante.NewValidateBasicDecorator(), + authante.NewTxTimeoutHeightDecorator(), + authante.NewValidateMemoDecorator(opts.AccountKeeper), + // TODO: UD + // cosmosante.NewMinGasPriceDecorator(options.FeeMarketKeeper, options.EvmKeeper), + ante.NewPostPriceFixedPriceDecorator(), + ante.AnteDecoratorStakingCommission{}, + authante.NewConsumeGasForTxSizeDecorator(opts.AccountKeeper), + authante.NewDeductFeeDecorator( + opts.AccountKeeper, opts.BankKeeper, opts.FeegrantKeeper, opts.TxFeeChecker), + devgasante.NewDevGasPayoutDecorator( + opts.DevGasBankKeeper, opts.DevGasKeeper), + // NOTE: SetPubKeyDecorator must be called before all signature verification decorators + authante.NewSetPubKeyDecorator(opts.AccountKeeper), + authante.NewValidateSigCountDecorator(opts.AccountKeeper), + authante.NewSigGasConsumeDecorator(opts.AccountKeeper, opts.SigGasConsumer), + authante.NewSigVerificationDecorator(opts.AccountKeeper, opts.SignModeHandler), + authante.NewIncrementSequenceDecorator(opts.AccountKeeper), + ibcante.NewRedundantRelayDecorator(opts.IBCKeeper), + NewGasWantedDecorator(k), + ) } diff --git a/app/ante/handler_opts.go b/app/ante/handler_opts.go new file mode 100644 index 000000000..79c099a00 --- /dev/null +++ b/app/ante/handler_opts.go @@ -0,0 +1,56 @@ +package ante + +import ( + sdkerrors "cosmossdk.io/errors" + wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/errors" + sdkante "github.com/cosmos/cosmos-sdk/x/auth/ante" + ibckeeper "github.com/cosmos/ibc-go/v7/modules/core/keeper" + + devgasante "github.com/NibiruChain/nibiru/x/devgas/v1/ante" + devgaskeeper "github.com/NibiruChain/nibiru/x/devgas/v1/keeper" +) + +type AnteHandlerOptions struct { + sdkante.HandlerOptions + IBCKeeper *ibckeeper.Keeper + DevGasKeeper *devgaskeeper.Keeper + DevGasBankKeeper devgasante.BankKeeper + + TxCounterStoreKey types.StoreKey + WasmConfig *wasmtypes.WasmConfig + MaxTxGasWanted uint64 +} + +func (opts *AnteHandlerOptions) ValidateAndClean() error { + if opts.AccountKeeper == nil { + return AnteHandlerError("account keeper") + } + if opts.BankKeeper == nil { + return AnteHandlerError("bank keeper") + } + if opts.SignModeHandler == nil { + return AnteHandlerError("sign mode handler") + } + if opts.SigGasConsumer == nil { + opts.SigGasConsumer = sdkante.DefaultSigVerificationGasConsumer + } + if opts.WasmConfig == nil { + return AnteHandlerError("wasm config") + } + if opts.DevGasKeeper == nil { + return AnteHandlerError("devgas keeper") + } + if opts.IBCKeeper == nil { + return AnteHandlerError("ibc keeper") + } + return nil +} + +func AnteHandlerError(shortDesc string) error { + return sdkerrors.Wrapf(errors.ErrLogic, "%s is required for AnteHandler", shortDesc) +} + +type TxFeeChecker func(ctx sdk.Context, feeTx sdk.FeeTx) (sdk.Coins, int64, error) diff --git a/app/app.go b/app/app.go index 8245208cf..65dca31b5 100644 --- a/app/app.go +++ b/app/app.go @@ -11,12 +11,14 @@ import ( wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" + "github.com/NibiruChain/nibiru/app/ante" "github.com/NibiruChain/nibiru/app/wasmext" dbm "github.com/cometbft/cometbft-db" abci "github.com/cometbft/cometbft/abci/types" "github.com/cometbft/cometbft/libs/log" tmos "github.com/cometbft/cometbft/libs/os" + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/client" _ "github.com/cosmos/cosmos-sdk/client/docs/statik" @@ -31,6 +33,7 @@ import ( storetypes "github.com/cosmos/cosmos-sdk/store/types" "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/mempool" "github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/version" authante "github.com/cosmos/cosmos-sdk/x/auth/ante" @@ -39,9 +42,11 @@ import ( capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" "github.com/cosmos/cosmos-sdk/x/crisis" paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" + ibckeeper "github.com/cosmos/ibc-go/v7/modules/core/keeper" ibctesting "github.com/cosmos/ibc-go/v7/testing" "github.com/cosmos/ibc-go/v7/testing/types" + "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" "github.com/rakyll/statik/fs" @@ -120,6 +125,8 @@ func GetWasmOpts(nibiru NibiruApp, appOpts servertypes.AppOptions) []wasmkeeper. )...) } +const DefaultMaxTxGasWanted uint64 = 0 + // NewNibiruApp returns a reference to an initialized NibiruApp. func NewNibiruApp( logger log.Logger, @@ -134,6 +141,13 @@ func NewNibiruApp( legacyAmino := encodingConfig.Amino interfaceRegistry := encodingConfig.InterfaceRegistry txConfig := encodingConfig.TxConfig + baseAppOptions = append(baseAppOptions, func(app *baseapp.BaseApp) { + mp := mempool.NoOpMempool{} + app.SetMempool(mp) + handler := baseapp.NewDefaultProposalHandler(mp, app) + app.SetPrepareProposal(handler.PrepareProposalHandler()) + app.SetProcessProposal(handler.ProcessProposalHandler()) + }) bApp := baseapp.NewBaseApp( appName, logger, db, encodingConfig.TxConfig.TxDecoder(), baseAppOptions...) @@ -182,29 +196,29 @@ func NewNibiruApp( // initialize BaseApp app.SetInitChainer(app.InitChainer) app.SetBeginBlocker(app.BeginBlocker) - anteHandler, err := NewAnteHandler(AnteHandlerOptions{ + anteHandler := NewAnteHandler(app.AppKeepers, ante.AnteHandlerOptions{ HandlerOptions: authante.HandlerOptions{ - AccountKeeper: app.AccountKeeper, - BankKeeper: app.BankKeeper, - FeegrantKeeper: app.FeeGrantKeeper, - SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), - SigGasConsumer: authante.DefaultSigVerificationGasConsumer, + AccountKeeper: app.AccountKeeper, + BankKeeper: app.BankKeeper, + FeegrantKeeper: app.FeeGrantKeeper, + SignModeHandler: encodingConfig.TxConfig.SignModeHandler(), + SigGasConsumer: authante.DefaultSigVerificationGasConsumer, + ExtensionOptionChecker: func(*codectypes.Any) bool { return true }, }, IBCKeeper: app.ibcKeeper, TxCounterStoreKey: keys[wasmtypes.StoreKey], WasmConfig: &wasmConfig, DevGasKeeper: &app.DevGasKeeper, DevGasBankKeeper: app.BankKeeper, + // TODO: feat(evm): enable app/server/config flag for Evm MaxTxGasWanted. + MaxTxGasWanted: DefaultMaxTxGasWanted, }) - if err != nil { - panic(fmt.Errorf("failed to create sdk.AnteHandler: %s", err)) - } app.SetAnteHandler(anteHandler) app.SetEndBlocker(app.EndBlocker) if snapshotManager := app.SnapshotManager(); snapshotManager != nil { - if err = snapshotManager.RegisterExtensions( + if err := snapshotManager.RegisterExtensions( wasmkeeper.NewWasmSnapshotter( app.CommitMultiStore(), &app.WasmKeeper, @@ -387,7 +401,7 @@ func (app *NibiruApp) GetBaseApp() *baseapp.BaseApp { } func (app *NibiruApp) GetStakingKeeper() types.StakingKeeper { - return app.stakingKeeper + return app.StakingKeeper } func (app *NibiruApp) GetIBCKeeper() *ibckeeper.Keeper { diff --git a/app/appconst/appconst.go b/app/appconst/appconst.go index 595ba94fb..d6e850d94 100644 --- a/app/appconst/appconst.go +++ b/app/appconst/appconst.go @@ -3,6 +3,7 @@ package appconst import ( "fmt" + "math/big" "runtime" ) @@ -42,3 +43,36 @@ func RuntimeVersion() string { GoArch, ) } + +// EIP 155 Chain IDs exported for tests. +const ( + ETH_CHAIN_ID_MAINNET int64 = 420 + ETH_CHAIN_ID_LOCAL int64 = 256 + ETH_CHAIN_ID_DEVNET int64 = 500 + ETH_CHAIN_ID_DEFAULT int64 = 3000 +) + +var knownEthChainIDMap = map[string]int64{ + "cataclysm-1": ETH_CHAIN_ID_MAINNET, + "nibiru-localnet-0": ETH_CHAIN_ID_LOCAL, + "nibiru-localnet-1": ETH_CHAIN_ID_LOCAL, + "nibiru-localnet-2": ETH_CHAIN_ID_LOCAL, + "nibiru-testnet-0": ETH_CHAIN_ID_DEVNET, + "nibiru-testnet-1": ETH_CHAIN_ID_DEVNET, + "nibiru-testnet-2": ETH_CHAIN_ID_DEVNET, + "nibiru-devnet-0": ETH_CHAIN_ID_DEVNET, + "nibiru-devnet-1": ETH_CHAIN_ID_DEVNET, + "nibiru-devnet-2": ETH_CHAIN_ID_DEVNET, +} + +// GetEthChainID: Maps the given chain ID from the block's `sdk.Context` to an +// EVM Chain ID (`*big.Int`). +func GetEthChainID(ctxChainID string) (ethChainID *big.Int) { + ethChainIdInt, found := knownEthChainIDMap[ctxChainID] + if !found { + ethChainID = big.NewInt(ETH_CHAIN_ID_DEFAULT) + } else { + ethChainID = big.NewInt(ethChainIdInt) + } + return ethChainID +} diff --git a/app/appconst/appconst_test.go b/app/appconst/appconst_test.go new file mode 100644 index 000000000..ba680ce91 --- /dev/null +++ b/app/appconst/appconst_test.go @@ -0,0 +1,49 @@ +package appconst_test + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/NibiruChain/nibiru/app/appconst" +) + +type TestSuite struct { + suite.Suite +} + +func TestSuite_RunAll(t *testing.T) { + suite.Run(t, new(TestSuite)) +} + +func (s *TestSuite) TestGetEthChainID() { + s.Run("mainnet", func() { + s.EqualValues( + big.NewInt(appconst.ETH_CHAIN_ID_MAINNET), + appconst.GetEthChainID("cataclysm-1"), + ) + }) + s.Run("localnet", func() { + s.EqualValues( + big.NewInt(appconst.ETH_CHAIN_ID_LOCAL), + appconst.GetEthChainID("nibiru-localnet-0"), + ) + }) + s.Run("devnet", func() { + want := big.NewInt(appconst.ETH_CHAIN_ID_DEVNET) + given := "nibiru-testnet-1" + s.EqualValues(want, appconst.GetEthChainID(given)) + + given = "nibiru-devnet-2" + s.EqualValues(want, appconst.GetEthChainID(given)) + }) + s.Run("else", func() { + want := big.NewInt(appconst.ETH_CHAIN_ID_DEFAULT) + for _, given := range []string{ + "foo", "bloop-blap", "not a chain ID", "", "0x12345", + } { + s.EqualValues(want, appconst.GetEthChainID(given)) + } + }) +} diff --git a/app/evmante_eth.go b/app/evmante_eth.go new file mode 100644 index 000000000..3b4c722df --- /dev/null +++ b/app/evmante_eth.go @@ -0,0 +1,393 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "math" + "math/big" + + "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/keeper" + "github.com/NibiruChain/nibiru/x/evm/statedb" + + gethcommon "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" +) + +var ( + _ sdk.AnteDecorator = (*AnteDecEthGasConsume)(nil) + _ sdk.AnteDecorator = (*AnteDecVerifyEthAcc)(nil) +) + +// AnteDecVerifyEthAcc validates an account balance checks +type AnteDecVerifyEthAcc struct { + AppKeepers +} + +// NewAnteDecVerifyEthAcc creates a new EthAccountVerificationDecorator +func NewAnteDecVerifyEthAcc(k AppKeepers) AnteDecVerifyEthAcc { + return AnteDecVerifyEthAcc{ + AppKeepers: k, + } +} + +// AnteHandle validates checks that the sender balance is greater than the total transaction cost. +// The account will be set to store if it doesn't exist, i.e. cannot be found on store. +// This AnteHandler decorator will fail if: +// - any of the msgs is not a MsgEthereumTx +// - from address is empty +// - account balance is lower than the transaction cost +func (anteDec AnteDecVerifyEthAcc) AnteHandle( + ctx sdk.Context, + tx sdk.Tx, + simulate bool, + next sdk.AnteHandler, +) (newCtx sdk.Context, err error) { + if !ctx.IsCheckTx() { + return next(ctx, tx, simulate) + } + + for i, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + } + + txData, err := evm.UnpackTxData(msgEthTx.Data) + if err != nil { + return ctx, errors.Wrapf(err, "failed to unpack tx data any for tx %d", i) + } + + // sender address should be in the tx cache from the previous AnteHandle call + from := msgEthTx.GetFrom() + if from.Empty() { + return ctx, errors.Wrap(errortypes.ErrInvalidAddress, "from address cannot be empty") + } + + // check whether the sender address is EOA + fromAddr := gethcommon.BytesToAddress(from) + acct := anteDec.EvmKeeper.GetAccount(ctx, fromAddr) + + if acct == nil { + acc := anteDec.AccountKeeper.NewAccountWithAddress(ctx, from) + anteDec.AccountKeeper.SetAccount(ctx, acc) + acct = statedb.NewEmptyAccount() + } else if acct.IsContract() { + return ctx, errors.Wrapf(errortypes.ErrInvalidType, + "the sender is not EOA: address %s, codeHash <%s>", fromAddr, acct.CodeHash) + } + + if err := keeper.CheckSenderBalance(sdkmath.NewIntFromBigInt(acct.Balance), txData); err != nil { + return ctx, errors.Wrap(err, "failed to check sender balance") + } + } + return next(ctx, tx, simulate) +} + +// AnteDecEthGasConsume validates enough intrinsic gas for the transaction and +// gas consumption. +type AnteDecEthGasConsume struct { + AppKeepers + // bankKeeper anteutils.BankKeeper + // distributionKeeper anteutils.DistributionKeeper + // evmKeeper EVMKeeper + // stakingKeeper anteutils.StakingKeeper + maxGasWanted uint64 +} + +// NewAnteDecEthGasConsume creates a new EthGasConsumeDecorator +func NewAnteDecEthGasConsume( + keepers AppKeepers, + maxGasWanted uint64, +) AnteDecEthGasConsume { + return AnteDecEthGasConsume{ + AppKeepers: keepers, + maxGasWanted: maxGasWanted, + } +} + +// AnteHandle validates that the Ethereum tx message has enough to cover +// intrinsic gas (during CheckTx only) and that the sender has enough balance to +// pay for the gas cost. If the balance is not sufficient, it will be attempted +// to withdraw enough staking rewards for the payment. +// +// Intrinsic gas for a transaction is the amount of gas that the transaction uses +// before the transaction is executed. The gas is a constant value plus any cost +// incurred by additional bytes of data supplied with the transaction. +// +// This AnteHandler decorator will fail if: +// - the message is not a MsgEthereumTx +// - sender account cannot be found +// - transaction's gas limit is lower than the intrinsic gas +// - user has neither enough balance nor staking rewards to deduct the transaction fees (gas_limit * gas_price) +// - transaction or block gas meter runs out of gas +// - sets the gas meter limit +// - gas limit is greater than the block gas meter limit +func (anteDec AnteDecEthGasConsume) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, +) (sdk.Context, error) { + gasWanted := uint64(0) + if ctx.IsReCheckTx() { + // Then, the limit for gas consumed was already checked during CheckTx so + // there's no need to verify it again during ReCheckTx + // + // Use new context with gasWanted = 0 + // Otherwise, there's an error on txmempool.postCheck (tendermint) + // that is not bubbled up. Thus, the Tx never runs on DeliverMode + // Error: "gas wanted -1 is negative" + newCtx := ctx.WithGasMeter(eth.NewInfiniteGasMeterWithLimit(gasWanted)) + return next(newCtx, tx, simulate) + } + + evmParams := anteDec.EvmKeeper.GetParams(ctx) + evmDenom := evmParams.GetEvmDenom() + chainCfg := evmParams.GetChainConfig() + ethCfg := chainCfg.EthereumConfig(anteDec.EvmKeeper.EthChainID(ctx)) + + blockHeight := big.NewInt(ctx.BlockHeight()) + homestead := ethCfg.IsHomestead(blockHeight) + istanbul := ethCfg.IsIstanbul(blockHeight) + var events sdk.Events + + // Use the lowest priority of all the messages as the final one. + minPriority := int64(math.MaxInt64) + baseFee := anteDec.EvmKeeper.GetBaseFee(ctx, ethCfg) + + for _, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errors.Wrapf( + errortypes.ErrUnknownRequest, + "invalid message type %T, expected %T", + msg, (*evm.MsgEthereumTx)(nil), + ) + } + from := msgEthTx.GetFrom() + + txData, err := evm.UnpackTxData(msgEthTx.Data) + if err != nil { + return ctx, errors.Wrap(err, "failed to unpack tx data") + } + + if ctx.IsCheckTx() && anteDec.maxGasWanted != 0 { + // We can't trust the tx gas limit, because we'll refund the unused gas. + if txData.GetGas() > anteDec.maxGasWanted { + gasWanted += anteDec.maxGasWanted + } else { + gasWanted += txData.GetGas() + } + } else { + gasWanted += txData.GetGas() + } + + fees, err := keeper.VerifyFee(txData, evmDenom, baseFee, homestead, istanbul, ctx.IsCheckTx()) + if err != nil { + return ctx, errors.Wrapf(err, "failed to verify the fees") + } + + if err = anteDec.deductFee(ctx, fees, from); err != nil { + return ctx, err + } + + events = append(events, + sdk.NewEvent( + sdk.EventTypeTx, + sdk.NewAttribute(sdk.AttributeKeyFee, fees.String()), + ), + ) + + priority := evm.GetTxPriority(txData, baseFee) + + if priority < minPriority { + minPriority = priority + } + } + + ctx.EventManager().EmitEvents(events) + + blockGasLimit := eth.BlockGasLimit(ctx) + + // return error if the tx gas is greater than the block limit (max gas) + + // NOTE: it's important here to use the gas wanted instead of the gas consumed + // from the tx gas pool. The latter only has the value so far since the + // EthSetupContextDecorator, so it will never exceed the block gas limit. + if gasWanted > blockGasLimit { + return ctx, errors.Wrapf( + errortypes.ErrOutOfGas, + "tx gas (%d) exceeds block gas limit (%d)", + gasWanted, + blockGasLimit, + ) + } + + // Set tx GasMeter with a limit of GasWanted (i.e. gas limit from the Ethereum tx). + // The gas consumed will be then reset to the gas used by the state transition + // in the EVM. + + // FIXME: use a custom gas configuration that doesn't add any additional gas and only + // takes into account the gas consumed at the end of the EVM transaction. + newCtx := ctx. + WithGasMeter(eth.NewInfiniteGasMeterWithLimit(gasWanted)). + WithPriority(minPriority) + + // we know that we have enough gas on the pool to cover the intrinsic gas + return next(newCtx, tx, simulate) +} + +// deductFee checks if the fee payer has enough funds to pay for the fees and deducts them. +// If the spendable balance is not enough, it tries to claim enough staking rewards to cover the fees. +func (anteDec AnteDecEthGasConsume) deductFee(ctx sdk.Context, fees sdk.Coins, feePayer sdk.AccAddress) error { + if fees.IsZero() { + return nil + } + + // If the account balance is not sufficient, try to withdraw enough staking rewards + + if err := anteDec.EvmKeeper.DeductTxCostsFromUserBalance(ctx, fees, gethcommon.BytesToAddress(feePayer)); err != nil { + return errors.Wrapf(err, "failed to deduct transaction costs from user balance") + } + return nil +} + +// CanTransferDecorator checks if the sender is allowed to transfer funds according to the EVM block +// context rules. +type CanTransferDecorator struct { + AppKeepers +} + +// NewCanTransferDecorator creates a new CanTransferDecorator instance. +func NewCanTransferDecorator(k AppKeepers) CanTransferDecorator { + return CanTransferDecorator{ + AppKeepers: k, + } +} + +// AnteHandle creates an EVM from the message and calls the BlockContext CanTransfer function to +// see if the address can execute the transaction. +func (ctd CanTransferDecorator) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, +) (sdk.Context, error) { + params := ctd.EvmKeeper.GetParams(ctx) + ethCfg := params.ChainConfig.EthereumConfig(ctd.EvmKeeper.EthChainID(ctx)) + signer := gethcore.MakeSigner(ethCfg, big.NewInt(ctx.BlockHeight())) + + for _, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + } + + baseFee := ctd.EvmKeeper.GetBaseFee(ctx, ethCfg) + + coreMsg, err := msgEthTx.AsMessage(signer, baseFee) + if err != nil { + return ctx, errors.Wrapf( + err, + "failed to create an ethereum core.Message from signer %T", signer, + ) + } + + if evm.IsLondon(ethCfg, ctx.BlockHeight()) { + if baseFee == nil { + return ctx, errors.Wrap( + evm.ErrInvalidBaseFee, + "base fee is supported but evm block context value is nil", + ) + } + if coreMsg.GasFeeCap().Cmp(baseFee) < 0 { + return ctx, errors.Wrapf( + errortypes.ErrInsufficientFee, + "max fee per gas less than block base fee (%s < %s)", + coreMsg.GasFeeCap(), baseFee, + ) + } + } + + // NOTE: pass in an empty coinbase address and nil tracer as we don't need them for the check below + cfg := &statedb.EVMConfig{ + ChainConfig: ethCfg, + Params: params, + CoinBase: gethcommon.Address{}, + BaseFee: baseFee, + } + + stateDB := statedb.New(ctx, &ctd.EvmKeeper, statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes()))) + evm := ctd.EvmKeeper.NewEVM(ctx, coreMsg, cfg, evm.NewNoOpTracer(), stateDB) + + // check that caller has enough balance to cover asset transfer for **topmost** call + // NOTE: here the gas consumed is from the context with the infinite gas meter + if coreMsg.Value().Sign() > 0 && !evm.Context.CanTransfer(stateDB, coreMsg.From(), coreMsg.Value()) { + return ctx, errors.Wrapf( + errortypes.ErrInsufficientFunds, + "failed to transfer %s from address %s using the EVM block context transfer function", + coreMsg.Value(), + coreMsg.From(), + ) + } + } + + return next(ctx, tx, simulate) +} + +// AnteDecEthIncrementSenderSequence increments the sequence of the signers. +type AnteDecEthIncrementSenderSequence struct { + AppKeepers +} + +// NewAnteDecEthIncrementSenderSequence creates a new EthIncrementSenderSequenceDecorator. +func NewAnteDecEthIncrementSenderSequence(k AppKeepers) AnteDecEthIncrementSenderSequence { + return AnteDecEthIncrementSenderSequence{ + AppKeepers: k, + } +} + +// AnteHandle handles incrementing the sequence of the signer (i.e. sender). If the transaction is a +// contract creation, the nonce will be incremented during the transaction execution and not within +// this AnteHandler decorator. +func (issd AnteDecEthIncrementSenderSequence) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + for _, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + } + + txData, err := evm.UnpackTxData(msgEthTx.Data) + if err != nil { + return ctx, errors.Wrap(err, "failed to unpack tx data") + } + + // increase sequence of sender + acc := issd.AccountKeeper.GetAccount(ctx, msgEthTx.GetFrom()) + if acc == nil { + return ctx, errors.Wrapf( + errortypes.ErrUnknownAddress, + "account %s is nil", gethcommon.BytesToAddress(msgEthTx.GetFrom().Bytes()), + ) + } + nonce := acc.GetSequence() + + // we merged the nonce verification to nonce increment, so when tx includes multiple messages + // with same sender, they'll be accepted. + if txData.GetNonce() != nonce { + return ctx, errors.Wrapf( + errortypes.ErrInvalidSequence, + "invalid nonce; got %d, expected %d", txData.GetNonce(), nonce, + ) + } + + if err := acc.SetSequence(nonce + 1); err != nil { + return ctx, errors.Wrapf(err, "failed to set sequence to %d", acc.GetSequence()+1) + } + + issd.AccountKeeper.SetAccount(ctx, acc) + } + + return next(ctx, tx, simulate) +} diff --git a/app/evmante_fee_checker.go b/app/evmante_fee_checker.go new file mode 100644 index 000000000..1583ebb88 --- /dev/null +++ b/app/evmante_fee_checker.go @@ -0,0 +1,146 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "math" + + "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + + "github.com/NibiruChain/nibiru/app/ante" + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/x/evm" + evmkeeper "github.com/NibiruChain/nibiru/x/evm/keeper" +) + +// NewDynamicFeeChecker returns a `TxFeeChecker` that applies a dynamic fee to +// Cosmos txs using the EIP-1559 fee market logic. +// This can be called in both CheckTx and deliverTx modes. +// a) feeCap = tx.fees / tx.gas +// b) tipFeeCap = tx.MaxPriorityPrice (default) or MaxInt64 +// - when `ExtensionOptionDynamicFeeTx` is omitted, `tipFeeCap` defaults to `MaxInt64`. +// - when london hardfork is not enabled, it falls back to SDK default behavior (validator min-gas-prices). +// - Tx priority is set to `effectiveGasPrice / DefaultPriorityReduction`. +func NewDynamicFeeChecker(k evmkeeper.Keeper) ante.TxFeeChecker { + return func(ctx sdk.Context, feeTx sdk.FeeTx) (sdk.Coins, int64, error) { + // TODO: in the e2e test, if the fee in the genesis transaction meet the baseFee and minGasPrice in the feemarket, we can remove this code + if ctx.BlockHeight() == 0 { + // genesis transactions: fallback to min-gas-price logic + return checkTxFeeWithValidatorMinGasPrices(ctx, feeTx) + } + params := k.GetParams(ctx) + denom := params.EvmDenom + ethCfg := params.ChainConfig.EthereumConfig(k.EthChainID(ctx)) + + baseFee := k.GetBaseFee(ctx, ethCfg) + if baseFee == nil { + // london hardfork is not enabled: fallback to min-gas-prices logic + return checkTxFeeWithValidatorMinGasPrices(ctx, feeTx) + } + + // default to `MaxInt64` when there's no extension option. + maxPriorityPrice := sdkmath.NewInt(math.MaxInt64) + + // get the priority tip cap from the extension option. + if hasExtOptsTx, ok := feeTx.(authante.HasExtensionOptionsTx); ok { + for _, opt := range hasExtOptsTx.GetExtensionOptions() { + if extOpt, ok := opt.GetCachedValue().(*eth.ExtensionOptionDynamicFeeTx); ok { + maxPriorityPrice = extOpt.MaxPriorityPrice + break + } + } + } + + // priority fee cannot be negative + if maxPriorityPrice.IsNegative() { + return nil, 0, errors.Wrapf(errortypes.ErrInsufficientFee, "max priority price cannot be negative") + } + + gas := feeTx.GetGas() + feeCoins := feeTx.GetFee() + fee := feeCoins.AmountOfNoDenomValidation(denom) + + feeCap := fee.Quo(sdkmath.NewIntFromUint64(gas)) + baseFeeInt := sdkmath.NewIntFromBigInt(baseFee) + + if feeCap.LT(baseFeeInt) { + return nil, 0, errors.Wrapf(errortypes.ErrInsufficientFee, "gas prices too low, got: %s%s required: %s%s. Please retry using a higher gas price or a higher fee", feeCap, denom, baseFeeInt, denom) + } + + // calculate the effective gas price using the EIP-1559 logic. + effectivePrice := sdkmath.NewIntFromBigInt(evm.EffectiveGasPrice(baseFeeInt.BigInt(), feeCap.BigInt(), maxPriorityPrice.BigInt())) + + // NOTE: create a new coins slice without having to validate the denom + effectiveFee := sdk.Coins{ + { + Denom: denom, + Amount: effectivePrice.Mul(sdkmath.NewIntFromUint64(gas)), + }, + } + + bigPriority := effectivePrice.Sub(baseFeeInt).Quo(evm.DefaultPriorityReduction) + priority := int64(math.MaxInt64) + + if bigPriority.IsInt64() { + priority = bigPriority.Int64() + } + + return effectiveFee, priority, nil + } +} + +// checkTxFeeWithValidatorMinGasPrices implements the default fee logic, where the minimum price per +// unit of gas is fixed and set by each validator, and the tx priority is computed from the gas price. +func checkTxFeeWithValidatorMinGasPrices(ctx sdk.Context, tx sdk.FeeTx) (sdk.Coins, int64, error) { + feeCoins := tx.GetFee() + minGasPrices := ctx.MinGasPrices() + gas := int64(tx.GetGas()) //#nosec G701 -- checked for int overflow on ValidateBasic() + + // Ensure that the provided fees meet a minimum threshold for the validator, + // if this is a CheckTx. This is only for local mempool purposes, and thus + // is only ran on check tx. + if ctx.IsCheckTx() && !minGasPrices.IsZero() { + requiredFees := make(sdk.Coins, len(minGasPrices)) + + // Determine the required fees by multiplying each required minimum gas + // price by the gas limit, where fee = ceil(minGasPrice * gasLimit). + glDec := sdkmath.LegacyNewDec(gas) + for i, gp := range minGasPrices { + fee := gp.Amount.Mul(glDec) + requiredFees[i] = sdk.NewCoin(gp.Denom, fee.Ceil().RoundInt()) + } + + if !feeCoins.IsAnyGTE(requiredFees) { + return nil, 0, errors.Wrapf(errortypes.ErrInsufficientFee, "insufficient fees; got: %s required: %s", feeCoins, requiredFees) + } + } + + priority := getTxPriority(feeCoins, gas) + return feeCoins, priority, nil +} + +// getTxPriority returns a naive tx priority based on the amount of the smallest denomination of the gas price +// provided in a transaction. +func getTxPriority(fees sdk.Coins, gas int64) int64 { + var priority int64 + + for _, fee := range fees { + gasPrice := fee.Amount.QuoRaw(gas) + amt := gasPrice.Quo(evm.DefaultPriorityReduction) + p := int64(math.MaxInt64) + + if amt.IsInt64() { + p = amt.Int64() + } + + if priority == 0 || p < priority { + priority = p + } + } + + return priority +} diff --git a/app/evmante_fee_market.go b/app/evmante_fee_market.go new file mode 100644 index 000000000..0efc0089f --- /dev/null +++ b/app/evmante_fee_market.go @@ -0,0 +1,58 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "math/big" + + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/NibiruChain/nibiru/eth" +) + +// GasWantedDecorator keeps track of the gasWanted amount on the current block in transient store +// for BaseFee calculation. +// NOTE: This decorator does not perform any validation +type GasWantedDecorator struct { + AppKeepers +} + +// NewGasWantedDecorator creates a new NewGasWantedDecorator +func NewGasWantedDecorator( + k AppKeepers, +) GasWantedDecorator { + return GasWantedDecorator{ + AppKeepers: k, + } +} + +func (gwd GasWantedDecorator) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, +) (newCtx sdk.Context, err error) { + evmParams := gwd.EvmKeeper.GetParams(ctx) + chainCfg := evmParams.GetChainConfig() + ethCfg := chainCfg.EthereumConfig(gwd.EvmKeeper.EthChainID(ctx)) + + blockHeight := big.NewInt(ctx.BlockHeight()) + isLondon := ethCfg.IsLondon(blockHeight) + + feeTx, ok := tx.(sdk.FeeTx) + if !ok || !isLondon { + return next(ctx, tx, simulate) + } + + gasWanted := feeTx.GetGas() + // return error if the tx gas is greater than the block limit (max gas) + blockGasLimit := eth.BlockGasLimit(ctx) + if gasWanted > blockGasLimit { + return ctx, errors.Wrapf( + errortypes.ErrOutOfGas, + "tx gas (%d) exceeds block gas limit (%d)", + gasWanted, + blockGasLimit, + ) + } + + return next(ctx, tx, simulate) +} diff --git a/app/evmante_fees.go b/app/evmante_fees.go new file mode 100644 index 000000000..b0628f7bf --- /dev/null +++ b/app/evmante_fees.go @@ -0,0 +1,163 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "math/big" + + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + + gethcore "github.com/ethereum/go-ethereum/core/types" + + "github.com/NibiruChain/nibiru/x/evm" +) + +var ( + _ sdk.AnteDecorator = EthMinGasPriceDecorator{} + _ sdk.AnteDecorator = EthMempoolFeeDecorator{} +) + +// EthMinGasPriceDecorator will check if the transaction's fee is at least as large +// as the MinGasPrices param. If fee is too low, decorator returns error and tx +// is rejected. This applies to both CheckTx and DeliverTx and regardless +// if London hard fork or fee market params (EIP-1559) are enabled. +// If fee is high enough, then call next AnteHandler +type EthMinGasPriceDecorator struct { + AppKeepers +} + +// EthMempoolFeeDecorator will check if the transaction's effective fee is at +// least as large as the local validator's minimum gasFee (defined in validator +// config). +// If fee is too low, decorator returns error and tx is rejected from mempool. +// Note this only applies when ctx.CheckTx = true +// If fee is high enough or not CheckTx, then call next AnteHandler +// CONTRACT: Tx must implement FeeTx to use MempoolFeeDecorator +type EthMempoolFeeDecorator struct { + AppKeepers +} + +// NewEthMinGasPriceDecorator creates a new MinGasPriceDecorator instance used only for +// Ethereum transactions. +func NewEthMinGasPriceDecorator(k AppKeepers) EthMinGasPriceDecorator { + return EthMinGasPriceDecorator{AppKeepers: k} +} + +// NewEthMempoolFeeDecorator creates a new NewEthMempoolFeeDecorator instance used only for +// Ethereum transactions. +func NewEthMempoolFeeDecorator(k AppKeepers) EthMempoolFeeDecorator { + return EthMempoolFeeDecorator{ + AppKeepers: k, + } +} + +// AnteHandle ensures that the effective fee from the transaction is greater than the +// minimum global fee, which is defined by the MinGasPrice (parameter) * GasLimit (tx argument). +func (empd EthMinGasPriceDecorator) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, +) (newCtx sdk.Context, err error) { + minGasPrices := ctx.MinGasPrices() + evmParams := empd.EvmKeeper.GetParams(ctx) + evmDenom := evmParams.GetEvmDenom() + minGasPrice := minGasPrices.AmountOf(evmDenom) + + // short-circuit if min gas price is 0 + if minGasPrice.IsZero() { + return next(ctx, tx, simulate) + } + + chainCfg := evmParams.GetChainConfig() + ethCfg := chainCfg.EthereumConfig(empd.EvmKeeper.EthChainID(ctx)) + baseFee := empd.EvmKeeper.GetBaseFee(ctx, ethCfg) + + for _, msg := range tx.GetMsgs() { + ethMsg, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errors.Wrapf( + errortypes.ErrUnknownRequest, + "invalid message type %T, expected %T", + msg, (*evm.MsgEthereumTx)(nil), + ) + } + + feeAmt := ethMsg.GetFee() + + // For dynamic transactions, GetFee() uses the GasFeeCap value, which + // is the maximum gas price that the signer can pay. In practice, the + // signer can pay less, if the block's BaseFee is lower. So, in this case, + // we use the EffectiveFee. If the feemarket formula results in a BaseFee + // that lowers EffectivePrice until it is < MinGasPrices, the users must + // increase the GasTipCap (priority fee) until EffectivePrice > MinGasPrices. + // Transactions with MinGasPrices * gasUsed < tx fees < EffectiveFee are rejected + // by the feemarket AnteHandle + + txData, err := evm.UnpackTxData(ethMsg.Data) + if err != nil { + return ctx, errors.Wrapf(err, "failed to unpack tx data %s", ethMsg.Hash) + } + + if txData.TxType() != gethcore.LegacyTxType { + feeAmt = ethMsg.GetEffectiveFee(baseFee) + } + + gasLimit := sdk.NewDecFromBigInt(new(big.Int).SetUint64(ethMsg.GetGas())) + + requiredFee := minGasPrice.Mul(gasLimit) + fee := sdk.NewDecFromBigInt(feeAmt) + + if fee.LT(requiredFee) { + return ctx, errors.Wrapf( + errortypes.ErrInsufficientFee, + "provided fee < minimum global fee (%s < %s). Please increase the priority tip (for EIP-1559 txs) or the gas prices (for access list or legacy txs)", //nolint:lll + fee.TruncateInt().String(), requiredFee.TruncateInt().String(), + ) + } + } + + return next(ctx, tx, simulate) +} + +// AnteHandle ensures that the provided fees meet a minimum threshold for the validator. +// This check only for local mempool purposes, and thus it is only run on (Re)CheckTx. +// The logic is also skipped if the London hard fork and EIP-1559 are enabled. +func (mfd EthMempoolFeeDecorator) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, +) (newCtx sdk.Context, err error) { + if !ctx.IsCheckTx() || simulate { + return next(ctx, tx, simulate) + } + evmParams := mfd.EvmKeeper.GetParams(ctx) + chainCfg := evmParams.GetChainConfig() + ethCfg := chainCfg.EthereumConfig(mfd.EvmKeeper.EthChainID(ctx)) + + baseFee := mfd.EvmKeeper.GetBaseFee(ctx, ethCfg) + // skip check as the London hard fork and EIP-1559 are enabled + if baseFee != nil { + return next(ctx, tx, simulate) + } + + evmDenom := evmParams.GetEvmDenom() + minGasPrice := ctx.MinGasPrices().AmountOf(evmDenom) + + for _, msg := range tx.GetMsgs() { + ethMsg, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + } + + fee := sdk.NewDecFromBigInt(ethMsg.GetFee()) + gasLimit := sdk.NewDecFromBigInt(new(big.Int).SetUint64(ethMsg.GetGas())) + requiredFee := minGasPrice.Mul(gasLimit) + + if fee.LT(requiredFee) { + return ctx, errors.Wrapf( + errortypes.ErrInsufficientFee, + "insufficient fee; got: %s required: %s", + fee, requiredFee, + ) + } + } + + return next(ctx, tx, simulate) +} diff --git a/app/evmante_handler.go b/app/evmante_handler.go new file mode 100644 index 000000000..2ab2557ff --- /dev/null +++ b/app/evmante_handler.go @@ -0,0 +1,31 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/app/ante" +) + +// NewAnteHandlerEVM creates the default ante handler for Ethereum transactions +func NewAnteHandlerEVM( + k AppKeepers, options ante.AnteHandlerOptions, +) sdk.AnteHandler { + return sdk.ChainAnteDecorators( + // outermost AnteDecorator. SetUpContext must be called first + NewEthSetUpContextDecorator(k), + // Check eth effective gas price against the node's minimal-gas-prices config + NewEthMempoolFeeDecorator(k), + // Check eth effective gas price against the global MinGasPrice + NewEthMinGasPriceDecorator(k), + NewEthValidateBasicDecorator(k), + NewEthSigVerificationDecorator(k), + NewAnteDecVerifyEthAcc(k), + NewCanTransferDecorator(k), + NewAnteDecEthGasConsume(k, options.MaxTxGasWanted), + NewAnteDecEthIncrementSenderSequence(k), + NewGasWantedDecorator(k), + // emit eth tx hash and index at the very last ante handler. + NewEthEmitEventDecorator(k), + ) +} diff --git a/app/evmante_interfaces.go b/app/evmante_interfaces.go new file mode 100644 index 000000000..dc8192bfa --- /dev/null +++ b/app/evmante_interfaces.go @@ -0,0 +1,33 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/NibiruChain/nibiru/x/evm" + evmkeeper "github.com/NibiruChain/nibiru/x/evm/keeper" + "github.com/NibiruChain/nibiru/x/evm/statedb" +) + +// EVMKeeper defines the expected keeper interface used on the AnteHandler +type EVMKeeper interface { + statedb.Keeper + + NewEVM(ctx sdk.Context, msg core.Message, cfg *statedb.EVMConfig, tracer vm.EVMLogger, stateDB vm.StateDB) *vm.EVM + DeductTxCostsFromUserBalance(ctx sdk.Context, fees sdk.Coins, from common.Address) error + GetEvmGasBalance(ctx sdk.Context, addr common.Address) *big.Int + ResetTransientGasUsed(ctx sdk.Context) + GetParams(ctx sdk.Context) evm.Params + + EVMState() evmkeeper.EvmState +} + +type protoTxProvider interface { + GetProtoTx() *tx.Tx +} diff --git a/app/evmante_reject_msgs.go b/app/evmante_reject_msgs.go new file mode 100644 index 000000000..b9b1bb2ee --- /dev/null +++ b/app/evmante_reject_msgs.go @@ -0,0 +1,30 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/NibiruChain/nibiru/x/evm" +) + +// AnteDecoratorPreventEtheruemTxMsgs prevents invalid msg types from being executed +type AnteDecoratorPreventEtheruemTxMsgs struct{} + +// AnteHandle rejects messages that requires ethereum-specific authentication. +// For example `MsgEthereumTx` requires fee to be deducted in the antehandler in +// order to perform the refund. +func (rmd AnteDecoratorPreventEtheruemTxMsgs) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, +) (newCtx sdk.Context, err error) { + for _, msg := range tx.GetMsgs() { + if _, ok := msg.(*evm.MsgEthereumTx); ok { + return ctx, errors.Wrapf( + errortypes.ErrInvalidType, + "MsgEthereumTx needs to be contained within a tx with 'ExtensionOptionsEthereumTx' option", + ) + } + } + return next(ctx, tx, simulate) +} diff --git a/app/evmante_setup_ctx.go b/app/evmante_setup_ctx.go new file mode 100644 index 000000000..2c1af8d8d --- /dev/null +++ b/app/evmante_setup_ctx.go @@ -0,0 +1,197 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "errors" + "strconv" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + gethcore "github.com/ethereum/go-ethereum/core/types" + + "github.com/NibiruChain/nibiru/x/evm" +) + +// EthSetupContextDecorator is adapted from SetUpContextDecorator from cosmos-sdk, it ignores gas consumption +// by setting the gas meter to infinite +type EthSetupContextDecorator struct { + AppKeepers +} + +func NewEthSetUpContextDecorator(k AppKeepers) EthSetupContextDecorator { + return EthSetupContextDecorator{ + AppKeepers: k, + } +} + +func (esc EthSetupContextDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + // all transactions must implement GasTx + _, ok := tx.(authante.GasTx) + if !ok { + return ctx, errorsmod.Wrapf(errortypes.ErrInvalidType, "invalid transaction type %T, expected GasTx", tx) + } + + // We need to setup an empty gas config so that the gas is consistent with Ethereum. + newCtx = ctx.WithGasMeter(sdk.NewInfiniteGasMeter()). + WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}) + + // Reset transient gas used to prepare the execution of current cosmos tx. + // Transient gas-used is necessary to sum the gas-used of cosmos tx, when it contains multiple eth msgs. + esc.EvmKeeper.ResetTransientGasUsed(ctx) + return next(newCtx, tx, simulate) +} + +// EthEmitEventDecorator emit events in ante handler in case of tx execution failed (out of block gas limit). +type EthEmitEventDecorator struct { + AppKeepers +} + +// NewEthEmitEventDecorator creates a new EthEmitEventDecorator +func NewEthEmitEventDecorator(k AppKeepers) EthEmitEventDecorator { + return EthEmitEventDecorator{AppKeepers: k} +} + +// AnteHandle emits some basic events for the eth messages +func (eeed EthEmitEventDecorator) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, +) (newCtx sdk.Context, err error) { + // After eth tx passed ante handler, the fee is deducted and nonce increased, + // it shouldn't be ignored by json-rpc. We need to emit some events at the + // very end of ante handler to be indexed by the consensus engine. + txIndex := eeed.EvmKeeper.EVMState().BlockTxIndex.GetOr(ctx, 0) + + for i, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + } + + // emit ethereum tx hash as an event so that it can be indexed by + // Tendermint for query purposes it's emitted in ante handler, so we can + // query failed transaction (out of block gas limit). + ctx.EventManager().EmitEvent(sdk.NewEvent( + evm.EventTypeEthereumTx, + sdk.NewAttribute(evm.AttributeKeyEthereumTxHash, msgEthTx.Hash), + sdk.NewAttribute(evm.AttributeKeyTxIndex, strconv.FormatUint(txIndex+uint64(i), 10)), // #nosec G701 + )) + } + + return next(ctx, tx, simulate) +} + +// EthValidateBasicDecorator is adapted from ValidateBasicDecorator from cosmos-sdk, it ignores ErrNoSignatures +type EthValidateBasicDecorator struct { + AppKeepers +} + +// NewEthValidateBasicDecorator creates a new EthValidateBasicDecorator +func NewEthValidateBasicDecorator(k AppKeepers) EthValidateBasicDecorator { + return EthValidateBasicDecorator{ + AppKeepers: k, + } +} + +// AnteHandle handles basic validation of tx +func (vbd EthValidateBasicDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (sdk.Context, error) { + // no need to validate basic on recheck tx, call next antehandler + if ctx.IsReCheckTx() { + return next(ctx, tx, simulate) + } + + err := tx.ValidateBasic() + // ErrNoSignatures is fine with eth tx + if err != nil && !errors.Is(err, errortypes.ErrNoSignatures) { + return ctx, errorsmod.Wrap(err, "tx basic validation failed") + } + + // For eth type cosmos tx, some fields should be verified as zero values, + // since we will only verify the signature against the hash of the MsgEthereumTx.Data + wrapperTx, ok := tx.(protoTxProvider) + if !ok { + return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid tx type %T, didn't implement interface protoTxProvider", tx) + } + + protoTx := wrapperTx.GetProtoTx() + body := protoTx.Body + if body.Memo != "" || body.TimeoutHeight != uint64(0) || len(body.NonCriticalExtensionOptions) > 0 { + return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, + "for eth tx body Memo TimeoutHeight NonCriticalExtensionOptions should be empty") + } + + if len(body.ExtensionOptions) != 1 { + return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx length of ExtensionOptions should be 1") + } + + authInfo := protoTx.AuthInfo + if len(authInfo.SignerInfos) > 0 { + return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx AuthInfo SignerInfos should be empty") + } + + if authInfo.Fee.Payer != "" || authInfo.Fee.Granter != "" { + return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx AuthInfo Fee payer and granter should be empty") + } + + sigs := protoTx.Signatures + if len(sigs) > 0 { + return ctx, errorsmod.Wrap(errortypes.ErrInvalidRequest, "for eth tx Signatures should be empty") + } + + txFee := sdk.Coins{} + txGasLimit := uint64(0) + + evmParams := vbd.EvmKeeper.GetParams(ctx) + chainCfg := evmParams.GetChainConfig() + chainID := vbd.EvmKeeper.EthChainID(ctx) + ethCfg := chainCfg.EthereumConfig(chainID) + baseFee := vbd.EvmKeeper.GetBaseFee(ctx, ethCfg) + enableCreate := evmParams.GetEnableCreate() + enableCall := evmParams.GetEnableCall() + evmDenom := evmParams.GetEvmDenom() + + for _, msg := range protoTx.GetMsgs() { + msgEthTx, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errorsmod.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + } + + // Validate `From` field + if msgEthTx.From != "" { + return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid From %s, expect empty string", msgEthTx.From) + } + + txGasLimit += msgEthTx.GetGas() + + txData, err := evm.UnpackTxData(msgEthTx.Data) + if err != nil { + return ctx, errorsmod.Wrap(err, "failed to unpack MsgEthereumTx Data") + } + + // return error if contract creation or call are disabled through governance + if !enableCreate && txData.GetTo() == nil { + return ctx, errorsmod.Wrap(evm.ErrCreateDisabled, "failed to create new contract") + } else if !enableCall && txData.GetTo() != nil { + return ctx, errorsmod.Wrap(evm.ErrCallDisabled, "failed to call contract") + } + + if baseFee == nil && txData.TxType() == gethcore.DynamicFeeTxType { + return ctx, errorsmod.Wrap(gethcore.ErrTxTypeNotSupported, "dynamic fee tx not supported") + } + + txFee = txFee.Add(sdk.Coin{Denom: evmDenom, Amount: sdkmath.NewIntFromBigInt(txData.Fee())}) + } + + if !authInfo.Fee.Amount.IsEqual(txFee) { + return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid AuthInfo Fee Amount (%s != %s)", authInfo.Fee.Amount, txFee) + } + + if authInfo.Fee.GasLimit != txGasLimit { + return ctx, errorsmod.Wrapf(errortypes.ErrInvalidRequest, "invalid AuthInfo Fee GasLimit (%d != %d)", authInfo.Fee.GasLimit, txGasLimit) + } + + return next(ctx, tx, simulate) +} diff --git a/app/evmante_sigverify.go b/app/evmante_sigverify.go new file mode 100644 index 000000000..25d0fb2bc --- /dev/null +++ b/app/evmante_sigverify.go @@ -0,0 +1,70 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package app + +import ( + "math/big" + + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + gethcore "github.com/ethereum/go-ethereum/core/types" + + "github.com/NibiruChain/nibiru/x/evm" +) + +// EthSigVerificationDecorator validates an ethereum signatures +type EthSigVerificationDecorator struct { + AppKeepers +} + +// NewEthSigVerificationDecorator creates a new EthSigVerificationDecorator +func NewEthSigVerificationDecorator(k AppKeepers) EthSigVerificationDecorator { + return EthSigVerificationDecorator{ + AppKeepers: k, + } +} + +// AnteHandle validates checks that the registered chain id is the same as the one on the message, and +// that the signer address matches the one defined on the message. +// It's not skipped for RecheckTx, because it set `From` address which is critical from other ante handler to work. +// Failure in RecheckTx will prevent tx to be included into block, especially when CheckTx succeed, in which case user +// won't see the error message. +func (esvd EthSigVerificationDecorator) AnteHandle( + ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler, +) (newCtx sdk.Context, err error) { + chainID := esvd.EvmKeeper.EthChainID(ctx) + evmParams := esvd.EvmKeeper.GetParams(ctx) + chainCfg := evmParams.GetChainConfig() + ethCfg := chainCfg.EthereumConfig(chainID) + blockNum := big.NewInt(ctx.BlockHeight()) + signer := gethcore.MakeSigner(ethCfg, blockNum) + + for _, msg := range tx.GetMsgs() { + msgEthTx, ok := msg.(*evm.MsgEthereumTx) + if !ok { + return ctx, errors.Wrapf(errortypes.ErrUnknownRequest, "invalid message type %T, expected %T", msg, (*evm.MsgEthereumTx)(nil)) + } + + allowUnprotectedTxs := evmParams.GetAllowUnprotectedTxs() + ethTx := msgEthTx.AsTransaction() + if !allowUnprotectedTxs && !ethTx.Protected() { + return ctx, errors.Wrapf( + errortypes.ErrNotSupported, + "rejected unprotected Ethereum transaction. Please EIP155 sign your transaction to protect it against replay-attacks") + } + + sender, err := signer.Sender(ethTx) + if err != nil { + return ctx, errors.Wrapf( + errortypes.ErrorInvalidSigner, + "couldn't retrieve sender address from the ethereum transaction: %s", + err.Error(), + ) + } + + // set up the sender to the transaction field if not already + msgEthTx.From = sender.Hex() + } + + return next(ctx, tx, simulate) +} diff --git a/app/evmante_test.go b/app/evmante_test.go new file mode 100644 index 000000000..ef5306116 --- /dev/null +++ b/app/evmante_test.go @@ -0,0 +1,191 @@ +package app_test + +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" + + gethparams "github.com/ethereum/go-ethereum/params" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/evmtest" + + "github.com/NibiruChain/nibiru/x/evm/statedb" +) + +var NextNoOpAnteHandler sdk.AnteHandler = func( + ctx sdk.Context, tx sdk.Tx, simulate bool, +) (newCtx sdk.Context, err error) { + return ctx, nil +} + +func (s *TestSuite) TestAnteDecoratorVerifyEthAcc_CheckTx() { + testCases := []struct { + name string + beforeTxSetup func(deps *evmtest.TestDeps, sdb *statedb.StateDB) + txSetup func(deps *evmtest.TestDeps) *evm.MsgEthereumTx + wantErr string + }{ + { + name: "happy: sender with funds", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + sdb.AddBalance(deps.Sender.EthAddr, happyGasLimit()) + }, + txSetup: happyCreateContractTx, + wantErr: "", + }, + { + name: "sad: sender has insufficient gas balance", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) {}, + txSetup: happyCreateContractTx, + wantErr: "sender balance < tx cost", + }, + { + name: "sad: sender cannot be a contract -> no contract bytecode", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + // Force account to be a smart contract + sdb.SetCode(deps.Sender.EthAddr, []byte("evm bytecode stuff")) + }, + txSetup: happyCreateContractTx, + wantErr: "sender is not EOA", + }, + { + name: "sad: invalid tx", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) {}, + txSetup: func(deps *evmtest.TestDeps) *evm.MsgEthereumTx { + return new(evm.MsgEthereumTx) + }, + wantErr: "failed to unpack tx data", + }, + { + name: "sad: empty from addr", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) {}, + txSetup: func(deps *evmtest.TestDeps) *evm.MsgEthereumTx { + tx := happyCreateContractTx(deps) + tx.From = "" + return tx + }, + wantErr: "from address cannot be empty", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + stateDB := deps.StateDB() + anteDec := app.NewAnteDecVerifyEthAcc(deps.Chain.AppKeepers) + + tc.beforeTxSetup(&deps, stateDB) + tx := tc.txSetup(&deps) + s.Require().NoError(stateDB.Commit()) + + deps.Ctx = deps.Ctx.WithIsCheckTx(true) + _, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) + } +} + +func happyGasLimit() *big.Int { + return new(big.Int).SetUint64( + gethparams.TxGasContractCreation + 888, + // 888 is a cushion to account for KV store reads and writes + ) +} + +func gasLimitCreateContract() *big.Int { + return new(big.Int).SetUint64( + gethparams.TxGasContractCreation + 700, + ) +} + +func happyCreateContractTx(deps *evmtest.TestDeps) *evm.MsgEthereumTx { + ethContractCreationTxParams := &evm.EvmTxArgs{ + ChainID: deps.Chain.EvmKeeper.EthChainID(deps.Ctx), + Nonce: 1, + Amount: big.NewInt(10), + GasLimit: gasLimitCreateContract().Uint64(), + GasPrice: big.NewInt(1), + } + tx := evm.NewTx(ethContractCreationTxParams) + tx.From = deps.Sender.EthAddr.Hex() + return tx +} + +func (s *TestSuite) TestAnteDecEthGasConsume() { + testCases := []struct { + name string + beforeTxSetup func(deps *evmtest.TestDeps, sdb *statedb.StateDB) + txSetup func(deps *evmtest.TestDeps) *evm.MsgEthereumTx + wantErr string + maxGasWanted uint64 + gasMeter sdk.GasMeter + }{ + { + name: "happy: sender with funds", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + gasLimit := happyGasLimit() + balance := new(big.Int).Add(gasLimit, big.NewInt(100)) + sdb.AddBalance(deps.Sender.EthAddr, balance) + }, + txSetup: happyCreateContractTx, + wantErr: "", + gasMeter: eth.NewInfiniteGasMeterWithLimit(happyGasLimit().Uint64()), + maxGasWanted: 0, + }, + { + name: "happy: is recheck tx", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + deps.Ctx = deps.Ctx.WithIsReCheckTx(true) + }, + txSetup: happyCreateContractTx, + gasMeter: eth.NewInfiniteGasMeterWithLimit(0), + wantErr: "", + }, + { + name: "sad: out of gas", + beforeTxSetup: func(deps *evmtest.TestDeps, sdb *statedb.StateDB) { + gasLimit := happyGasLimit() + balance := new(big.Int).Add(gasLimit, big.NewInt(100)) + sdb.AddBalance(deps.Sender.EthAddr, balance) + }, + txSetup: happyCreateContractTx, + wantErr: "exceeds block gas limit (0)", + gasMeter: eth.NewInfiniteGasMeterWithLimit(0), + maxGasWanted: 0, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + stateDB := deps.StateDB() + anteDec := app.NewAnteDecEthGasConsume( + deps.Chain.AppKeepers, tc.maxGasWanted, + ) + + tc.beforeTxSetup(&deps, stateDB) + tx := tc.txSetup(&deps) + s.Require().NoError(stateDB.Commit()) + + deps.Ctx = deps.Ctx.WithIsCheckTx(true) + deps.Ctx = deps.Ctx.WithBlockGasMeter(tc.gasMeter) + _, err := anteDec.AnteHandle( + deps.Ctx, tx, false, NextNoOpAnteHandler, + ) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) + } +} diff --git a/app/export.go b/app/export.go index 7a9a5ed6a..34a8e36ff 100644 --- a/app/export.go +++ b/app/export.go @@ -35,7 +35,7 @@ func (app *NibiruApp) ExportAppStateAndValidators( return servertypes.ExportedApp{}, err } - validators, err := staking.WriteValidators(ctx, app.stakingKeeper) + validators, err := staking.WriteValidators(ctx, app.StakingKeeper) return servertypes.ExportedApp{ AppState: appState, Validators: validators, @@ -72,13 +72,13 @@ func (app *NibiruApp) prepForZeroHeightGenesis(ctx sdk.Context, jailAllowedAddrs /* Handle fee distribution state. */ // withdraw all validator commission - app.stakingKeeper.IterateValidators(ctx, func(_ int64, val stakingtypes.ValidatorI) (stop bool) { + app.StakingKeeper.IterateValidators(ctx, func(_ int64, val stakingtypes.ValidatorI) (stop bool) { _, _ = app.DistrKeeper.WithdrawValidatorCommission(ctx, val.GetOperator()) return false }) // withdraw all delegator rewards - dels := app.stakingKeeper.GetAllDelegations(ctx) + dels := app.StakingKeeper.GetAllDelegations(ctx) for _, delegation := range dels { valAddr, err := sdk.ValAddressFromBech32(delegation.ValidatorAddress) if err != nil { @@ -103,7 +103,7 @@ func (app *NibiruApp) prepForZeroHeightGenesis(ctx sdk.Context, jailAllowedAddrs ctx = ctx.WithBlockHeight(0) // reinitialize all validators - app.stakingKeeper.IterateValidators(ctx, func(_ int64, val stakingtypes.ValidatorI) (stop bool) { + app.StakingKeeper.IterateValidators(ctx, func(_ int64, val stakingtypes.ValidatorI) (stop bool) { // donate any unwithdrawn outstanding reward fraction tokens to the community pool scraps := app.DistrKeeper.GetValidatorOutstandingRewardsCoins(ctx, val.GetOperator()) feePool := app.DistrKeeper.GetFeePool(ctx) @@ -143,20 +143,20 @@ func (app *NibiruApp) prepForZeroHeightGenesis(ctx sdk.Context, jailAllowedAddrs /* Handle staking state. */ // iterate through redelegations, reset creation height - app.stakingKeeper.IterateRedelegations(ctx, func(_ int64, red stakingtypes.Redelegation) (stop bool) { + app.StakingKeeper.IterateRedelegations(ctx, func(_ int64, red stakingtypes.Redelegation) (stop bool) { for i := range red.Entries { red.Entries[i].CreationHeight = 0 } - app.stakingKeeper.SetRedelegation(ctx, red) + app.StakingKeeper.SetRedelegation(ctx, red) return false }) // iterate through unbonding delegations, reset creation height - app.stakingKeeper.IterateUnbondingDelegations(ctx, func(_ int64, ubd stakingtypes.UnbondingDelegation) (stop bool) { + app.StakingKeeper.IterateUnbondingDelegations(ctx, func(_ int64, ubd stakingtypes.UnbondingDelegation) (stop bool) { for i := range ubd.Entries { ubd.Entries[i].CreationHeight = 0 } - app.stakingKeeper.SetUnbondingDelegation(ctx, ubd) + app.StakingKeeper.SetUnbondingDelegation(ctx, ubd) return false }) @@ -168,7 +168,7 @@ func (app *NibiruApp) prepForZeroHeightGenesis(ctx sdk.Context, jailAllowedAddrs for ; iter.Valid(); iter.Next() { addr := sdk.ValAddress(stakingtypes.AddressFromValidatorsKey(iter.Key())) - validator, found := app.stakingKeeper.GetValidator(ctx, addr) + validator, found := app.StakingKeeper.GetValidator(ctx, addr) if !found { panic("expected validator, not found") } @@ -178,13 +178,13 @@ func (app *NibiruApp) prepForZeroHeightGenesis(ctx sdk.Context, jailAllowedAddrs validator.Jailed = true } - app.stakingKeeper.SetValidator(ctx, validator) + app.StakingKeeper.SetValidator(ctx, validator) counter++ } iter.Close() - _, err := app.stakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx) + _, err := app.StakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx) if err != nil { log.Fatal(err) } diff --git a/app/keepers.go b/app/keepers.go index e5ed3f5c6..cb073a336 100644 --- a/app/keepers.go +++ b/app/keepers.go @@ -139,7 +139,7 @@ type AppKeepers struct { // BankKeeper defines a module interface that facilitates the transfer of coins between accounts BankKeeper bankkeeper.Keeper capabilityKeeper *capabilitykeeper.Keeper - stakingKeeper *stakingkeeper.Keeper + StakingKeeper *stakingkeeper.Keeper slashingKeeper slashingkeeper.Keeper /* DistrKeeper is the keeper of the distribution store */ DistrKeeper distrkeeper.Keeper @@ -233,7 +233,7 @@ func initStoreKeys() ( evm.StoreKey, ) - tkeys = sdk.NewTransientStoreKeys(paramstypes.TStoreKey) + tkeys = sdk.NewTransientStoreKeys(paramstypes.TStoreKey, evm.TransientKey) memKeys = sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey) return keys, tkeys, memKeys } @@ -298,7 +298,7 @@ func (app *NibiruApp) InitKeepers( BlockedAddresses(), govModuleAddr, ) - app.stakingKeeper = stakingkeeper.NewKeeper( + app.StakingKeeper = stakingkeeper.NewKeeper( appCodec, keys[stakingtypes.StoreKey], app.AccountKeeper, @@ -310,7 +310,7 @@ func (app *NibiruApp) InitKeepers( keys[distrtypes.StoreKey], app.AccountKeeper, app.BankKeeper, - app.stakingKeeper, + app.StakingKeeper, authtypes.FeeCollectorName, govModuleAddr, ) @@ -350,11 +350,11 @@ func (app *NibiruApp) InitKeepers( appCodec, legacyAmino, keys[slashingtypes.StoreKey], - app.stakingKeeper, + app.StakingKeeper, govModuleAddr, ) - app.stakingKeeper.SetHooks( + app.StakingKeeper.SetHooks( stakingtypes.NewMultiStakingHooks(app.DistrKeeper.Hooks(), app.slashingKeeper.Hooks()), ) @@ -372,7 +372,7 @@ func (app *NibiruApp) InitKeepers( ) app.OracleKeeper = oraclekeeper.NewKeeper(appCodec, keys[oracletypes.StoreKey], - app.AccountKeeper, app.BankKeeper, app.DistrKeeper, app.stakingKeeper, app.slashingKeeper, + app.AccountKeeper, app.BankKeeper, app.DistrKeeper, app.StakingKeeper, app.slashingKeeper, app.SudoKeeper, distrtypes.ModuleName, ) @@ -383,7 +383,7 @@ func (app *NibiruApp) InitKeepers( app.InflationKeeper = inflationkeeper.NewKeeper( appCodec, keys[inflationtypes.StoreKey], app.GetSubspace(inflationtypes.ModuleName), - app.AccountKeeper, app.BankKeeper, app.DistrKeeper, app.stakingKeeper, app.SudoKeeper, authtypes.FeeCollectorName, + app.AccountKeeper, app.BankKeeper, app.DistrKeeper, app.StakingKeeper, app.SudoKeeper, authtypes.FeeCollectorName, ) app.EpochsKeeper.SetHooks( @@ -393,6 +393,7 @@ func (app *NibiruApp) InitKeepers( ), ) + evmTracer := "json" app.EvmKeeper = evmkeeper.NewKeeper( appCodec, keys[evm.StoreKey], @@ -400,6 +401,8 @@ func (app *NibiruApp) InitKeepers( authtypes.NewModuleAddress(govtypes.ModuleName), app.AccountKeeper, app.BankKeeper, + app.StakingKeeper, + evmTracer, ) // ---------------------------------- IBC keepers @@ -408,7 +411,7 @@ func (app *NibiruApp) InitKeepers( appCodec, keys[ibcexported.StoreKey], app.GetSubspace(ibcexported.ModuleName), - app.stakingKeeper, + app.StakingKeeper, app.upgradeKeeper, app.ScopedIBCKeeper, ) @@ -479,7 +482,7 @@ func (app *NibiruApp) InitKeepers( keys[wasmtypes.StoreKey], app.AccountKeeper, app.BankKeeper, - app.stakingKeeper, + app.StakingKeeper, distrkeeper.NewQuerier(app.DistrKeeper), app.ibcFeeKeeper, // ISC4 Wrapper: fee IBC middleware app.ibcKeeper.ChannelKeeper, @@ -520,7 +523,7 @@ func (app *NibiruApp) InitKeepers( // Create evidence keeper. // This keeper automatically includes an evidence router. app.evidenceKeeper = *evidencekeeper.NewKeeper( - appCodec, keys[evidencetypes.StoreKey], app.stakingKeeper, + appCodec, keys[evidencetypes.StoreKey], app.StakingKeeper, app.slashingKeeper, ) @@ -599,7 +602,7 @@ func (app *NibiruApp) InitKeepers( keys[govtypes.StoreKey], app.AccountKeeper, app.BankKeeper, - app.stakingKeeper, + app.StakingKeeper, app.MsgServiceRouter(), govConfig, govModuleAddr, @@ -622,7 +625,7 @@ func (app *NibiruApp) initAppModules( return []module.AppModule{ // core modules genutil.NewAppModule( - app.AccountKeeper, app.stakingKeeper, app.BaseApp.DeliverTx, + app.AccountKeeper, app.StakingKeeper, app.BaseApp.DeliverTx, encodingConfig.TxConfig, ), auth.NewAppModule(appCodec, app.AccountKeeper, authsims.RandomGenesisAccounts, app.GetSubspace(authtypes.ModuleName)), @@ -631,9 +634,9 @@ func (app *NibiruApp) initAppModules( capability.NewAppModule(appCodec, *app.capabilityKeeper, false), feegrantmodule.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry), gov.NewAppModule(appCodec, &app.GovKeeper, app.AccountKeeper, app.BankKeeper, app.GetSubspace(govtypes.ModuleName)), - slashing.NewAppModule(appCodec, app.slashingKeeper, app.AccountKeeper, app.BankKeeper, app.stakingKeeper, app.GetSubspace(slashingtypes.ModuleName)), - distr.NewAppModule(appCodec, app.DistrKeeper, app.AccountKeeper, app.BankKeeper, app.stakingKeeper, app.GetSubspace(distrtypes.ModuleName)), - staking.NewAppModule(appCodec, app.stakingKeeper, app.AccountKeeper, app.BankKeeper, app.GetSubspace(stakingtypes.ModuleName)), + slashing.NewAppModule(appCodec, app.slashingKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper, app.GetSubspace(slashingtypes.ModuleName)), + distr.NewAppModule(appCodec, app.DistrKeeper, app.AccountKeeper, app.BankKeeper, app.StakingKeeper, app.GetSubspace(distrtypes.ModuleName)), + staking.NewAppModule(appCodec, app.StakingKeeper, app.AccountKeeper, app.BankKeeper, app.GetSubspace(stakingtypes.ModuleName)), upgrade.NewAppModule(&app.upgradeKeeper), params.NewAppModule(app.paramsKeeper), authzmodule.NewAppModule(appCodec, app.authzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), @@ -641,7 +644,7 @@ func (app *NibiruApp) initAppModules( // Nibiru modules oracle.NewAppModule(appCodec, app.OracleKeeper, app.AccountKeeper, app.BankKeeper), epochs.NewAppModule(appCodec, app.EpochsKeeper), - inflation.NewAppModule(app.InflationKeeper, app.AccountKeeper, *app.stakingKeeper), + inflation.NewAppModule(app.InflationKeeper, app.AccountKeeper, *app.StakingKeeper), sudo.NewAppModule(appCodec, app.SudoKeeper), genmsg.NewAppModule(app.MsgServiceRouter()), @@ -656,7 +659,7 @@ func (app *NibiruApp) initAppModules( // wasm wasm.NewAppModule( - appCodec, &app.WasmKeeper, app.stakingKeeper, app.AccountKeeper, + appCodec, &app.WasmKeeper, app.StakingKeeper, app.AccountKeeper, app.BankKeeper, app.MsgServiceRouter(), app.GetSubspace(wasmtypes.ModuleName)), devgas.NewAppModule( diff --git a/app/server/config/server_config.go b/app/server/config/server_config.go index 18ad43d93..5b34d7a77 100644 --- a/app/server/config/server_config.go +++ b/app/server/config/server_config.go @@ -242,7 +242,7 @@ func GetAPINamespaces() []string { // DefaultJSONRPCConfig returns an EVM config with the JSON-RPC API enabled by default func DefaultJSONRPCConfig() *JSONRPCConfig { return &JSONRPCConfig{ - Enable: true, + Enable: false, API: GetDefaultAPINamespaces(), Address: DefaultJSONRPCAddress, WsAddress: DefaultJSONRPCWsAddress, diff --git a/app/server/start.go b/app/server/start.go index 0447b85c7..0eb9d689e 100644 --- a/app/server/start.go +++ b/app/server/start.go @@ -9,9 +9,7 @@ import ( "os" "os/signal" "path/filepath" - "regexp" "runtime/pprof" - "strings" "syscall" "time" @@ -403,7 +401,7 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt clientCtx = clientCtx. WithHomeDir(home). - WithChainID(hackChainID(genDoc.ChainID)) + WithChainID(genDoc.ChainID) // Set `GRPCClient` to `clientCtx` to enjoy concurrent grpc query. // only use it if gRPC server is enabled. @@ -506,7 +504,7 @@ func startInProcess(ctx *server.Context, clientCtx client.Context, opts StartOpt return err } - clientCtx := clientCtx.WithChainID(hackChainID(genDoc.ChainID)) + clientCtx := clientCtx.WithChainID(genDoc.ChainID) tmEndpoint := "/websocket" tmRPCAddr := cfg.RPC.ListenAddress @@ -659,17 +657,3 @@ func wrapCPUProfile(ctx *server.Context, callback func() error) error { return WaitForQuitSignals() } - -// hackChainID replaces nibiru-localnet-0 with nibirulocalnet-9000-1 which matches the standard -func hackChainID(chainID string) string { - re := regexp.MustCompile(`-\d+$`) - lastNumber := re.FindString(chainID) - trimmedInput := strings.TrimSuffix(chainID, lastNumber) - if lastNumber == "-0" { - lastNumber = "-1" - } - trimmedInput = strings.ReplaceAll(trimmedInput, "-", "") - result := trimmedInput + "_9000" + lastNumber - - return result -} diff --git a/contrib/scripts/localnet.sh b/contrib/scripts/localnet.sh index 52e2b3924..df2c86e4f 100755 --- a/contrib/scripts/localnet.sh +++ b/contrib/scripts/localnet.sh @@ -149,6 +149,10 @@ $BINARY config # Prints config. echo_info "config/app.toml: Enabling API server" sed -i $SEDOPTION '/\[api\]/,+3 s/enable = false/enable = true/' $CHAIN_DIR/config/app.toml +# Enable JSON RPC Server +echo_info "config/app.toml: Enabling JSON API server" +sed -i $SEDOPTION '/\[json\-rpc\]/,+3 s/enable = false/enable = true/' $CHAIN_DIR/config/app.toml + # Enable Swagger Docs echo_info "config/app.toml: Enabling Swagger Docs" sed -i $SEDOPTION 's/swagger = false/swagger = true/' $CHAIN_DIR/config/app.toml @@ -163,6 +167,8 @@ val_key_name="validator" echo "$MNEMONIC" | $BINARY keys add $val_key_name --recover $BINARY add-genesis-account $($BINARY keys show $val_key_name -a) $GENESIS_COINS +# EVM encrypted nibi address for the same account +$BINARY add-genesis-account nibi1cr6tg4cjvux00pj6zjqkh6d0jzg7mksaywxyl3 $GENESIS_COINS echo_success "Successfully added genesis account: $val_key_name" val_address=$($BINARY keys list | jq -r '.[] | select(.name == "validator") | .address') diff --git a/e2e/evm/.env_sample b/e2e/evm/.env_sample new file mode 100644 index 000000000..98bebd6b2 --- /dev/null +++ b/e2e/evm/.env_sample @@ -0,0 +1,2 @@ +JSON_RPC_ENDPOINT="http://127.0.0.1:8545" +MNEMONIC="guard cream sadness conduct invite crumble clock pudding hole grit liar hotel maid produce squeeze return argue turtle know drive eight casino maze host" diff --git a/e2e/evm/README.md b/e2e/evm/README.md new file mode 100644 index 000000000..4375ea399 --- /dev/null +++ b/e2e/evm/README.md @@ -0,0 +1,54 @@ +# EVM Tests + +Folder contains ethers.js test bundle which executes main +Nibiru EVM methods via JSON RPC. + +Contract [FunToken.sol](./contracts/FunToken.sol) represents +simple ERC20 token with initial supply `1000,000 * 10e18` tokens. + +Contract is compiled via HardHat into [json file](./contracts/FunTokenCompiled.json) +with ABI and bytecode. + + +## Setup and Run + +### Run Nibiru node + +Tests require Nibiru node running with JSON RPC enabled. + +Localnet has JSON RPC enabled by default. + +### Install dependencies + +```bash +npm install +``` + +### Configure environment in `.env` file + +Use [env.sample](./.env_sample) as a reference. + +```ini +JSON_RPC_ENDPOINT="http://127.0.0.1:8545" +MNEMONIC="guard cream sadness conduct invite crumble clock pudding hole grit liar hotel maid produce squeeze return argue turtle know drive eight casino maze host" +``` + +### Execute + +```bash +npm test + +> nibiru-evm-test@0.0.1 test +> jest + + PASS test/evm.test.js (13.163 s) + Ethereum JSON-RPC Interface Tests + ✓ Simple Transfer, balance check (4258 ms) + ✓ Smart Contract (8656 ms) + +Test Suites: 1 passed, 1 total +Tests: 2 passed, 2 total +Snapshots: 0 total +Time: 13.187 s, estimated 14 s +Ran all test suites. +``` diff --git a/e2e/evm/contracts/FunToken.sol b/e2e/evm/contracts/FunToken.sol new file mode 100644 index 000000000..008a2723b --- /dev/null +++ b/e2e/evm/contracts/FunToken.sol @@ -0,0 +1,16 @@ +// contracts/FunToken.sol +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract FunToken is ERC20 { + + // Define the supply of FunToken: 1,000,000 + uint256 constant initialSupply = 1000000 * (10**18); + + // Constructor will be called on contract creation + constructor() ERC20("FunToken", "FUN") { + _mint(msg.sender, initialSupply); + } +} diff --git a/e2e/evm/contracts/FunTokenCompiled.json b/e2e/evm/contracts/FunTokenCompiled.json new file mode 100644 index 000000000..f7aaeb730 --- /dev/null +++ b/e2e/evm/contracts/FunTokenCompiled.json @@ -0,0 +1,324 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "FunToken", + "sourceName": "contracts/FunToken.sol", + "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x60806040523480156200001157600080fd5b506040518060400160405280600881526020017f46756e546f6b656e0000000000000000000000000000000000000000000000008152506040518060400160405280600381526020017f46554e000000000000000000000000000000000000000000000000000000000081525081600390816200008f9190620005fd565b508060049081620000a19190620005fd565b505050620000c03369d3c21bcecceda1000000620000c660201b60201c565b6200081b565b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16036200013b5760006040517fec442f0500000000000000000000000000000000000000000000000000000000815260040162000132919062000729565b60405180910390fd5b6200014f600083836200015360201b60201c565b5050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff1603620001a95780600260008282546200019c919062000775565b925050819055506200027f565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490508181101562000238578381836040517fe450d38c0000000000000000000000000000000000000000000000000000000081526004016200022f93929190620007c1565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1603620002ca578060026000828254039250508190555062000317565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051620003769190620007fe565b60405180910390a3505050565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200040557607f821691505b6020821081036200041b576200041a620003bd565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620004857fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000446565b62000491868362000446565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b6000620004de620004d8620004d284620004a9565b620004b3565b620004a9565b9050919050565b6000819050919050565b620004fa83620004bd565b620005126200050982620004e5565b84845462000453565b825550505050565b600090565b620005296200051a565b62000536818484620004ef565b505050565b5b818110156200055e57620005526000826200051f565b6001810190506200053c565b5050565b601f821115620005ad57620005778162000421565b620005828462000436565b8101602085101562000592578190505b620005aa620005a18562000436565b8301826200053b565b50505b505050565b600082821c905092915050565b6000620005d260001984600802620005b2565b1980831691505092915050565b6000620005ed8383620005bf565b9150826002028217905092915050565b620006088262000383565b67ffffffffffffffff8111156200062457620006236200038e565b5b620006308254620003ec565b6200063d82828562000562565b600060209050601f83116001811462000675576000841562000660578287015190505b6200066c8582620005df565b865550620006dc565b601f198416620006858662000421565b60005b82811015620006af5784890151825560018201915060208501945060208101905062000688565b86831015620006cf5784890151620006cb601f891682620005bf565b8355505b6001600288020188555050505b505050505050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006200071182620006e4565b9050919050565b620007238162000704565b82525050565b600060208201905062000740600083018462000718565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006200078282620004a9565b91506200078f83620004a9565b9250828201905080821115620007aa57620007a962000746565b5b92915050565b620007bb81620004a9565b82525050565b6000606082019050620007d8600083018662000718565b620007e76020830185620007b0565b620007f66040830184620007b0565b949350505050565b6000602082019050620008156000830184620007b0565b92915050565b610e55806200082b6000396000f3fe608060405234801561001057600080fd5b50600436106100935760003560e01c8063313ce56711610066578063313ce5671461013457806370a082311461015257806395d89b4114610182578063a9059cbb146101a0578063dd62ed3e146101d057610093565b806306fdde0314610098578063095ea7b3146100b657806318160ddd146100e657806323b872dd14610104575b600080fd5b6100a0610200565b6040516100ad9190610aa9565b60405180910390f35b6100d060048036038101906100cb9190610b64565b610292565b6040516100dd9190610bbf565b60405180910390f35b6100ee6102b5565b6040516100fb9190610be9565b60405180910390f35b61011e60048036038101906101199190610c04565b6102bf565b60405161012b9190610bbf565b60405180910390f35b61013c6102ee565b6040516101499190610c73565b60405180910390f35b61016c60048036038101906101679190610c8e565b6102f7565b6040516101799190610be9565b60405180910390f35b61018a61033f565b6040516101979190610aa9565b60405180910390f35b6101ba60048036038101906101b59190610b64565b6103d1565b6040516101c79190610bbf565b60405180910390f35b6101ea60048036038101906101e59190610cbb565b6103f4565b6040516101f79190610be9565b60405180910390f35b60606003805461020f90610d2a565b80601f016020809104026020016040519081016040528092919081815260200182805461023b90610d2a565b80156102885780601f1061025d57610100808354040283529160200191610288565b820191906000526020600020905b81548152906001019060200180831161026b57829003601f168201915b5050505050905090565b60008061029d61047b565b90506102aa818585610483565b600191505092915050565b6000600254905090565b6000806102ca61047b565b90506102d7858285610495565b6102e2858585610529565b60019150509392505050565b60006012905090565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60606004805461034e90610d2a565b80601f016020809104026020016040519081016040528092919081815260200182805461037a90610d2a565b80156103c75780601f1061039c576101008083540402835291602001916103c7565b820191906000526020600020905b8154815290600101906020018083116103aa57829003601f168201915b5050505050905090565b6000806103dc61047b565b90506103e9818585610529565b600191505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b610490838383600161061d565b505050565b60006104a184846103f4565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146105235781811015610513578281836040517ffb8f41b200000000000000000000000000000000000000000000000000000000815260040161050a93929190610d6a565b60405180910390fd5b6105228484848403600061061d565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361059b5760006040517f96c6fd1e0000000000000000000000000000000000000000000000000000000081526004016105929190610da1565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361060d5760006040517fec442f050000000000000000000000000000000000000000000000000000000081526004016106049190610da1565b60405180910390fd5b6106188383836107f4565b505050565b600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff160361068f5760006040517fe602df050000000000000000000000000000000000000000000000000000000081526004016106869190610da1565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036107015760006040517f94280d620000000000000000000000000000000000000000000000000000000081526004016106f89190610da1565b60405180910390fd5b81600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555080156107ee578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516107e59190610be9565b60405180910390a35b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361084657806002600082825461083a9190610deb565b92505081905550610919565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156108d2578381836040517fe450d38c0000000000000000000000000000000000000000000000000000000081526004016108c993929190610d6a565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361096257806002600082825403925050819055506109af565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610a0c9190610be9565b60405180910390a3505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610a53578082015181840152602081019050610a38565b60008484015250505050565b6000601f19601f8301169050919050565b6000610a7b82610a19565b610a858185610a24565b9350610a95818560208601610a35565b610a9e81610a5f565b840191505092915050565b60006020820190508181036000830152610ac38184610a70565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610afb82610ad0565b9050919050565b610b0b81610af0565b8114610b1657600080fd5b50565b600081359050610b2881610b02565b92915050565b6000819050919050565b610b4181610b2e565b8114610b4c57600080fd5b50565b600081359050610b5e81610b38565b92915050565b60008060408385031215610b7b57610b7a610acb565b5b6000610b8985828601610b19565b9250506020610b9a85828601610b4f565b9150509250929050565b60008115159050919050565b610bb981610ba4565b82525050565b6000602082019050610bd46000830184610bb0565b92915050565b610be381610b2e565b82525050565b6000602082019050610bfe6000830184610bda565b92915050565b600080600060608486031215610c1d57610c1c610acb565b5b6000610c2b86828701610b19565b9350506020610c3c86828701610b19565b9250506040610c4d86828701610b4f565b9150509250925092565b600060ff82169050919050565b610c6d81610c57565b82525050565b6000602082019050610c886000830184610c64565b92915050565b600060208284031215610ca457610ca3610acb565b5b6000610cb284828501610b19565b91505092915050565b60008060408385031215610cd257610cd1610acb565b5b6000610ce085828601610b19565b9250506020610cf185828601610b19565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610d4257607f821691505b602082108103610d5557610d54610cfb565b5b50919050565b610d6481610af0565b82525050565b6000606082019050610d7f6000830186610d5b565b610d8c6020830185610bda565b610d996040830184610bda565b949350505050565b6000602082019050610db66000830184610d5b565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610df682610b2e565b9150610e0183610b2e565b9250828201905080821115610e1957610e18610dbc565b5b9291505056fea26469706673582212200260074039b179ca88933aa33752c910c15aa2062c21e8c76d09940bd406048b64736f6c63430008180033", + "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100935760003560e01c8063313ce56711610066578063313ce5671461013457806370a082311461015257806395d89b4114610182578063a9059cbb146101a0578063dd62ed3e146101d057610093565b806306fdde0314610098578063095ea7b3146100b657806318160ddd146100e657806323b872dd14610104575b600080fd5b6100a0610200565b6040516100ad9190610aa9565b60405180910390f35b6100d060048036038101906100cb9190610b64565b610292565b6040516100dd9190610bbf565b60405180910390f35b6100ee6102b5565b6040516100fb9190610be9565b60405180910390f35b61011e60048036038101906101199190610c04565b6102bf565b60405161012b9190610bbf565b60405180910390f35b61013c6102ee565b6040516101499190610c73565b60405180910390f35b61016c60048036038101906101679190610c8e565b6102f7565b6040516101799190610be9565b60405180910390f35b61018a61033f565b6040516101979190610aa9565b60405180910390f35b6101ba60048036038101906101b59190610b64565b6103d1565b6040516101c79190610bbf565b60405180910390f35b6101ea60048036038101906101e59190610cbb565b6103f4565b6040516101f79190610be9565b60405180910390f35b60606003805461020f90610d2a565b80601f016020809104026020016040519081016040528092919081815260200182805461023b90610d2a565b80156102885780601f1061025d57610100808354040283529160200191610288565b820191906000526020600020905b81548152906001019060200180831161026b57829003601f168201915b5050505050905090565b60008061029d61047b565b90506102aa818585610483565b600191505092915050565b6000600254905090565b6000806102ca61047b565b90506102d7858285610495565b6102e2858585610529565b60019150509392505050565b60006012905090565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60606004805461034e90610d2a565b80601f016020809104026020016040519081016040528092919081815260200182805461037a90610d2a565b80156103c75780601f1061039c576101008083540402835291602001916103c7565b820191906000526020600020905b8154815290600101906020018083116103aa57829003601f168201915b5050505050905090565b6000806103dc61047b565b90506103e9818585610529565b600191505092915050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054905092915050565b600033905090565b610490838383600161061d565b505050565b60006104a184846103f4565b90507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81146105235781811015610513578281836040517ffb8f41b200000000000000000000000000000000000000000000000000000000815260040161050a93929190610d6a565b60405180910390fd5b6105228484848403600061061d565b5b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361059b5760006040517f96c6fd1e0000000000000000000000000000000000000000000000000000000081526004016105929190610da1565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361060d5760006040517fec442f050000000000000000000000000000000000000000000000000000000081526004016106049190610da1565b60405180910390fd5b6106188383836107f4565b505050565b600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff160361068f5760006040517fe602df050000000000000000000000000000000000000000000000000000000081526004016106869190610da1565b60405180910390fd5b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff16036107015760006040517f94280d620000000000000000000000000000000000000000000000000000000081526004016106f89190610da1565b60405180910390fd5b81600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555080156107ee578273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516107e59190610be9565b60405180910390a35b50505050565b600073ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff160361084657806002600082825461083a9190610deb565b92505081905550610919565b60008060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050818110156108d2578381836040517fe450d38c0000000000000000000000000000000000000000000000000000000081526004016108c993929190610d6a565b60405180910390fd5b8181036000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550505b600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff160361096257806002600082825403925050819055506109af565b806000808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055505b8173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051610a0c9190610be9565b60405180910390a3505050565b600081519050919050565b600082825260208201905092915050565b60005b83811015610a53578082015181840152602081019050610a38565b60008484015250505050565b6000601f19601f8301169050919050565b6000610a7b82610a19565b610a858185610a24565b9350610a95818560208601610a35565b610a9e81610a5f565b840191505092915050565b60006020820190508181036000830152610ac38184610a70565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610afb82610ad0565b9050919050565b610b0b81610af0565b8114610b1657600080fd5b50565b600081359050610b2881610b02565b92915050565b6000819050919050565b610b4181610b2e565b8114610b4c57600080fd5b50565b600081359050610b5e81610b38565b92915050565b60008060408385031215610b7b57610b7a610acb565b5b6000610b8985828601610b19565b9250506020610b9a85828601610b4f565b9150509250929050565b60008115159050919050565b610bb981610ba4565b82525050565b6000602082019050610bd46000830184610bb0565b92915050565b610be381610b2e565b82525050565b6000602082019050610bfe6000830184610bda565b92915050565b600080600060608486031215610c1d57610c1c610acb565b5b6000610c2b86828701610b19565b9350506020610c3c86828701610b19565b9250506040610c4d86828701610b4f565b9150509250925092565b600060ff82169050919050565b610c6d81610c57565b82525050565b6000602082019050610c886000830184610c64565b92915050565b600060208284031215610ca457610ca3610acb565b5b6000610cb284828501610b19565b91505092915050565b60008060408385031215610cd257610cd1610acb565b5b6000610ce085828601610b19565b9250506020610cf185828601610b19565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610d4257607f821691505b602082108103610d5557610d54610cfb565b5b50919050565b610d6481610af0565b82525050565b6000606082019050610d7f6000830186610d5b565b610d8c6020830185610bda565b610d996040830184610bda565b949350505050565b6000602082019050610db66000830184610d5b565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610df682610b2e565b9150610e0183610b2e565b9250828201905080821115610e1957610e18610dbc565b5b9291505056fea26469706673582212200260074039b179ca88933aa33752c910c15aa2062c21e8c76d09940bd406048b64736f6c63430008180033", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/e2e/evm/jest.config.js b/e2e/evm/jest.config.js new file mode 100644 index 000000000..ef3b85897 --- /dev/null +++ b/e2e/evm/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + testEnvironment: 'node', + testMatch: ['**/test/**/*.js'], + verbose: true, +}; diff --git a/e2e/evm/package-lock.json b/e2e/evm/package-lock.json new file mode 100644 index 000000000..76c2e076d --- /dev/null +++ b/e2e/evm/package-lock.json @@ -0,0 +1,3784 @@ +{ + "name": "nibiru-evm-test", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nibiru-evm-test", + "version": "0.0.1", + "license": "ISC", + "devDependencies": { + "@types/jest": "^29.5.12", + "dotenv": "^16.4.5", + "ethers": "^6.12.1", + "jest": "^29.7.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "dev": true + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", + "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.6", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz", + "integrity": "sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.6.tgz", + "integrity": "sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.6", + "@babel/generator": "^7.24.6", + "@babel/helper-compilation-targets": "^7.24.6", + "@babel/helper-module-transforms": "^7.24.6", + "@babel/helpers": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/template": "^7.24.6", + "@babel/traverse": "^7.24.6", + "@babel/types": "^7.24.6", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz", + "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz", + "integrity": "sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.6", + "@babel/helper-validator-option": "^7.24.6", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", + "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", + "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz", + "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz", + "integrity": "sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz", + "integrity": "sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-module-imports": "^7.24.6", + "@babel/helper-simple-access": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz", + "integrity": "sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz", + "integrity": "sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", + "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz", + "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", + "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz", + "integrity": "sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.6.tgz", + "integrity": "sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", + "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.6", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", + "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.6.tgz", + "integrity": "sha512-lWfvAIFNWMlCsU0DRUun2GpFwZdGTukLaHJqRh1JRb80NdAP5Sb1HDHB5X9P9OtgZHQl089UzQkpYlBq2VTPRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.6.tgz", + "integrity": "sha512-TzCtxGgVTEJWWwcYwQhCIQ6WaKlo80/B+Onsk4RRCcYqpYGFcG9etPW94VToGte5AAcxRrhjPUFvUS3Y2qKi4A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", + "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz", + "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.6", + "@babel/generator": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-hoist-variables": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", + "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dev": true, + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "dev": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/node": { + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "dev": true + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001621", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz", + "integrity": "sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.782", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.782.tgz", + "integrity": "sha512-JUfU61e8tr+i5Y1FKXcKs+Xe+rJ+CEqm4cgv1kMihPE2EvYHmYyVr3Im/+1+Z5B29Be2EEGCZCwAc6Tazdl1Yg==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ethers": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.12.1.tgz", + "integrity": "sha512-j6wcVoZf06nqEcBbDWkKg8Fp895SS96dSnTCjiXT+8vt2o02raTn4Lo9ERUuIVU5bAjoPYeA+7ytQFexFmLuVw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "18.15.13", + "aes-js": "4.0.0-beta.5", + "tslib": "2.4.0", + "ws": "8.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/e2e/evm/package.json b/e2e/evm/package.json new file mode 100644 index 000000000..20f4bf6c2 --- /dev/null +++ b/e2e/evm/package.json @@ -0,0 +1,17 @@ +{ + "name": "nibiru-evm-test", + "version": "0.0.1", + "description": "Nibiru EVM tests", + "keywords": [], + "author": "Nibiru Team", + "license": "ISC", + "devDependencies": { + "@types/jest": "^29.5.12", + "dotenv": "^16.4.5", + "ethers": "^6.12.1", + "jest": "^29.7.0" + }, + "scripts": { + "test": "jest" + } +} diff --git a/e2e/evm/test/evm.test.js b/e2e/evm/test/evm.test.js new file mode 100644 index 000000000..d11276e85 --- /dev/null +++ b/e2e/evm/test/evm.test.js @@ -0,0 +1,86 @@ +const {ethers} = require('ethers') +const {config} = require('dotenv') +const fs = require('fs') + +config() + +describe('Ethereum JSON-RPC Interface Tests', () => { + let provider + let wallet + let account + + beforeAll(async () => { + const rpcEndpoint = process.env.JSON_RPC_ENDPOINT + const mnemonic = process.env.MNEMONIC + provider = ethers.getDefaultProvider(rpcEndpoint) + wallet = ethers.Wallet.fromPhrase(mnemonic) + account = wallet.connect(provider) + }) + + test('Simple Transfer, balance check', async () => { + const randomAddress = ethers.Wallet.createRandom().address + const amountToSend = ethers.toBigInt(1000) // unibi + const gasLimit = ethers.toBigInt(100_000) // unibi + + const senderBalanceBefore = await provider.getBalance(wallet.address) + const recipientBalanceBefore = await provider.getBalance(randomAddress) + + expect(senderBalanceBefore).toBeGreaterThan(0) + expect(recipientBalanceBefore).toEqual(ethers.toBigInt(0)) + + // Execute EVM transfer + const transaction = { + gasLimit: gasLimit, + to: randomAddress, + value: amountToSend + } + const txResponse = await account.sendTransaction(transaction) + await txResponse.wait() + expect(txResponse).toHaveProperty('blockHash') + + const senderBalanceAfter = await provider.getBalance(wallet.address) + const recipientBalanceAfter = await provider.getBalance(randomAddress) + + // TODO: gas is not deducted regardless the gas limit, check this + const expectedSenderBalance = senderBalanceBefore - amountToSend + expect(senderBalanceAfter).toBeLessThanOrEqual(expectedSenderBalance) + expect(recipientBalanceAfter).toEqual(amountToSend) + }, 20_000) + + test('Smart Contract', async () => { + // Read contract ABI and bytecode + const contractJSON = JSON.parse( + fs.readFileSync('contracts/FunTokenCompiled.json').toString() + ) + const bytecode = contractJSON['bytecode'] + const abi = contractJSON['abi'] + + // Deploy contract + const contractFactory = new ethers.ContractFactory(abi, bytecode, account) + const contract = await contractFactory.deploy() + await contract.waitForDeployment() + const contractAddress = await contract.getAddress() + expect(contractAddress).toBeDefined() + + // Execute contract: ERC20 transfer + const shrimpAddress = ethers.Wallet.createRandom().address + let ownerInitialBalance = ethers.parseUnits("1000000", 18) + + const amountToSend = ethers.parseUnits("1000", 18) // contract tokens + + let ownerBalance = await contract.balanceOf(account.address) + let shrimpBalance = await contract.balanceOf(shrimpAddress) + + expect(ownerBalance).toEqual(ownerInitialBalance) + expect(shrimpBalance).toEqual(ethers.toBigInt(0)) + + let tx = await contract.transfer(shrimpAddress, amountToSend) + await tx.wait() + + ownerBalance = await contract.balanceOf(account.address) + shrimpBalance = await contract.balanceOf(shrimpAddress) + + expect(ownerBalance).toEqual(ownerInitialBalance - amountToSend) + expect(shrimpBalance).toEqual(amountToSend) + }, 20000) +}) diff --git a/eth/assert_test.go b/eth/assert_test.go index 792ef7213..0a57487ed 100644 --- a/eth/assert_test.go +++ b/eth/assert_test.go @@ -72,7 +72,7 @@ func TestValidateAddress(t *testing.T) { "zero address", common.Address{}.String(), false, }, { - "valid address", evmtest.NewEthAddr().Hex(), false, + "valid address", evmtest.NewEthAccInfo().EthAddr.Hex(), false, }, } @@ -103,7 +103,7 @@ func TestValidateNonZeroAddress(t *testing.T) { "zero address", common.Address{}.String(), true, }, { - "valid address", evmtest.NewEthAddr().Hex(), false, + "valid address", evmtest.NewEthAccInfo().EthAddr.Hex(), false, }, } diff --git a/eth/chain_id.go b/eth/chain_id.go index 60dfaa880..d6d09dab4 100644 --- a/eth/chain_id.go +++ b/eth/chain_id.go @@ -6,15 +6,21 @@ import ( "math/big" "regexp" "strings" + + "github.com/NibiruChain/nibiru/app/appconst" ) var ( - regexChainID = `[a-z]{1,}` - regexEIP155Separator = `_{1}` - regexEIP155 = `[1-9][0-9]*` - regexEpochSeparator = `-{1}` - regexEpoch = `[1-9][0-9]*` - nibiruEvmChainId = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)%s(%s)$`, + // one of any lower case letter from "a"-"z" + regexChainID = `[a-z]{1,}` + // one of either "_" or "-" + regexEIP155Separator = `[_-]{1}` + // one of "_" + // regexEIP155Separator = `_{1}` + regexEIP155 = `[1-9][0-9]*` + regexEpochSeparator = `-{1}` + regexEpoch = `[1-9][0-9]*` + nibiruEvmChainId = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)%s(%s)$`, regexChainID, regexEIP155Separator, regexEIP155, @@ -32,10 +38,20 @@ func IsValidChainID(chainID string) bool { return nibiruEvmChainId.MatchString(chainID) } -// ParseChainID parses a string chain identifier's epoch to an +// ParseEthChainID parses a string chain identifier's epoch to an +// Ethereum-compatible chain-id in *big.Int format. +// +// This function uses Nibiru's map of chain IDs defined in Nibiru/app/appconst +// rather than the regex of EIP155, which is implemented by +// ParseEthChainIDStrict. +func ParseEthChainID(chainID string) (*big.Int, error) { + return appconst.GetEthChainID(chainID), nil +} + +// ParseEthChainIDStrict parses a string chain identifier's epoch to an // Ethereum-compatible chain-id in *big.Int format. The function returns an error // if the chain-id has an invalid format -func ParseChainID(chainID string) (*big.Int, error) { +func ParseEthChainIDStrict(chainID string) (*big.Int, error) { chainID = strings.TrimSpace(chainID) if len(chainID) > 48 { return nil, ErrInvalidChainID.Wrapf( diff --git a/eth/chain_id_test.go b/eth/chain_id_test.go index ccddc17f2..2594c13c0 100644 --- a/eth/chain_id_test.go +++ b/eth/chain_id_test.go @@ -21,14 +21,24 @@ func TestParseChainID_Happy(t *testing.T) { expInt: big.NewInt(1), }, { - chainID: "aragonchain_256-1", + chainID: "cataclysm_256-1", name: "valid chain-id, multiple digits", expInt: big.NewInt(256), }, + { + chainID: "cataclysm-4-20", + name: "valid chain-id, dashed, multiple digits", + expInt: big.NewInt(4), + }, + { + chainID: "chain-1-1", + name: "valid chain-id, double dash", + expInt: big.NewInt(1), + }, } for _, tc := range testCases { - chainIDEpoch, err := ParseChainID(tc.chainID) + chainIDEpoch, err := ParseEthChainIDStrict(tc.chainID) require.NoError(t, err, tc.name) var errMsg string = "" if err != nil { @@ -46,11 +56,7 @@ func TestParseChainID_Sad(t *testing.T) { chainID string }{ { - chainID: "aragonchain-1-1", - name: "invalid chain-id, double dash", - }, - { - chainID: "aragonchain_1_1", + chainID: "chain_1_1", name: "invalid chain-id, double underscore", }, { @@ -116,7 +122,7 @@ func TestParseChainID_Sad(t *testing.T) { } for _, tc := range testCases { - chainIDEpoch, err := ParseChainID(tc.chainID) + chainIDEpoch, err := ParseEthChainIDStrict(tc.chainID) require.Error(t, err, tc.name) require.Nil(t, chainIDEpoch) require.False(t, IsValidChainID(tc.chainID), tc.name) diff --git a/eth/codec.go b/eth/codec.go index 75be8cae2..fe0116973 100644 --- a/eth/codec.go +++ b/eth/codec.go @@ -2,11 +2,8 @@ package eth import ( - fmt "fmt" "math/big" - "strings" - sdkmath "cosmossdk.io/math" codectypes "github.com/cosmos/cosmos-sdk/codec/types" sdktx "github.com/cosmos/cosmos-sdk/types/tx" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" @@ -18,7 +15,7 @@ const ( EthBaseDenom = appconst.BondDenom // EIP155ChainID_Testnet: Chain ID for a testnet Nibiru following the // format proposed by Vitalik in EIP155. - EIP155ChainID_Testnet = "nibirutest_420" + EIP155ChainID_Testnet = "nibirutest_420-1" DefaultGasPrice = 20 @@ -69,14 +66,9 @@ func RegisterInterfaces(registry codectypes.InterfaceRegistry) { } func ParseEIP155ChainIDNumber(chainId string) *big.Int { - parts := strings.Split(chainId, "_") - int, ok := sdkmath.NewIntFromString(parts[len(parts)-1]) - if !ok { - err := fmt.Errorf( - "failed to parse EIP155 chain ID number from chain ID: \"%s\"", - chainId, - ) + idNum, err := ParseEthChainID(chainId) + if err != nil { panic(err) } - return int.BigInt() + return idNum } diff --git a/eth/eip712/eip712_test.go b/eth/eip712/eip712_test.go index ec6ecb3ad..885f292a5 100644 --- a/eth/eip712/eip712_test.go +++ b/eth/eip712/eip712_test.go @@ -48,7 +48,6 @@ import ( const ( msgsFieldName = "msgs" - baseDenom = "anibi" TESTNET_CHAIN_ID = "nibiru_9000" ) @@ -81,7 +80,7 @@ func TestEIP712TestSuite(t *testing.T) { func (suite *EIP712TestSuite) SetupTest() { suite.config = encoding.MakeConfig(app.ModuleBasics) suite.clientCtx = client.Context{}.WithTxConfig(suite.config.TxConfig) - suite.denom = baseDenom + suite.denom = evm.DefaultEVMDenom sdk.GetConfig().SetBech32PrefixForAccount(ethclient.Bech32Prefix, "") eip712.SetEncodingConfig(suite.config) @@ -274,7 +273,7 @@ func (suite *EIP712TestSuite) TestEIP712() { expectSuccess: false, }, { - title: "Fails - Invalid ChainID", + title: "Success - Invalid ChainID uses default", chainID: "invalidchainid", msgs: []sdk.Msg{ govtypes.NewMsgVote( @@ -283,7 +282,7 @@ func (suite *EIP712TestSuite) TestEIP712() { govtypes.OptionNo, ), }, - expectSuccess: false, + expectSuccess: true, }, { title: "Fails - Includes TimeoutHeight", diff --git a/eth/eip712/encoding.go b/eth/eip712/encoding.go index 97d2a9dbb..b7f54114a 100644 --- a/eth/eip712/encoding.go +++ b/eth/eip712/encoding.go @@ -103,7 +103,7 @@ func decodeAminoSignDoc(signDocBytes []byte) (apitypes.TypedData, error) { return apitypes.TypedData{}, err } - chainID, err := eth.ParseChainID(aminoDoc.ChainID) + chainID, err := eth.ParseEthChainID(aminoDoc.ChainID) if err != nil { return apitypes.TypedData{}, errors.New("invalid chain ID passed as argument") } @@ -167,7 +167,7 @@ func decodeProtobufSignDoc(signDocBytes []byte) (apitypes.TypedData, error) { signerInfo := authInfo.SignerInfos[0] - chainID, err := eth.ParseChainID(signDoc.ChainId) + chainID, err := eth.ParseEthChainID(signDoc.ChainId) if err != nil { return apitypes.TypedData{}, fmt.Errorf("invalid chain ID passed as argument: %w", err) } diff --git a/eth/eip712/encoding_legacy.go b/eth/eip712/encoding_legacy.go index 08653560a..8bf5fc486 100644 --- a/eth/eip712/encoding_legacy.go +++ b/eth/eip712/encoding_legacy.go @@ -95,7 +95,7 @@ func legacyDecodeAminoSignDoc(signDocBytes []byte) (apitypes.TypedData, error) { FeePayer: feePayer, } - chainID, err := eth.ParseChainID(aminoDoc.ChainID) + chainID, err := eth.ParseEthChainID(aminoDoc.ChainID) if err != nil { return apitypes.TypedData{}, errors.New("invalid chain ID passed as argument") } @@ -165,7 +165,7 @@ func legacyDecodeProtobufSignDoc(signDocBytes []byte) (apitypes.TypedData, error signerInfo := authInfo.SignerInfos[0] - chainID, err := eth.ParseChainID(signDoc.ChainId) + chainID, err := eth.ParseEthChainID(signDoc.ChainId) if err != nil { return apitypes.TypedData{}, fmt.Errorf("invalid chain ID passed as argument: %w", err) } diff --git a/eth/eip712/preprocess.go b/eth/eip712/preprocess.go index d03a44fbc..14545f60b 100644 --- a/eth/eip712/preprocess.go +++ b/eth/eip712/preprocess.go @@ -46,7 +46,7 @@ func PreprocessLedgerTx(chainID string, keyType cosmoskr.KeyType, txBuilder clie sigBytes := sigData.Signature // Parse Chain ID as big.Int - chainIDInt, err := eth.ParseChainID(chainID) + chainIDInt, err := eth.ParseEthChainID(chainID) if err != nil { return fmt.Errorf("could not parse chain id: %w", err) } diff --git a/eth/encoding/config_test.go b/eth/encoding/config_test.go index 40f58901c..06ffa0a4d 100644 --- a/eth/encoding/config_test.go +++ b/eth/encoding/config_test.go @@ -15,7 +15,8 @@ import ( ) func TestTxEncoding(t *testing.T) { - addr, key := evmtest.PrivKeyEth() + ethAcc := evmtest.NewEthAccInfo() + addr, key := ethAcc.EthAddr, ethAcc.PrivKey signer := evmtest.NewSigner(key) ethTxParams := evm.EvmTxArgs{ diff --git a/eth/gas_limit.go b/eth/gas_limit.go new file mode 100644 index 000000000..1606bd693 --- /dev/null +++ b/eth/gas_limit.go @@ -0,0 +1,161 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package eth + +import ( + "encoding/json" + fmt "fmt" + math "math" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// BlockGasLimit: Gas (limit) as defined by the block gas meter. Gas limit is +// derived from the consensus params if the block gas meter is nil. +func BlockGasLimit(ctx sdk.Context) (gasLimit uint64) { + blockGasMeter := ctx.BlockGasMeter() + + // Get the limit from the gas meter only if its not null and not an + // InfiniteGasMeter + if blockGasMeter != nil && blockGasMeter.Limit() != 0 { + return blockGasMeter.Limit() + } + + // Otherwise get from the consensus parameters + cp := ctx.ConsensusParams() + if cp == nil || cp.Block == nil { + return 0 + } + + maxGas := cp.Block.MaxGas + + // Setting max_gas to -1 in Tendermint means there is no limit on the maximum gas consumption for transactions + // https://github.com/cometbft/cometbft/blob/v0.37.2/proto/tendermint/types/params.proto#L25-L27 + if maxGas == -1 { + return math.MaxUint64 + } + + if maxGas > 0 { + return uint64(maxGas) // #nosec G701 -- maxGas is int64 type. It can never be greater than math.MaxUint64 + } + + return 0 +} + +// NewInfiniteGasMeterWithLimit returns a reference to a new infiniteGasMeter. +func NewInfiniteGasMeterWithLimit(limit sdk.Gas) sdk.GasMeter { + return &InfiniteGasMeter{ + consumed: 0, + limit: limit, + } +} + +// NewInfiniteGasMeter: Alias for an infinite gas meter +// ([NewInfiniteGasMeterWithLimitla)] with a tracked but unenforced gas limit. +func NewInfiniteGasMeter() sdk.GasMeter { + return NewInfiniteGasMeterWithLimit(math.MaxUint64) +} + +var _ sdk.GasMeter = &InfiniteGasMeter{} + +// InfiniteGasMeter: A special impl of `sdk.GasMeter` that ignores any gas +// limits, allowing an unlimited amount of gas to be consumed. This is especially +// useful for scenarios where gas consumption needs to be monitored but not +// restricted, such as during testing or in parts of the chain where constraints +// are meant to be set differently. +type InfiniteGasMeter struct { + // consumed: Tracks the amount of gas units consumed. + consumed sdk.Gas + // limit: Nominal unit for the gas limit, which is not enforced in a way that + // restricts consumption. + limit sdk.Gas +} + +// GasConsumedToLimit returns the gas limit if gas consumed is past the limit, +// otherwise it returns the consumed gas. +// +// Note that This function is used when recovering +// from a panic in "BlockGasMeter" when the consumed gas passes the limit. +func (g *InfiniteGasMeter) GasConsumedToLimit() sdk.Gas { + return g.consumed +} + +// GasConsumed returns the gas consumed from the GasMeter. +func (g *InfiniteGasMeter) GasConsumed() sdk.Gas { + return g.consumed +} + +// Limit returns the gas limit of the GasMeter. +func (g *InfiniteGasMeter) Limit() sdk.Gas { + return g.limit +} + +// addUint64Overflow performs the addition operation on two uint64 integers and +// returns a boolean on whether or not the result overflows. +func addUint64Overflow(a, b uint64) (uint64, bool) { + if math.MaxUint64-a < b { + return 0, true + } + + return a + b, false +} + +// ConsumeGas adds the given amount of gas to the gas consumed and panics if it overflows the limit or out of gas. +func (g *InfiniteGasMeter) ConsumeGas(amount sdk.Gas, descriptor string) { + var overflow bool + // TODO: Should we set the consumed field after overflow checking? + g.consumed, overflow = addUint64Overflow(g.consumed, amount) + if overflow { + panic(ErrorGasOverflow{descriptor}) + } +} + +// RefundGas will deduct the given amount from the gas consumed. If the amount is +// greater than the gas consumed, the function will panic. +// +// Use case: This functionality enables refunding gas to the trasaction or block gas pools so that +// EVM-compatible chains can fully support the go-ethereum StateDb interface. +// See https://github.com/cosmos/cosmos-sdk/pull/9403 for reference. +func (g *InfiniteGasMeter) RefundGas(amount sdk.Gas, descriptor string) { + if g.consumed < amount { + panic(ErrorNegativeGasConsumed{Descriptor: descriptor}) + } + + g.consumed -= amount +} + +// IsPastLimit returns true if gas consumed is past limit, otherwise it returns +// false. In the case of the the [InfiniteGasMeter], this always returns false. +func (g *InfiniteGasMeter) IsPastLimit() bool { + return false +} + +// IsOutOfGas returns true if gas consumed is greater than or equal to gas limit, +// otherwise it returns false. In the case of the the [InfiniteGasMeter], this +// always returns false for unrestricted gas consumption. +func (g *InfiniteGasMeter) IsOutOfGas() bool { + return false +} + +// String returns the BasicGasMeter's gas limit and gas consumed. +func (g *InfiniteGasMeter) String() string { + data := map[string]uint64{"consumed": g.consumed, "limit": g.limit} + jsonData, _ := json.Marshal(data) + return fmt.Sprintf("InfiniteGasMeter: %s", jsonData) +} + +// GasRemaining returns MaxUint64 since limit is not confined in infiniteGasMeter. +func (g *InfiniteGasMeter) GasRemaining() sdk.Gas { + return math.MaxUint64 +} + +// ErrorNegativeGasConsumed defines an error thrown when the amount of gas +// refunded results in a negative gas consumed amount. +type ErrorNegativeGasConsumed struct { + Descriptor string +} + +// ErrorGasOverflow defines an error thrown when an action results gas consumption +// unsigned integer overflow. +type ErrorGasOverflow struct { + Descriptor string +} diff --git a/eth/gas_limit_test.go b/eth/gas_limit_test.go new file mode 100644 index 000000000..fc126a758 --- /dev/null +++ b/eth/gas_limit_test.go @@ -0,0 +1,165 @@ +package eth_test + +import ( + "math" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/x/evm/evmtest" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" +) + +// TestGasMeter: Ensures correct behvaior of the `InfiniteGasMeterWithLimit` +// implementation by checking that gas consumption and usage values are correctly +// tracked, that consuming gas within the limit does not cause a panic, and that +// gas refunds work as expected. +func (s *Suite) TestGasMeter() { + meter := eth.NewInfiniteGasMeter() + s.Require().Equal(uint64(math.MaxUint64), meter.Limit()) + s.Require().Equal(uint64(math.MaxUint64), meter.GasRemaining()) + s.Require().Equal(uint64(0), meter.GasConsumed()) + s.Require().Equal(uint64(0), meter.GasConsumedToLimit()) + + meter.ConsumeGas(10, "consume 10") + s.Require().Equal(uint64(math.MaxUint64), meter.GasRemaining()) + s.Require().Equal(uint64(10), meter.GasConsumed()) + s.Require().Equal(uint64(10), meter.GasConsumedToLimit()) + + // Test RefundGas + meter.RefundGas(1, "refund 1") + s.Require().Equal(uint64(math.MaxUint64), meter.GasRemaining()) + s.Require().Equal(uint64(9), meter.GasConsumed()) + + // Test IsPastLimit and IsOutOfGas + s.False(meter.IsPastLimit()) + s.False(meter.IsOutOfGas()) + + // Consume large amount fo gas to test overflow handling + meter.ConsumeGas(sdk.Gas(math.MaxUint64/2), "consume half max uint64") + s.Require().Panics(func() { meter.ConsumeGas(sdk.Gas(math.MaxUint64/2)+2, "panic") }) + s.Require().Panics(func() { meter.RefundGas(meter.GasConsumed()+1, "refund greater than consumed") }) + + // Additional tests for RefundGas + s.Require().NotPanics(func() { + meter.RefundGas(meter.GasConsumed(), "refund all") + }) + s.Require().Equal(uint64(0), meter.GasConsumed()) + s.Require().Panics(func() { + meter.RefundGas(meter.GasConsumed()+1, "refund more than consumed") + }) + s.Require().NotPanics(func() { meter.RefundGas(meter.GasConsumed(), "refund all consumed gas") }) + s.Require().Equal(uint64(0), meter.GasConsumed()) + s.Require().Equal(uint64(math.MaxUint64), meter.GasRemaining()) + + // Additional tests for IsPastLimit and IsOutOfGas with high gas usage + s.Equal(uint64(math.MaxUint64), meter.GasRemaining()) + meter.ConsumeGas(sdk.Gas(math.MaxUint64-1), "consume nearly all gas") + s.Equal(uint64(math.MaxUint64), meter.GasRemaining()) + s.Require().False(meter.IsPastLimit()) + s.Require().False(meter.IsOutOfGas()) + + // Test the String method + expectedString := `InfiniteGasMeter: {"consumed":18446744073709551614,"limit":18446744073709551615}` + s.Require().Equal(expectedString, meter.String()) + + // Test another instance with a specific limit + meter2 := eth.NewInfiniteGasMeterWithLimit(100) + s.Require().Equal(uint64(100), meter2.Limit()) + s.Require().Equal(uint64(0), meter2.GasConsumed()) + + meter2.ConsumeGas(50, "consume 50") + s.Require().Equal(uint64(50), meter2.GasConsumed()) + s.False(meter2.IsPastLimit()) + s.False(meter2.IsOutOfGas()) + + meter2.ConsumeGas(50, "consume remaining 50") + s.Require().Equal(uint64(math.MaxUint64), meter2.GasRemaining()) + s.False(meter2.IsPastLimit()) + s.False(meter2.IsOutOfGas()) + s.Require().NotPanics(func() { meter2.ConsumeGas(1, "exceed limit") }) + + // Test the String method for the second meter + expectedString2 := `InfiniteGasMeter: {"consumed":101,"limit":100}` + s.Require().Equal(expectedString2, meter2.String()) +} + +func (s *Suite) TestBlockGasLimit() { + newCtx := func() sdk.Context { return evmtest.NewTestDeps().Ctx } + tests := []struct { + name string + setupContext func() sdk.Context + wantGasLimit uint64 + }{ + { + name: "BlockGasMeter is not nil and has a non-zero limit", + setupContext: func() sdk.Context { + ctx := newCtx() + gasMeter := eth.NewInfiniteGasMeterWithLimit(100) + ctx = ctx.WithBlockGasMeter(gasMeter) + return ctx + }, + wantGasLimit: 100, + }, + { + name: "BlockGasMeter is nil and ConsensusParams is nil", + setupContext: func() sdk.Context { + ctx := newCtx() + ctx = ctx.WithConsensusParams(nil) + return ctx + }, + wantGasLimit: 0, + }, + { + name: "BlockGasMeter is nil and ConsensusParams has Block with MaxGas -1", + setupContext: func() sdk.Context { + ctx := newCtx() + cp := &tmproto.ConsensusParams{ + Block: &tmproto.BlockParams{ + MaxGas: -1, + }, + } + ctx = ctx.WithConsensusParams(cp) + return ctx + }, + wantGasLimit: math.MaxUint64, + }, + { + name: "BlockGasMeter is nil and ConsensusParams has Block with MaxGas > 0", + setupContext: func() sdk.Context { + ctx := newCtx() + cp := &tmproto.ConsensusParams{ + Block: &tmproto.BlockParams{ + MaxGas: 1000, + }, + } + ctx = ctx.WithConsensusParams(cp) + return ctx + }, + wantGasLimit: 1000, + }, + { + name: "BlockGasMeter is nil and ConsensusParams has Block with MaxGas 0", + setupContext: func() sdk.Context { + ctx := newCtx() + cp := &tmproto.ConsensusParams{ + Block: &tmproto.BlockParams{ + MaxGas: 0, + }, + } + ctx = ctx.WithConsensusParams(cp) + return ctx + }, + wantGasLimit: 0, + }, + } + + for _, tt := range tests { + s.Run(tt.name, func() { + ctx := tt.setupContext() + gotGaslimit := eth.BlockGasLimit(ctx) + s.Require().Equal(tt.wantGasLimit, gotGaslimit) + }) + } +} diff --git a/eth/rpc/backend/account_info_test.go b/eth/rpc/backend/account_info_test.go index 64531bdec..a4112bbad 100644 --- a/eth/rpc/backend/account_info_test.go +++ b/eth/rpc/backend/account_info_test.go @@ -33,7 +33,7 @@ func (s *BackendSuite) TestGetCode() { }{ { "fail - BlockHash and BlockNumber are both nil ", - evmtest.NewEthAddr(), + evmtest.NewEthAccInfo().EthAddr, rpc.BlockNumberOrHash{}, func(addr common.Address) {}, false, @@ -41,7 +41,7 @@ func (s *BackendSuite) TestGetCode() { }, { "fail - query client errors on getting Code", - evmtest.NewEthAddr(), + evmtest.NewEthAccInfo().EthAddr, rpc.BlockNumberOrHash{BlockNumber: &blockNr}, func(addr common.Address) { queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) @@ -52,7 +52,7 @@ func (s *BackendSuite) TestGetCode() { }, { "pass", - evmtest.NewEthAddr(), + evmtest.NewEthAccInfo().EthAddr, rpc.BlockNumberOrHash{BlockNumber: &blockNr}, func(addr common.Address) { queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) @@ -81,7 +81,7 @@ func (s *BackendSuite) TestGetCode() { func (s *BackendSuite) TestGetProof() { blockNrInvalid := rpc.NewBlockNumber(big.NewInt(1)) blockNr := rpc.NewBlockNumber(big.NewInt(4)) - address1 := evmtest.NewEthAddr() + address1 := evmtest.NewEthAccInfo().EthAddr testCases := []struct { name string @@ -199,7 +199,7 @@ func (s *BackendSuite) TestGetStorageAt() { }{ { "fail - BlockHash and BlockNumber are both nil", - evmtest.NewEthAddr(), + evmtest.NewEthAccInfo().EthAddr, "0x0", rpc.BlockNumberOrHash{}, func(addr common.Address, key string, storage string) {}, @@ -208,7 +208,7 @@ func (s *BackendSuite) TestGetStorageAt() { }, { "fail - query client errors on getting Storage", - evmtest.NewEthAddr(), + evmtest.NewEthAccInfo().EthAddr, "0x0", rpc.BlockNumberOrHash{BlockNumber: &blockNr}, func(addr common.Address, key string, storage string) { @@ -220,7 +220,7 @@ func (s *BackendSuite) TestGetStorageAt() { }, { "pass", - evmtest.NewEthAddr(), + evmtest.NewEthAccInfo().EthAddr, "0x0", rpc.BlockNumberOrHash{BlockNumber: &blockNr}, func(addr common.Address, key string, storage string) { @@ -260,7 +260,7 @@ func (s *BackendSuite) TestGetEvmGasBalance() { }{ { "fail - BlockHash and BlockNumber are both nil", - evmtest.NewEthAddr(), + evmtest.NewEthAccInfo().EthAddr, rpc.BlockNumberOrHash{}, func(bn rpc.BlockNumber, addr common.Address) { }, @@ -269,7 +269,7 @@ func (s *BackendSuite) TestGetEvmGasBalance() { }, { "fail - tendermint client failed to get block", - evmtest.NewEthAddr(), + evmtest.NewEthAccInfo().EthAddr, rpc.BlockNumberOrHash{BlockNumber: &blockNr}, func(bn rpc.BlockNumber, addr common.Address) { client := s.backend.clientCtx.Client.(*mocks.Client) @@ -280,7 +280,7 @@ func (s *BackendSuite) TestGetEvmGasBalance() { }, { "fail - query client failed to get balance", - evmtest.NewEthAddr(), + evmtest.NewEthAccInfo().EthAddr, rpc.BlockNumberOrHash{BlockNumber: &blockNr}, func(bn rpc.BlockNumber, addr common.Address) { client := s.backend.clientCtx.Client.(*mocks.Client) @@ -294,7 +294,7 @@ func (s *BackendSuite) TestGetEvmGasBalance() { }, { "fail - invalid balance", - evmtest.NewEthAddr(), + evmtest.NewEthAccInfo().EthAddr, rpc.BlockNumberOrHash{BlockNumber: &blockNr}, func(bn rpc.BlockNumber, addr common.Address) { client := s.backend.clientCtx.Client.(*mocks.Client) @@ -308,7 +308,7 @@ func (s *BackendSuite) TestGetEvmGasBalance() { }, { "fail - pruned node state", - evmtest.NewEthAddr(), + evmtest.NewEthAccInfo().EthAddr, rpc.BlockNumberOrHash{BlockNumber: &blockNr}, func(bn rpc.BlockNumber, addr common.Address) { client := s.backend.clientCtx.Client.(*mocks.Client) @@ -322,7 +322,7 @@ func (s *BackendSuite) TestGetEvmGasBalance() { }, { "pass", - evmtest.NewEthAddr(), + evmtest.NewEthAccInfo().EthAddr, rpc.BlockNumberOrHash{BlockNumber: &blockNr}, func(bn rpc.BlockNumber, addr common.Address) { client := s.backend.clientCtx.Client.(*mocks.Client) @@ -393,7 +393,7 @@ func (s *BackendSuite) TestGetTransactionCount() { s.Run(fmt.Sprintf("Case %s", tc.name), func() { s.SetupTest() - addr := evmtest.NewEthAddr() + addr := evmtest.NewEthAccInfo().EthAddr if tc.accExists { addr = common.BytesToAddress(s.acc.Bytes()) } diff --git a/eth/rpc/backend/backend.go b/eth/rpc/backend/backend.go index c6344efe6..6976795fb 100644 --- a/eth/rpc/backend/backend.go +++ b/eth/rpc/backend/backend.go @@ -155,7 +155,7 @@ func NewBackend( allowUnprotectedTxs bool, indexer eth.EVMTxIndexer, ) *Backend { - chainID, err := eth.ParseChainID(clientCtx.ChainID) + chainID, err := eth.ParseEthChainID(clientCtx.ChainID) if err != nil { panic(err) } diff --git a/eth/rpc/backend/backend_suite_test.go b/eth/rpc/backend/backend_suite_test.go index e77d53c45..b42fe2de1 100644 --- a/eth/rpc/backend/backend_suite_test.go +++ b/eth/rpc/backend/backend_suite_test.go @@ -42,7 +42,7 @@ func TestBackendSuite(t *testing.T) { suite.Run(t, new(BackendSuite)) } -const ChainID = eth.EIP155ChainID_Testnet + "-1" +const ChainID = eth.EIP155ChainID_Testnet // SetupTest is executed before every BackendTestSuite test func (s *BackendSuite) SetupTest() { @@ -58,7 +58,7 @@ func (s *BackendSuite) SetupTest() { } // Create Account with set sequence - s.acc = sdk.AccAddress(evmtest.NewEthAddr().Bytes()) + s.acc = sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()) accounts := map[string]client.TestAccount{} accounts[s.acc.String()] = client.TestAccount{ Address: s.acc, @@ -66,7 +66,8 @@ func (s *BackendSuite) SetupTest() { Seq: uint64(1), } - from, priv := evmtest.PrivKeyEth() + ethAcc := evmtest.NewEthAccInfo() + from, priv := ethAcc.EthAddr, ethAcc.PrivKey s.from = from s.signer = evmtest.NewSigner(priv) s.Require().NoError(err) @@ -179,7 +180,8 @@ func (s *BackendSuite) generateTestKeyring(clientDir string) (keyring.Keyring, e } func (s *BackendSuite) signAndEncodeEthTx(msgEthereumTx *evm.MsgEthereumTx) []byte { - from, priv := evmtest.PrivKeyEth() + ethAcc := evmtest.NewEthAccInfo() + from, priv := ethAcc.EthAddr, ethAcc.PrivKey signer := evmtest.NewSigner(priv) queryClient := s.backend.queryClient.QueryClient.(*mocks.EVMQueryClient) diff --git a/eth/rpc/backend/blocks_test.go b/eth/rpc/backend/blocks_test.go index e415bf334..b65607494 100644 --- a/eth/rpc/backend/blocks_test.go +++ b/eth/rpc/backend/blocks_test.go @@ -104,7 +104,7 @@ func (s *BackendSuite) TestGetBlockByNumber() { blockNumber: rpc.BlockNumber(1), fullTx: true, baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), ethTx: nil, ethTxBz: nil, registerMock: func(blockNum rpc.BlockNumber, _ math.Int, _ sdk.AccAddress, _ []byte) { @@ -120,7 +120,7 @@ func (s *BackendSuite) TestGetBlockByNumber() { blockNumber: rpc.BlockNumber(1), fullTx: true, baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), ethTx: nil, ethTxBz: nil, registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { @@ -136,7 +136,7 @@ func (s *BackendSuite) TestGetBlockByNumber() { blockNumber: rpc.BlockNumber(1), fullTx: true, baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), ethTx: nil, ethTxBz: nil, registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { @@ -153,7 +153,7 @@ func (s *BackendSuite) TestGetBlockByNumber() { blockNumber: rpc.BlockNumber(1), fullTx: true, baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), ethTx: nil, ethTxBz: nil, registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { @@ -175,7 +175,7 @@ func (s *BackendSuite) TestGetBlockByNumber() { blockNumber: rpc.BlockNumber(1), fullTx: true, baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), ethTx: msgEthereumTx, ethTxBz: bz, registerMock: func(blockNum rpc.BlockNumber, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { @@ -249,7 +249,7 @@ func (s *BackendSuite) TestGetBlockByHash() { hash: common.BytesToHash(block.Hash()), fullTx: true, baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), tx: nil, txBz: nil, registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { @@ -264,7 +264,7 @@ func (s *BackendSuite) TestGetBlockByHash() { hash: common.BytesToHash(block.Hash()), fullTx: true, baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), tx: nil, txBz: nil, registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { @@ -279,7 +279,7 @@ func (s *BackendSuite) TestGetBlockByHash() { hash: common.BytesToHash(block.Hash()), fullTx: true, baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), tx: nil, txBz: nil, registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { @@ -297,7 +297,7 @@ func (s *BackendSuite) TestGetBlockByHash() { hash: common.BytesToHash(block.Hash()), fullTx: true, baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), tx: nil, txBz: nil, registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { @@ -320,7 +320,7 @@ func (s *BackendSuite) TestGetBlockByHash() { hash: common.BytesToHash(block.Hash()), fullTx: true, baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), tx: msgEthereumTx, txBz: bz, registerMock: func(hash common.Hash, baseFee math.Int, validator sdk.AccAddress, txBz []byte) { @@ -926,7 +926,7 @@ func (s *BackendSuite) TestGetEthBlockFromTendermint() { { name: "pass - block with tx - with BaseFee error", baseFee: nil, - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), height: int64(1), resBlock: &cmtrpc.ResultBlock{ Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), @@ -974,7 +974,7 @@ func (s *BackendSuite) TestGetEthBlockFromTendermint() { { name: "pass - block with tx - with ConsensusParams error - BlockMaxGas defaults to max uint32", baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), height: int64(1), resBlock: &cmtrpc.ResultBlock{ Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), @@ -998,7 +998,7 @@ func (s *BackendSuite) TestGetEthBlockFromTendermint() { { name: "pass - block with tx - with ShouldIgnoreGasUsed - empty txs", baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), height: int64(1), resBlock: &cmtrpc.ResultBlock{ Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), @@ -1028,7 +1028,7 @@ func (s *BackendSuite) TestGetEthBlockFromTendermint() { { name: "pass - block with tx - non fullTx", baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), height: int64(1), resBlock: &cmtrpc.ResultBlock{ Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), @@ -1052,7 +1052,7 @@ func (s *BackendSuite) TestGetEthBlockFromTendermint() { { name: "pass - block with tx", baseFee: math.NewInt(1).BigInt(), - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), height: int64(1), resBlock: &cmtrpc.ResultBlock{ Block: cmt.MakeBlock(1, []cmt.Tx{bz}, nil, nil), diff --git a/eth/rpc/backend/call_tx.go b/eth/rpc/backend/call_tx.go index b207f7b66..d70161b81 100644 --- a/eth/rpc/backend/call_tx.go +++ b/eth/rpc/backend/call_tx.go @@ -38,7 +38,7 @@ func (b *Backend) Resend(args evm.JsonTxArgs, gasPrice *hexutil.Big, gasLimit *h // The signer used should always be the 'latest' known one because we expect // signers to be backwards-compatible with old transactions. - eip155ChainID, err := eth.ParseChainID(b.clientCtx.ChainID) + eip155ChainID, err := eth.ParseEthChainID(b.clientCtx.ChainID) if err != nil { return common.Hash{}, err } diff --git a/eth/rpc/backend/call_tx_test.go b/eth/rpc/backend/call_tx_test.go index dd55c1500..9a87546bf 100644 --- a/eth/rpc/backend/call_tx_test.go +++ b/eth/rpc/backend/call_tx_test.go @@ -22,7 +22,7 @@ func (s *BackendSuite) TestResend() { txNonce := (hexutil.Uint64)(1) baseFee := math.NewInt(1) gasPrice := new(hexutil.Big) - toAddr := evmtest.NewEthAddr() + toAddr := evmtest.NewEthAccInfo().EthAddr chainID := (*hexutil.Big)(s.backend.chainID) callArgs := evm.JsonTxArgs{ From: nil, @@ -387,7 +387,7 @@ func (s *BackendSuite) TestSendRawTransaction() { func (s *BackendSuite) TestDoCall() { _, bz := s.buildEthereumTx() gasPrice := (*hexutil.Big)(big.NewInt(1)) - toAddr := evmtest.NewEthAddr() + toAddr := evmtest.NewEthAccInfo().EthAddr chainID := (*hexutil.Big)(s.backend.chainID) callArgs := evm.JsonTxArgs{ From: nil, diff --git a/eth/rpc/backend/chain_info.go b/eth/rpc/backend/chain_info.go index 3936d5416..4b67db79a 100644 --- a/eth/rpc/backend/chain_info.go +++ b/eth/rpc/backend/chain_info.go @@ -22,7 +22,7 @@ import ( // ChainID is the EIP-155 replay-protection chain id for the current ethereum chain config. func (b *Backend) ChainID() (*hexutil.Big, error) { - eip155ChainID, err := eth.ParseChainID(b.clientCtx.ChainID) + eip155ChainID, err := eth.ParseEthChainID(b.clientCtx.ChainID) if err != nil { panic(err) } diff --git a/eth/rpc/backend/chain_info_test.go b/eth/rpc/backend/chain_info_test.go index c1267cd0d..cc533257c 100644 --- a/eth/rpc/backend/chain_info_test.go +++ b/eth/rpc/backend/chain_info_test.go @@ -99,7 +99,8 @@ func (s *BackendSuite) TestBaseFee() { } func (s *BackendSuite) TestChainId() { - expChainIDNumber := eth.ParseEIP155ChainIDNumber(eth.EIP155ChainID_Testnet) + expChainIDNumber, err := eth.ParseEthChainID(eth.EIP155ChainID_Testnet) + s.Require().NoError(err) expChainID := (*hexutil.Big)(expChainIDNumber) testCases := []struct { name string @@ -136,7 +137,7 @@ func (s *BackendSuite) TestChainId() { } func (s *BackendSuite) TestGetCoinbase() { - validatorAcc := sdk.AccAddress(evmtest.NewEthAddr().Bytes()) + validatorAcc := sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()) testCases := []struct { name string registerMock func() @@ -328,7 +329,7 @@ func (s *BackendSuite) TestFeeHistory() { GasUsedRatio: []float64{0}, Reward: [][]*hexutil.Big{{(*hexutil.Big)(big.NewInt(0)), (*hexutil.Big)(big.NewInt(0)), (*hexutil.Big)(big.NewInt(0)), (*hexutil.Big)(big.NewInt(0))}}, }, - validator: sdk.AccAddress(evmtest.NewEthAddr().Bytes()), + validator: sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()), expPass: true, }, } diff --git a/eth/rpc/backend/evm_query_client_test.go b/eth/rpc/backend/evm_query_client_test.go index 112cc8a07..974694dc9 100644 --- a/eth/rpc/backend/evm_query_client_test.go +++ b/eth/rpc/backend/evm_query_client_test.go @@ -34,7 +34,10 @@ import ( // To use a mock method it has to be registered in a given test. var _ evm.QueryClient = &mocks.EVMQueryClient{} -var TEST_CHAIN_ID_NUMBER = eth.ParseEIP155ChainIDNumber(eth.EIP155ChainID_Testnet).Int64() +func TEST_CHAIN_ID_NUMBER() int64 { + n, _ := eth.ParseEthChainID(eth.EIP155ChainID_Testnet) + return n.Int64() +} // TraceTransaction func RegisterTraceTransactionWithPredecessors( @@ -42,7 +45,7 @@ func RegisterTraceTransactionWithPredecessors( ) { data := []byte{0x7b, 0x22, 0x74, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x7d} queryClient.On("TraceTx", rpc.NewContextWithHeight(1), - &evm.QueryTraceTxRequest{Msg: msgEthTx, BlockNumber: 1, Predecessors: predecessors, ChainId: TEST_CHAIN_ID_NUMBER, BlockMaxGas: -1}). + &evm.QueryTraceTxRequest{Msg: msgEthTx, BlockNumber: 1, Predecessors: predecessors, ChainId: TEST_CHAIN_ID_NUMBER(), BlockMaxGas: -1}). Return(&evm.QueryTraceTxResponse{Data: data}, nil) } @@ -50,14 +53,14 @@ func RegisterTraceTransaction( queryClient *mocks.EVMQueryClient, msgEthTx *evm.MsgEthereumTx, ) { data := []byte{0x7b, 0x22, 0x74, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x7d} - queryClient.On("TraceTx", rpc.NewContextWithHeight(1), &evm.QueryTraceTxRequest{Msg: msgEthTx, BlockNumber: 1, ChainId: TEST_CHAIN_ID_NUMBER, BlockMaxGas: -1}). + queryClient.On("TraceTx", rpc.NewContextWithHeight(1), &evm.QueryTraceTxRequest{Msg: msgEthTx, BlockNumber: 1, ChainId: TEST_CHAIN_ID_NUMBER(), BlockMaxGas: -1}). Return(&evm.QueryTraceTxResponse{Data: data}, nil) } func RegisterTraceTransactionError( queryClient *mocks.EVMQueryClient, msgEthTx *evm.MsgEthereumTx, ) { - queryClient.On("TraceTx", rpc.NewContextWithHeight(1), &evm.QueryTraceTxRequest{Msg: msgEthTx, BlockNumber: 1, ChainId: TEST_CHAIN_ID_NUMBER}). + queryClient.On("TraceTx", rpc.NewContextWithHeight(1), &evm.QueryTraceTxRequest{Msg: msgEthTx, BlockNumber: 1, ChainId: TEST_CHAIN_ID_NUMBER()}). Return(nil, errortypes.ErrInvalidRequest) } @@ -67,7 +70,7 @@ func RegisterTraceBlock( ) { data := []byte{0x7b, 0x22, 0x74, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x22, 0x7d} queryClient.On("TraceBlock", rpc.NewContextWithHeight(1), - &evm.QueryTraceBlockRequest{Txs: txs, BlockNumber: 1, TraceConfig: &evm.TraceConfig{}, ChainId: TEST_CHAIN_ID_NUMBER, BlockMaxGas: -1}). + &evm.QueryTraceBlockRequest{Txs: txs, BlockNumber: 1, TraceConfig: &evm.TraceConfig{}, ChainId: TEST_CHAIN_ID_NUMBER(), BlockMaxGas: -1}). Return(&evm.QueryTraceBlockResponse{Data: data}, nil) } @@ -245,7 +248,7 @@ func RegisterValidatorAccountError(queryClient *mocks.EVMQueryClient) { func TestRegisterValidatorAccount(t *testing.T) { queryClient := mocks.NewEVMQueryClient(t) - validator := sdk.AccAddress(evmtest.NewEthAddr().Bytes()) + validator := sdk.AccAddress(evmtest.NewEthAccInfo().EthAddr.Bytes()) RegisterValidatorAccount(queryClient, validator) res, err := queryClient.ValidatorAccount(rpc.NewContextWithHeight(1), &evm.QueryValidatorAccountRequest{}) require.Equal(t, &evm.QueryValidatorAccountResponse{AccountAddress: validator.String()}, res) diff --git a/eth/rpc/backend/sign_tx_test.go b/eth/rpc/backend/sign_tx_test.go index 7141bf5bb..9d4c80b5a 100644 --- a/eth/rpc/backend/sign_tx_test.go +++ b/eth/rpc/backend/sign_tx_test.go @@ -24,7 +24,7 @@ func (s *BackendSuite) TestSendTransaction() { gasPrice := new(hexutil.Big) gas := hexutil.Uint64(1) zeroGas := hexutil.Uint64(0) - toAddr := evmtest.NewEthAddr() + toAddr := evmtest.NewEthAccInfo().EthAddr priv, _ := ethsecp256k1.GenerateKey() from := common.BytesToAddress(priv.PubKey().Address().Bytes()) nonce := hexutil.Uint64(1) @@ -145,7 +145,9 @@ func (s *BackendSuite) TestSendTransaction() { } func (s *BackendSuite) TestSign() { - from, priv := evmtest.PrivKeyEth() + ethAcc := evmtest.NewEthAccInfo() + from, priv := ethAcc.EthAddr, ethAcc.PrivKey + testCases := []struct { name string registerMock func() @@ -192,7 +194,8 @@ func (s *BackendSuite) TestSign() { } func (s *BackendSuite) TestSignTypedData() { - from, priv := evmtest.PrivKeyEth() + ethAcc := evmtest.NewEthAccInfo() + from, priv := ethAcc.EthAddr, ethAcc.PrivKey testCases := []struct { name string registerMock func() diff --git a/eth/rpc/rpcapi/eth_api_test.go b/eth/rpc/rpcapi/eth_api_test.go new file mode 100644 index 000000000..a34aae621 --- /dev/null +++ b/eth/rpc/rpcapi/eth_api_test.go @@ -0,0 +1,319 @@ +package rpcapi_test + +import ( + "context" + "crypto/ecdsa" + "math/big" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + geth "github.com/ethereum/go-ethereum" + gethcommon "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/params" + + "github.com/NibiruChain/nibiru/app/appconst" + + nibirucommon "github.com/NibiruChain/nibiru/x/common" + "github.com/NibiruChain/nibiru/x/common/denoms" + "github.com/NibiruChain/nibiru/x/evm/evmtest" + + "github.com/stretchr/testify/suite" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/x/common/testutil" + testutilcli "github.com/NibiruChain/nibiru/x/common/testutil/cli" + "github.com/NibiruChain/nibiru/x/common/testutil/genesis" + "github.com/NibiruChain/nibiru/x/common/testutil/testapp" +) + +type IntegrationSuite struct { + suite.Suite + cfg testutilcli.Config + network *testutilcli.Network + + ethClient *ethclient.Client + + fundedAccPrivateKey *ecdsa.PrivateKey + fundedAccEthAddr gethcommon.Address + fundedAccNibiAddr sdk.AccAddress + + contractData evmtest.CompiledEvmContract +} + +func TestSuite_IntegrationSuite_RunAll(t *testing.T) { + suite.Run(t, new(IntegrationSuite)) +} + +// SetupSuite initialize network +func (s *IntegrationSuite) SetupSuite() { + testutil.BeforeIntegrationSuite(s.T()) + testapp.EnsureNibiruPrefix() + + genState := genesis.NewTestGenesisState(app.MakeEncodingConfig()) + homeDir := s.T().TempDir() + s.cfg = testutilcli.BuildNetworkConfig(genState) + network, err := testutilcli.New(s.T(), homeDir, s.cfg) + s.Require().NoError(err) + + s.network = network + s.ethClient = network.Validators[0].JSONRPCClient + + s.contractData = evmtest.SmartContract_FunToken.Load(s.T()) + + testAccPrivateKey, _ := crypto.GenerateKey() + s.fundedAccPrivateKey = testAccPrivateKey + s.fundedAccEthAddr = crypto.PubkeyToAddress(testAccPrivateKey.PublicKey) + s.fundedAccNibiAddr = evmtest.EthAddrToNibiruAddr(s.fundedAccEthAddr) + + val := s.network.Validators[0] + + funds := sdk.NewCoins(sdk.NewInt64Coin(denoms.NIBI, 100_000_000)) // 10 NIBI + s.NoError(testutilcli.FillWalletFromValidator(s.fundedAccNibiAddr, funds, val, denoms.NIBI)) + s.NoError(s.network.WaitForNextBlock()) +} + +// Test_ChainID EVM method: eth_chainId +func (s *IntegrationSuite) Test_ChainID() { + ethChainID, err := s.ethClient.ChainID(context.Background()) + s.NoError(err) + s.Equal(appconst.ETH_CHAIN_ID_DEFAULT, ethChainID.Int64()) +} + +// Test_BlockNumber EVM method: eth_blockNumber +func (s *IntegrationSuite) Test_BlockNumber() { + networkBlockNumber, err := s.network.LatestHeight() + s.NoError(err) + + ethBlockNumber, err := s.ethClient.BlockNumber(context.Background()) + s.NoError(err) + s.Equal(networkBlockNumber, int64(ethBlockNumber)) +} + +// Test_BlockByNumber EVM method: eth_getBlockByNumber +func (s *IntegrationSuite) Test_BlockByNumber() { + networkBlockNumber, err := s.network.LatestHeight() + s.NoError(err) + + ethBlock, err := s.ethClient.BlockByNumber(context.Background(), big.NewInt(networkBlockNumber)) + s.NoError(err) + + // TODO: add more checks about the eth block + s.NotNil(ethBlock) +} + +// Test_BalanceAt EVM method: eth_getBalance +func (s *IntegrationSuite) Test_BalanceAt() { + testAccEthAddr := gethcommon.BytesToAddress(testutilcli.NewAccount(s.network, "new-user")) + + // New user balance should be 0 + balance, err := s.ethClient.BalanceAt(context.Background(), testAccEthAddr, nil) + s.NoError(err) + s.NotNil(balance) + s.Equal(int64(0), balance.Int64()) + + // Funded account balance should be > 0 + balance, err = s.ethClient.BalanceAt(context.Background(), s.fundedAccEthAddr, nil) + s.NoError(err) + s.NotNil(balance) + s.Greater(balance.Int64(), int64(0)) +} + +// Test_StorageAt EVM method: eth_getStorageAt +func (s *IntegrationSuite) Test_StorageAt() { + storage, err := s.ethClient.StorageAt( + context.Background(), s.fundedAccEthAddr, gethcommon.Hash{}, nil, + ) + s.NoError(err) + // TODO: add more checks + s.NotNil(storage) +} + +// Test_PendingStorageAt EVM method: eth_getStorageAt | pending +func (s *IntegrationSuite) Test_PendingStorageAt() { + storage, err := s.ethClient.PendingStorageAt( + context.Background(), s.fundedAccEthAddr, gethcommon.Hash{}, + ) + s.NoError(err) + + // TODO: add more checks + s.NotNil(storage) +} + +// Test_CodeAt EVM method: eth_getCode +func (s *IntegrationSuite) Test_CodeAt() { + code, err := s.ethClient.CodeAt(context.Background(), s.fundedAccEthAddr, nil) + s.NoError(err) + + // TODO: add more checks + s.NotNil(code) +} + +// Test_PendingCodeAt EVM method: eth_getCode +func (s *IntegrationSuite) Test_PendingCodeAt() { + code, err := s.ethClient.PendingCodeAt(context.Background(), s.fundedAccEthAddr) + s.NoError(err) + + // TODO: add more checks + s.NotNil(code) +} + +// Test_EstimateGas EVM method: eth_estimateGas +func (s *IntegrationSuite) Test_EstimateGas() { + testAccEthAddr := gethcommon.BytesToAddress(testutilcli.NewAccount(s.network, "new-user")) + gasLimit := uint64(21000) + msg := geth.CallMsg{ + From: s.fundedAccEthAddr, + To: &testAccEthAddr, + Gas: gasLimit, + Value: big.NewInt(1), + } + gasEstimated, err := s.ethClient.EstimateGas(context.Background(), msg) + s.NoError(err) + s.Equal(gasEstimated, gasLimit) +} + +// Test_SuggestGasPrice EVM method: eth_gasPrice +func (s *IntegrationSuite) Test_SuggestGasPrice() { + // TODO: the backend method is stubbed to 0 + _, err := s.ethClient.SuggestGasPrice(context.Background()) + s.NoError(err) +} + +// Test_SimpleTransferTransaction EVM method: eth_sendRawTransaction +func (s *IntegrationSuite) Test_SimpleTransferTransaction() { + chainID, err := s.ethClient.ChainID(context.Background()) + s.NoError(err) + nonce, err := s.ethClient.PendingNonceAt(context.Background(), s.fundedAccEthAddr) + s.NoError(err) + + senderBalanceBefore, err := s.ethClient.BalanceAt( + context.Background(), s.fundedAccEthAddr, nil, + ) + s.NoError(err) + recipientAddr := gethcommon.BytesToAddress(testutilcli.NewAccount(s.network, "recipient")) + recipientBalanceBefore, err := s.ethClient.BalanceAt(context.Background(), recipientAddr, nil) + s.NoError(err) + s.Equal(int64(0), recipientBalanceBefore.Int64()) + + amountToSend := big.NewInt(1000) + + signer := gethcore.LatestSignerForChainID(chainID) + tx, err := gethcore.SignNewTx( + s.fundedAccPrivateKey, + signer, + &gethcore.LegacyTx{ + Nonce: nonce, + To: &recipientAddr, + Value: amountToSend, + Gas: params.TxGas, + GasPrice: big.NewInt(1), + }) + s.NoError(err) + err = s.ethClient.SendTransaction(context.Background(), tx) + s.NoError(err) + s.NoError(s.network.WaitForNextBlock()) + + senderAmountAfter, err := s.ethClient.BalanceAt(context.Background(), s.fundedAccEthAddr, nil) + s.NoError(err) + + expectedSenderBalance := senderBalanceBefore.Sub(senderBalanceBefore, amountToSend) + expectedSenderBalance = expectedSenderBalance.Sub(senderBalanceBefore, big.NewInt(int64(params.TxGas))) + + s.Equal(expectedSenderBalance.Int64(), senderAmountAfter.Int64()) + + recipientBalanceAfter, err := s.ethClient.BalanceAt(context.Background(), recipientAddr, nil) + s.NoError(err) + s.Equal(amountToSend.Int64(), recipientBalanceAfter.Int64()) +} + +// Test_SmartContract includes contract deployment, query, execution +func (s *IntegrationSuite) Test_SmartContract() { + chainID, err := s.ethClient.ChainID(context.Background()) + s.NoError(err) + nonce, err := s.ethClient.NonceAt(context.Background(), s.fundedAccEthAddr, nil) + s.NoError(err) + + // Deploying contract + signer := gethcore.LatestSignerForChainID(chainID) + txData := s.contractData.Bytecode + tx, err := gethcore.SignNewTx( + s.fundedAccPrivateKey, + signer, + &gethcore.LegacyTx{ + Nonce: nonce, + Gas: 1_500_000, + GasPrice: big.NewInt(1), + Data: txData, + }) + s.NoError(err) + err = s.ethClient.SendTransaction(context.Background(), tx) + s.NoError(err) + s.NoError(s.network.WaitForNextBlock()) + hash := tx.Hash() + receipt, err := s.ethClient.TransactionReceipt(context.Background(), hash) + s.NoError(err) + contractAddress := receipt.ContractAddress + + // Querying contract: owner's balance should be 1000_000 tokens + ownerInitialBalance := (&big.Int{}).Mul(big.NewInt(1000_000), nibirucommon.TO_ATTO) + s.assertERC20Balance(contractAddress, s.fundedAccEthAddr, ownerInitialBalance) + + // Querying contract: recipient balance should be 0 + recipientAddr := gethcommon.BytesToAddress(testutilcli.NewAccount(s.network, "contract_recipient")) + s.assertERC20Balance(contractAddress, recipientAddr, big.NewInt(0)) + + // Execute contract: send 1000 anibi to recipient + sendAmount := (&big.Int{}).Mul(big.NewInt(1000), nibirucommon.TO_ATTO) + input, err := s.contractData.ABI.Pack("transfer", recipientAddr, sendAmount) + s.NoError(err) + nonce, err = s.ethClient.NonceAt(context.Background(), s.fundedAccEthAddr, nil) + s.NoError(err) + tx, err = gethcore.SignNewTx( + s.fundedAccPrivateKey, + signer, + &gethcore.LegacyTx{ + Nonce: nonce, + To: &contractAddress, + Gas: 1_500_000, + GasPrice: big.NewInt(1), + Data: input, + }) + s.NoError(err) + err = s.ethClient.SendTransaction(context.Background(), tx) + s.NoError(err) + s.NoError(s.network.WaitForNextBlock()) + + // Querying contract: owner's balance should be 999_000 tokens + ownerBalance := (&big.Int{}).Mul(big.NewInt(999_000), nibirucommon.TO_ATTO) + s.assertERC20Balance(contractAddress, s.fundedAccEthAddr, ownerBalance) + + // Querying contract: recipient balance should be 1000 tokens + recipientBalance := (&big.Int{}).Mul(big.NewInt(1000), nibirucommon.TO_ATTO) + s.assertERC20Balance(contractAddress, recipientAddr, recipientBalance) +} + +func (s *IntegrationSuite) TearDownSuite() { + s.T().Log("tearing down integration test suite") + s.network.Cleanup() +} + +func (s *IntegrationSuite) assertERC20Balance( + contractAddress gethcommon.Address, + userAddress gethcommon.Address, + expectedBalance *big.Int, +) { + input, err := s.contractData.ABI.Pack("balanceOf", userAddress) + s.NoError(err) + msg := geth.CallMsg{ + From: s.fundedAccEthAddr, + To: &contractAddress, + Data: input, + } + recipientBalanceBeforeBytes, err := s.ethClient.CallContract(context.Background(), msg, nil) + s.NoError(err) + balance := new(big.Int).SetBytes(recipientBalanceBeforeBytes) + s.Equal(expectedBalance.String(), balance.String()) +} diff --git a/eth/rpc/rpcapi/net_api.go b/eth/rpc/rpcapi/net_api.go index 9325a19ad..284ef7108 100644 --- a/eth/rpc/rpcapi/net_api.go +++ b/eth/rpc/rpcapi/net_api.go @@ -23,7 +23,7 @@ type NetAPI struct { // NewImplNetAPI creates an instance of the public Net Web3 API. func NewImplNetAPI(clientCtx client.Context) *NetAPI { // parse the chainID from a integer string - chainIDEpoch, err := eth.ParseChainID(clientCtx.ChainID) + chainIDEpoch, err := eth.ParseEthChainID(clientCtx.ChainID) if err != nil { panic(err) } diff --git a/eth/state_encoder_test.go b/eth/state_encoder_test.go index 837bc4fd6..02d5a8f89 100644 --- a/eth/state_encoder_test.go +++ b/eth/state_encoder_test.go @@ -34,15 +34,15 @@ func assertBijectiveValue[T any](t *testing.T, encoder collections.ValueEncoder[ require.NotEmpty(t, encoder.Name()) } -type SuiteEncoders struct { +type Suite struct { suite.Suite } -func TestSuiteEncoders_RunAll(t *testing.T) { - suite.Run(t, new(SuiteEncoders)) +func TestSuite_RunAll(t *testing.T) { + suite.Run(t, new(Suite)) } -func (s *SuiteEncoders) TestEncoderBytes() { +func (s *Suite) TestEncoderBytes() { testCases := []struct { name string value string @@ -61,7 +61,7 @@ func (s *SuiteEncoders) TestEncoderBytes() { } } -func (s *SuiteEncoders) TestEncoderEthAddr() { +func (s *Suite) TestEncoderEthAddr() { testCases := []struct { name string given eth.EthAddr diff --git a/geth b/geth deleted file mode 160000 index 7fb652f18..000000000 --- a/geth +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7fb652f186b09b81cce9977408e1aff744f4e3ef diff --git a/go.mod b/go.mod index c1dcd4e4b..4d23f2c32 100644 --- a/go.mod +++ b/go.mod @@ -115,6 +115,7 @@ require ( github.com/edsrzf/mmap-go v1.0.0 // indirect github.com/felixge/httpsnoop v1.0.2 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff // indirect github.com/getsentry/sentry-go v0.23.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/gin-gonic/gin v1.9.1 // indirect @@ -152,8 +153,10 @@ require ( github.com/hdevalence/ed25519consensus v0.1.0 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/huandu/skiplist v1.2.0 // indirect + github.com/huin/goupnp v1.0.3 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/klauspost/compress v1.16.7 // indirect @@ -191,6 +194,7 @@ require ( github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect diff --git a/go.sum b/go.sum index 3487c5f28..e9a0f66f0 100644 --- a/go.sum +++ b/go.sum @@ -543,6 +543,7 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= @@ -1188,6 +1189,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4 h1:Gb2Tyox57NRNuZ2d3rmvB3pcmbu7O1RS3m8WRx7ilrg= github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= @@ -1648,6 +1650,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/gosdk/export_test.go b/gosdk/export_test.go index 533afab68..70f4abd07 100644 --- a/gosdk/export_test.go +++ b/gosdk/export_test.go @@ -42,7 +42,7 @@ func CreateBlockchain(t *testing.T) (nibiru Blockchain, err error) { } val := network.Validators[0] - AbsorbServerConfig(cfg, val.AppConfig) + AbsorbServerConfig(cfg, &val.AppConfig.Config) AbsorbTmConfig(cfg, val.Ctx.Config) grpcConn, err := ConnectGrpcToVal(val) diff --git a/proto/eth/evm/v1/evm.proto b/proto/eth/evm/v1/evm.proto index 246f4d0b3..024d81bd5 100644 --- a/proto/eth/evm/v1/evm.proto +++ b/proto/eth/evm/v1/evm.proto @@ -8,6 +8,7 @@ option go_package = "github.com/NibiruChain/nibiru/x/evm"; // Params defines the EVM module parameters message Params { + option (gogoproto.equal) = true; // evm_denom represents the token denomination used to run the EVM state // transitions. string evm_denom = 1 [(gogoproto.moretags) = "yaml:\"evm_denom\""]; @@ -32,6 +33,7 @@ message Params { // ChainConfig defines the Ethereum ChainConfig parameters using *sdk.Int values // instead of *big.Int. message ChainConfig { + option (gogoproto.equal) = true; // homestead_block switch (nil no fork, 0 = already homestead) string homestead_block = 1 [(gogoproto.customtype) = "cosmossdk.io/math.Int", (gogoproto.moretags) = "yaml:\"homestead_block\""]; diff --git a/x/common/constants.go b/x/common/constants.go index 031803825..83376347d 100644 --- a/x/common/constants.go +++ b/x/common/constants.go @@ -1,6 +1,10 @@ package common -import sdk "github.com/cosmos/cosmos-sdk/types" +import ( + "math/big" + + sdk "github.com/cosmos/cosmos-sdk/types" +) const ( TreasuryPoolModuleAccount = "treasury_pool" @@ -10,6 +14,9 @@ const ( NIBIRU_TEAM = "nibi1l8dxzwz9d4peazcqjclnkj2mhvtj7mpnkqx85mg0ndrlhwrnh7gskkzg0v" ) +// TO_ATTO eth multiplier +var TO_ATTO = big.NewInt(1e18) + func NibiruTeamAddr() sdk.AccAddress { return sdk.MustAccAddressFromBech32(NIBIRU_TEAM) } diff --git a/x/common/error.go b/x/common/error.go index 2f640ce50..de3e4acac 100644 --- a/x/common/error.go +++ b/x/common/error.go @@ -184,9 +184,7 @@ func CombineErrorsFromStrings(strs ...string) (err error) { return CombineErrors(errs...) } -func ErrNilGrpcMsg() error { - return grpcstatus.Errorf(grpccodes.InvalidArgument, "nil msg") -} +var ErrNilGrpcMsg = grpcstatus.Errorf(grpccodes.InvalidArgument, "nil msg") // ErrNotImplemented: Represents an function error value. func ErrNotImplemented() error { return fmt.Errorf("fn not implemented yet") } diff --git a/x/common/testutil/cli/network.go b/x/common/testutil/cli/network.go index 4987ec944..d43e264eb 100644 --- a/x/common/testutil/cli/network.go +++ b/x/common/testutil/cli/network.go @@ -14,6 +14,12 @@ import ( "sync" "time" + srvconfig "github.com/cosmos/cosmos-sdk/server/config" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + serverconfig "github.com/NibiruChain/nibiru/app/server/config" + "github.com/cometbft/cometbft/libs/log" "github.com/cosmos/cosmos-sdk/store/pruning/types" "github.com/cosmos/cosmos-sdk/testutil/sims" @@ -33,7 +39,6 @@ import ( cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" "github.com/cosmos/cosmos-sdk/server" serverapi "github.com/cosmos/cosmos-sdk/server/api" - serverconfig "github.com/cosmos/cosmos-sdk/server/config" servertypes "github.com/cosmos/cosmos-sdk/server/types" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" @@ -111,6 +116,9 @@ type ( // Address - account address Address sdk.AccAddress + // EthAddress - Ethereum address + EthAddress common.Address + // ValAddress - validator operator (valoper) address ValAddress sdk.ValAddress @@ -122,6 +130,8 @@ type ( // - rpc.Local RPCClient tmclient.Client + JSONRPCClient *ethclient.Client + tmNode *node.Node // API exposes the app's REST and gRPC interfaces, allowing clients to @@ -131,6 +141,8 @@ type ( grpc *grpc.Server grpcWeb *http.Server secretMnemonic string + jsonrpc *http.Server + jsonrpcDone chan struct{} } ) @@ -276,6 +288,18 @@ func New(logger Logger, baseDir string, cfg Config) (*Network, error) { } appCfg.GRPCWeb.Address = fmt.Sprintf("0.0.0.0:%s", grpcWebPort) appCfg.GRPCWeb.Enable = true + + if cfg.JSONRPCAddress != "" { + appCfg.JSONRPC.Address = cfg.JSONRPCAddress + } else { + _, jsonRPCPort, err := server.FreeTCPAddr() + if err != nil { + return nil, err + } + appCfg.JSONRPC.Address = fmt.Sprintf("0.0.0.0:%s", jsonRPCPort) + } + appCfg.JSONRPC.Enable = true + appCfg.JSONRPC.API = serverconfig.GetAPINamespaces() } loggerNoOp := log.NewNopLogger() @@ -344,6 +368,8 @@ func New(logger Logger, baseDir string, cfg Config) (*Network, error) { } addr, secret, err := sdktestutil.GenerateSaveCoinKey(kb, nodeDirName, mnemonic, true, algo) + ethAddr := common.BytesToAddress(addr.Bytes()) + if err != nil { return nil, err } @@ -425,7 +451,7 @@ func New(logger Logger, baseDir string, cfg Config) (*Network, error) { return nil, err } - serverconfig.WriteConfigFile(filepath.Join(nodeDir, "config", "app.toml"), appCfg) + srvconfig.WriteConfigFile(filepath.Join(nodeDir, "config", "app.toml"), appCfg) clientCtx := client.Context{}. WithKeyringDir(clientDir). @@ -450,6 +476,7 @@ func New(logger Logger, baseDir string, cfg Config) (*Network, error) { P2PAddress: tmCfg.P2P.ListenAddress, APIAddress: apiAddr, Address: addr, + EthAddress: ethAddr, ValAddress: sdk.ValAddress(addr), secretMnemonic: secret, } @@ -619,6 +646,9 @@ func (n *Network) Cleanup() { _ = v.grpcWeb.Close() } } + if v.jsonrpc != nil { + _ = v.jsonrpc.Close() + } } // Give a brief pause for things to finish closing in other processes. diff --git a/x/common/testutil/cli/network_config.go b/x/common/testutil/cli/network_config.go index 91fc799c6..80b452479 100644 --- a/x/common/testutil/cli/network_config.go +++ b/x/common/testutil/cli/network_config.go @@ -5,14 +5,14 @@ import ( "time" sdkmath "cosmossdk.io/math" - tmconfig "github.com/cometbft/cometbft/config" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/crypto/keyring" - serverconfig "github.com/cosmos/cosmos-sdk/server/config" sdk "github.com/cosmos/cosmos-sdk/types" + + serverconfig "github.com/NibiruChain/nibiru/app/server/config" ) // Config: Defines the parameters needed to start a local test network. @@ -44,6 +44,7 @@ type Config struct { RPCAddress string // RPC listen address (including port) APIAddress string // REST API listen address (including port) GRPCAddress string // GRPC server listen address (including port) + JSONRPCAddress string // JSON-RPC listen address (including port) } func (cfg *Config) AbsorbServerConfig(srvCfg *serverconfig.Config) { diff --git a/x/common/testutil/cli/util.go b/x/common/testutil/cli/util.go index bd018371a..272c4c58c 100644 --- a/x/common/testutil/cli/util.go +++ b/x/common/testutil/cli/util.go @@ -8,6 +8,11 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/NibiruChain/nibiru/app/server" + tmtypes "github.com/cometbft/cometbft/abci/types" sdkcodec "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/hd" @@ -96,7 +101,7 @@ func startInProcess(cfg Config, val *Validator) error { errCh := make(chan error) go func() { - if err := apiSrv.Start(*val.AppConfig); err != nil { + if err := apiSrv.Start(val.AppConfig.Config); err != nil { errCh <- err } }() @@ -119,12 +124,32 @@ func startInProcess(cfg Config, val *Validator) error { val.grpc = grpcSrv if val.AppConfig.GRPCWeb.Enable { - val.grpcWeb, err = servergrpc.StartGRPCWeb(grpcSrv, *val.AppConfig) + val.grpcWeb, err = servergrpc.StartGRPCWeb(grpcSrv, val.AppConfig.Config) if err != nil { return err } } } + if val.AppConfig.JSONRPC.Enable && val.AppConfig.JSONRPC.Address != "" { + if val.Ctx == nil || val.Ctx.Viper == nil { + return fmt.Errorf("validator %s context is nil", val.Moniker) + } + + tmEndpoint := "/websocket" + tmRPCAddr := fmt.Sprintf("tcp://%s", val.AppConfig.GRPC.Address) + + val.jsonrpc, val.jsonrpcDone, err = server.StartJSONRPC(val.Ctx, val.ClientCtx, tmRPCAddr, tmEndpoint, val.AppConfig, nil) + if err != nil { + return err + } + + address := fmt.Sprintf("http://%s", val.AppConfig.JSONRPC.Address) + + val.JSONRPCClient, err = ethclient.Dial(address) + if err != nil { + return fmt.Errorf("failed to dial JSON-RPC at %s: %w", val.AppConfig.JSONRPC.Address, err) + } + } return nil } @@ -284,6 +309,11 @@ func NewAccount(network *Network, uid string) sdk.AccAddress { return addr } +func NewEthAccount(network *Network, uid string) common.Address { + addr := NewAccount(network, uid) + return common.BytesToAddress(addr.Bytes()) +} + func NewKeyring(t *testing.T) ( kring keyring.Keyring, algo keyring.SignatureAlgo, diff --git a/x/common/testutil/testapp/testapp.go b/x/common/testutil/testapp/testapp.go index 94280dfc8..f77c3bfbe 100644 --- a/x/common/testutil/testapp/testapp.go +++ b/x/common/testutil/testapp/testapp.go @@ -62,10 +62,17 @@ func NewNibiruTestAppAndContext() (*app.NibiruApp, sdk.Context) { // NewContext: Returns a fresh sdk.Context corresponding to the given NibiruApp. func NewContext(nibiru *app.NibiruApp) sdk.Context { - return nibiru.NewContext(false, tmproto.Header{ + blockHeader := tmproto.Header{ Height: 1, Time: time.Now().UTC(), - }) + } + ctx := nibiru.NewContext(false, blockHeader) + + // Make sure there's a block proposer on the context. + blockHeader.ProposerAddress = FirstBlockProposer(nibiru, ctx) + ctx = ctx.WithBlockHeader(blockHeader) + + return ctx } // DefaultSudoers: State for the x/sudo module for the default test app. @@ -81,6 +88,15 @@ func DefaultSudoRoot() sdk.AccAddress { return sdk.MustAccAddressFromBech32(testutil.ADDR_SUDO_ROOT) } +func FirstBlockProposer( + chain *app.NibiruApp, ctx sdk.Context, +) (proposerAddr sdk.ConsAddress) { + maxQueryCount := uint32(10) + valopers := chain.StakingKeeper.GetValidators(ctx, maxQueryCount) + valAddrBz := valopers[0].GetOperator().Bytes() + return sdk.ConsAddress(valAddrBz) +} + // SetDefaultSudoGenesis: Sets the sudo module genesis state to a valid // default. See "DefaultSudoers". func SetDefaultSudoGenesis(gen app.GenesisState) { diff --git a/x/evm/deps.go b/x/evm/deps.go index b76eb37ac..1dab77147 100644 --- a/x/evm/deps.go +++ b/x/evm/deps.go @@ -5,6 +5,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + gethcore "github.com/ethereum/go-ethereum/core" + gethcoretypes "github.com/ethereum/go-ethereum/core/types" ) // AccountKeeper defines the expected account keeper interface @@ -34,3 +36,14 @@ type StakingKeeper interface { GetHistoricalInfo(ctx sdk.Context, height int64) (stakingtypes.HistoricalInfo, bool) GetValidatorByConsAddr(ctx sdk.Context, consAddr sdk.ConsAddress) (validator stakingtypes.Validator, found bool) } + +// EvmHooks: Ethereum transaction processing callbacks/hooks. +type EvmHooks interface { + // PostTxProcessing: Called after default tx processing. If the hook errors, + // the tx is reverted. + PostTxProcessing( + ctx sdk.Context, + msg gethcore.Message, + receipt *gethcoretypes.Receipt, + ) error +} diff --git a/x/evm/evm.pb.go b/x/evm/evm.pb.go index dcb320fab..5d51c7afe 100644 --- a/x/evm/evm.pb.go +++ b/x/evm/evm.pb.go @@ -708,113 +708,319 @@ func init() { func init() { proto.RegisterFile("eth/evm/v1/evm.proto", fileDescriptor_98abbdadb327b7d0) } var fileDescriptor_98abbdadb327b7d0 = []byte{ - // 1652 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x57, 0xcd, 0x6f, 0xdb, 0xc8, - 0x15, 0xb7, 0x2d, 0xda, 0xa6, 0x86, 0xb2, 0x44, 0x8f, 0x15, 0x47, 0x9b, 0x00, 0xa6, 0xc1, 0xbd, - 0xf8, 0xb0, 0x6b, 0xc5, 0x4e, 0xdd, 0x0d, 0xb6, 0xd8, 0x16, 0x61, 0xe2, 0x6d, 0xed, 0x66, 0xd3, - 0x60, 0xe2, 0xed, 0xa2, 0x45, 0x01, 0x62, 0x44, 0xce, 0x52, 0x5c, 0x93, 0x1c, 0x61, 0x66, 0xa8, - 0x48, 0xbd, 0x17, 0xe8, 0xb1, 0xe7, 0x9e, 0xfa, 0xe7, 0x2c, 0x7a, 0xda, 0x63, 0xd1, 0x03, 0x51, - 0x38, 0x40, 0x0f, 0x3e, 0xfa, 0x5e, 0xa0, 0x98, 0x0f, 0x7d, 0xc6, 0x31, 0x7c, 0xd2, 0xfc, 0xde, - 0xc7, 0xef, 0xcd, 0x7b, 0xf3, 0xa8, 0x37, 0x03, 0xda, 0x44, 0xf4, 0xbb, 0x64, 0x98, 0x77, 0x87, - 0x47, 0xf2, 0xe7, 0x70, 0xc0, 0xa8, 0xa0, 0x10, 0x10, 0xd1, 0x3f, 0x94, 0x70, 0x78, 0xf4, 0xa8, - 0x9d, 0xd0, 0x84, 0x2a, 0x71, 0x57, 0xae, 0xb4, 0x85, 0xff, 0xdf, 0x1a, 0xd8, 0x78, 0x83, 0x19, - 0xce, 0x39, 0x3c, 0x02, 0x75, 0x32, 0xcc, 0xc3, 0x98, 0x14, 0x34, 0xef, 0xac, 0xee, 0xaf, 0x1e, - 0xd4, 0x83, 0xf6, 0x4d, 0xe5, 0xb9, 0x63, 0x9c, 0x67, 0x5f, 0xfa, 0x53, 0x95, 0x8f, 0x6c, 0x32, - 0xcc, 0x5f, 0xca, 0x25, 0xfc, 0x0a, 0x6c, 0x91, 0x02, 0xf7, 0x32, 0x12, 0x46, 0x8c, 0x60, 0x41, - 0x3a, 0x6b, 0xfb, 0xab, 0x07, 0x76, 0xd0, 0xb9, 0xa9, 0xbc, 0xb6, 0x71, 0x9b, 0x57, 0xfb, 0xa8, - 0xa1, 0xf1, 0x0b, 0x05, 0xe1, 0x17, 0xc0, 0x99, 0xe8, 0x71, 0x96, 0x75, 0x6a, 0xca, 0x79, 0xf7, - 0xa6, 0xf2, 0xe0, 0xa2, 0x33, 0xce, 0x32, 0x1f, 0x01, 0xe3, 0x8a, 0xb3, 0x0c, 0x3e, 0x07, 0x80, - 0x8c, 0x04, 0xc3, 0x21, 0x49, 0x07, 0xbc, 0x63, 0xed, 0xd7, 0x0e, 0x6a, 0x81, 0x7f, 0x55, 0x79, - 0xf5, 0x53, 0x29, 0x3d, 0x3d, 0x7b, 0xc3, 0x6f, 0x2a, 0x6f, 0xdb, 0x90, 0x4c, 0x0d, 0x7d, 0x54, - 0x57, 0xe0, 0x34, 0x1d, 0x70, 0xf8, 0x1d, 0x68, 0x44, 0x7d, 0x9c, 0x16, 0x61, 0x44, 0x8b, 0xef, - 0xd3, 0xa4, 0xb3, 0xbe, 0xbf, 0x7a, 0xe0, 0x1c, 0x3f, 0x3c, 0x9c, 0x55, 0xec, 0xf0, 0x85, 0xd4, - 0xbf, 0x50, 0xea, 0xe0, 0xf1, 0x8f, 0x95, 0xb7, 0x72, 0x53, 0x79, 0x3b, 0x9a, 0x74, 0xde, 0xd5, - 0x47, 0x4e, 0x34, 0xb3, 0x84, 0xc7, 0xe0, 0x01, 0xce, 0x32, 0xfa, 0x2e, 0x2c, 0x0b, 0x59, 0x62, - 0x12, 0x09, 0x12, 0x87, 0x62, 0xc4, 0x3b, 0x1b, 0x32, 0x3d, 0xb4, 0xa3, 0x94, 0xdf, 0xce, 0x74, - 0x17, 0x23, 0x0e, 0x3f, 0x07, 0x10, 0x47, 0x22, 0x1d, 0x92, 0x70, 0xc0, 0x48, 0x44, 0xf3, 0x41, - 0x9a, 0x11, 0xde, 0xd9, 0xdc, 0xaf, 0x1d, 0xd4, 0xd1, 0xb6, 0xd6, 0xbc, 0x99, 0x29, 0xe0, 0x31, - 0x68, 0xc8, 0xe3, 0x88, 0xfa, 0xb8, 0x28, 0x48, 0xc6, 0x3b, 0xb6, 0x34, 0x0c, 0x5a, 0x57, 0x95, - 0xe7, 0x9c, 0xfe, 0xfe, 0x9b, 0x17, 0x46, 0x8c, 0x1c, 0x32, 0xcc, 0x27, 0xc0, 0xff, 0x5f, 0x13, - 0x38, 0x73, 0x09, 0xc1, 0x3f, 0x81, 0x56, 0x9f, 0xe6, 0x84, 0x0b, 0x82, 0xe3, 0xb0, 0x97, 0xd1, - 0xe8, 0xd2, 0x9c, 0xf9, 0xd3, 0x7f, 0x57, 0xde, 0x83, 0x88, 0xf2, 0x9c, 0x72, 0x1e, 0x5f, 0x1e, - 0xa6, 0xb4, 0x9b, 0x63, 0xd1, 0x3f, 0x3c, 0x2b, 0xc4, 0x4d, 0xe5, 0xed, 0xea, 0xf4, 0x97, 0x3c, - 0x7d, 0xd4, 0x9c, 0x4a, 0x02, 0x29, 0x80, 0x7d, 0xd0, 0x8c, 0x31, 0x0d, 0xbf, 0xa7, 0xec, 0xd2, - 0x90, 0xaf, 0x29, 0xf2, 0xe0, 0xa3, 0xe4, 0x57, 0x95, 0xd7, 0x78, 0xf9, 0xfc, 0x77, 0x5f, 0x53, - 0x76, 0xa9, 0x28, 0x6e, 0x2a, 0xef, 0x81, 0x0e, 0xb6, 0x48, 0xe4, 0xa3, 0x46, 0x8c, 0xe9, 0xd4, - 0x0c, 0x7e, 0x07, 0xdc, 0xa9, 0x01, 0x2f, 0x07, 0x03, 0xca, 0x84, 0x69, 0xa4, 0xcf, 0xaf, 0x2a, - 0xaf, 0x69, 0x28, 0xdf, 0x6a, 0xcd, 0x4d, 0xe5, 0x3d, 0x5c, 0x22, 0x35, 0x3e, 0x3e, 0x6a, 0x1a, - 0x5a, 0x63, 0x0a, 0x7b, 0xa0, 0x41, 0xd2, 0xc1, 0xd1, 0xc9, 0x13, 0x93, 0x80, 0xa5, 0x12, 0xf8, - 0xd5, 0x5d, 0x09, 0x38, 0xa7, 0x67, 0x6f, 0x8e, 0x4e, 0x9e, 0x4c, 0xf6, 0x6f, 0x7a, 0x65, 0x9e, - 0xc5, 0x47, 0x8e, 0x86, 0x7a, 0xf3, 0x67, 0xc0, 0xc0, 0xb0, 0x8f, 0x79, 0x5f, 0xf5, 0x60, 0x3d, - 0x38, 0xb8, 0xaa, 0x3c, 0xa0, 0x99, 0x7e, 0x83, 0x79, 0x7f, 0x56, 0xf5, 0xde, 0xf8, 0xcf, 0xb8, - 0x10, 0x69, 0x99, 0x4f, 0xb8, 0x80, 0x76, 0x96, 0x56, 0xd3, 0xed, 0x9e, 0x98, 0xed, 0x6e, 0xdc, - 0x77, 0xbb, 0x27, 0xb7, 0x6d, 0xf7, 0x64, 0x71, 0xbb, 0xda, 0x66, 0x1a, 0xe3, 0x99, 0x89, 0xb1, - 0x79, 0xdf, 0x18, 0xcf, 0x6e, 0x8b, 0xf1, 0x6c, 0x31, 0x86, 0xb6, 0x91, 0x7d, 0xb9, 0x94, 0x67, - 0xc7, 0xbe, 0x77, 0x5f, 0x7e, 0x50, 0xa1, 0xe6, 0x54, 0xa2, 0xd9, 0x2f, 0x41, 0x3b, 0xa2, 0x05, - 0x17, 0x52, 0x56, 0xd0, 0x41, 0x46, 0x4c, 0x88, 0xba, 0x0a, 0xf1, 0xec, 0xae, 0x10, 0x8f, 0xcd, - 0x97, 0x7f, 0x8b, 0xbb, 0x8f, 0x76, 0x16, 0xc5, 0x3a, 0x58, 0x08, 0xdc, 0x01, 0x11, 0x84, 0xf1, - 0x5e, 0xc9, 0x12, 0x13, 0x08, 0xa8, 0x40, 0x3f, 0xbb, 0x2b, 0x90, 0xe9, 0xd0, 0x65, 0x57, 0x1f, - 0xb5, 0x66, 0x22, 0x1d, 0xe0, 0x0f, 0xa0, 0x99, 0xca, 0xa8, 0xbd, 0x32, 0x33, 0xf4, 0x8e, 0xa2, - 0x3f, 0xbe, 0x8b, 0xde, 0x7c, 0x55, 0x8b, 0x8e, 0x3e, 0xda, 0x9a, 0x08, 0x34, 0x75, 0x0c, 0x60, - 0x5e, 0xa6, 0x2c, 0x4c, 0x32, 0x1c, 0xa5, 0x84, 0x19, 0xfa, 0x86, 0xa2, 0xff, 0xf9, 0x5d, 0xf4, - 0x9f, 0x68, 0xfa, 0x0f, 0x9d, 0x7d, 0xe4, 0x4a, 0xe1, 0xaf, 0xb5, 0x4c, 0x47, 0x79, 0x0b, 0x1a, - 0x3d, 0xc2, 0xb2, 0xb4, 0x30, 0xfc, 0x5b, 0x8a, 0xff, 0xc9, 0x5d, 0xfc, 0xa6, 0x83, 0xe6, 0xdd, - 0x7c, 0xe4, 0x68, 0x38, 0x25, 0xcd, 0x68, 0x11, 0xd3, 0x09, 0xe9, 0xf6, 0xbd, 0x49, 0xe7, 0xdd, - 0x7c, 0xe4, 0x68, 0xa8, 0x49, 0x13, 0xb0, 0x83, 0x19, 0xa3, 0xef, 0x96, 0x0a, 0x02, 0x15, 0xf7, - 0x17, 0x77, 0x71, 0x3f, 0xd2, 0xdc, 0xb7, 0x78, 0xfb, 0x68, 0x5b, 0x49, 0x17, 0x4a, 0x12, 0x03, - 0x98, 0x30, 0x3c, 0x5e, 0x8a, 0xd3, 0xbe, 0x77, 0xe1, 0x3f, 0x74, 0xf6, 0x91, 0x2b, 0x85, 0x0b, - 0x51, 0x7e, 0x00, 0xed, 0x9c, 0xb0, 0x84, 0x84, 0x05, 0x11, 0x7c, 0x90, 0xa5, 0xc2, 0xc4, 0x79, - 0x70, 0xef, 0xef, 0xe0, 0x36, 0x77, 0x1f, 0x41, 0x25, 0x7e, 0x6d, 0xa4, 0xd3, 0x2e, 0xe5, 0x7d, - 0x5c, 0x24, 0x7d, 0x9c, 0x9a, 0x28, 0xbb, 0xf7, 0xee, 0xd2, 0x45, 0x47, 0x1f, 0x6d, 0x4d, 0x04, - 0xd3, 0xa3, 0x8e, 0x70, 0x11, 0x95, 0x93, 0xa3, 0x7e, 0x78, 0xef, 0xa3, 0x9e, 0x77, 0x93, 0x03, - 0x5c, 0x41, 0x45, 0x7a, 0x6e, 0xd9, 0x4d, 0xb7, 0x75, 0x6e, 0xd9, 0x2d, 0xd7, 0x3d, 0xb7, 0x6c, - 0xd7, 0xdd, 0x3e, 0xb7, 0xec, 0x1d, 0xb7, 0x8d, 0xb6, 0xc6, 0x34, 0xa3, 0xe1, 0xf0, 0xa9, 0x76, - 0x42, 0x0e, 0x79, 0x87, 0xb9, 0xf9, 0xa3, 0x41, 0xcd, 0x08, 0x0b, 0x9c, 0x8d, 0xb9, 0x29, 0x04, - 0x72, 0x75, 0x79, 0xe6, 0xc6, 0x56, 0x17, 0xac, 0xbf, 0x15, 0xf2, 0xd2, 0xe3, 0x82, 0xda, 0x25, - 0x19, 0xeb, 0x61, 0x8b, 0xe4, 0x12, 0xb6, 0xc1, 0xfa, 0x10, 0x67, 0xa5, 0xbe, 0x3d, 0xd5, 0x91, - 0x06, 0xfe, 0x39, 0x68, 0x5d, 0x30, 0x5c, 0x70, 0x39, 0xfe, 0x69, 0xf1, 0x8a, 0x26, 0x1c, 0x42, - 0x60, 0xa9, 0x39, 0xa1, 0x7d, 0xd5, 0x1a, 0x7e, 0x0a, 0xac, 0x8c, 0x26, 0xbc, 0xb3, 0xb6, 0x5f, - 0x3b, 0x70, 0x8e, 0x5b, 0xf3, 0xf7, 0x97, 0x57, 0x34, 0x41, 0x4a, 0xe9, 0xff, 0x73, 0x0d, 0xd4, - 0x5e, 0xd1, 0x04, 0x76, 0xc0, 0x26, 0x8e, 0x63, 0x46, 0x38, 0x37, 0x1c, 0x13, 0x08, 0x77, 0xc1, - 0x86, 0xa0, 0x83, 0x34, 0xd2, 0x44, 0x75, 0x64, 0x90, 0x0c, 0x19, 0x63, 0x81, 0xd5, 0x48, 0x6d, - 0x20, 0xb5, 0x96, 0xd7, 0x0f, 0x95, 0x53, 0x58, 0x94, 0x79, 0x8f, 0x30, 0x35, 0x19, 0xad, 0xa0, - 0x75, 0x5d, 0x79, 0x8e, 0x92, 0xbf, 0x56, 0x62, 0x34, 0x0f, 0xe0, 0x67, 0x60, 0x53, 0x8c, 0xe6, - 0xa7, 0xdc, 0xce, 0x75, 0xe5, 0xb5, 0xc4, 0x2c, 0x41, 0x39, 0xc4, 0xd0, 0x86, 0x18, 0xa9, 0x61, - 0xd6, 0x05, 0xb6, 0x18, 0x85, 0x69, 0x11, 0x93, 0x91, 0x1a, 0x64, 0x56, 0xd0, 0xbe, 0xae, 0x3c, - 0x77, 0xce, 0xfc, 0x4c, 0xea, 0xd0, 0xa6, 0x18, 0xa9, 0x05, 0xfc, 0x0c, 0x00, 0xbd, 0x25, 0x15, - 0x41, 0xcf, 0xa5, 0xad, 0xeb, 0xca, 0xab, 0x2b, 0xa9, 0xe2, 0x9e, 0x2d, 0xa1, 0x0f, 0xd6, 0x35, - 0xb7, 0xad, 0xb8, 0x1b, 0xd7, 0x95, 0x67, 0x67, 0x34, 0xd1, 0x9c, 0x5a, 0x25, 0x4b, 0xc5, 0x48, - 0x4e, 0x87, 0x24, 0x56, 0xc3, 0xc1, 0x46, 0x13, 0xe8, 0xff, 0x65, 0x0d, 0xd8, 0x17, 0x23, 0x44, - 0x78, 0x99, 0x09, 0xf8, 0x35, 0x70, 0x23, 0x5a, 0x08, 0x86, 0x23, 0x11, 0x2e, 0x94, 0x36, 0x78, - 0x3c, 0xfb, 0x2b, 0x5f, 0xb6, 0xf0, 0x51, 0x6b, 0x22, 0x7a, 0x6e, 0xea, 0xdf, 0x06, 0xeb, 0xbd, - 0x8c, 0xd2, 0x5c, 0xf5, 0x40, 0x03, 0x69, 0x00, 0x5f, 0xa9, 0xaa, 0xa9, 0xf3, 0xad, 0xa9, 0xfb, - 0xe9, 0xe3, 0xf9, 0xf3, 0x5d, 0x6a, 0x8f, 0x60, 0xd7, 0xdc, 0x51, 0x9b, 0x3a, 0xaa, 0xf1, 0xf4, - 0x65, 0x55, 0x55, 0xfb, 0xb8, 0xa0, 0xc6, 0x88, 0x50, 0xc7, 0xd5, 0x40, 0x72, 0x09, 0x1f, 0x01, - 0x9b, 0x91, 0x21, 0x61, 0x82, 0xc4, 0xea, 0x58, 0x6c, 0x34, 0xc5, 0xf0, 0x13, 0x60, 0x27, 0x98, - 0x87, 0x25, 0x27, 0xb1, 0x3e, 0x03, 0xb4, 0x99, 0x60, 0xfe, 0x2d, 0x27, 0xf1, 0x97, 0xd6, 0x5f, - 0xff, 0xe1, 0xad, 0xf8, 0x18, 0x38, 0xcf, 0xa3, 0x88, 0x70, 0x7e, 0x51, 0x0e, 0x32, 0x72, 0x47, - 0x6f, 0x1d, 0x83, 0x06, 0x17, 0x94, 0xe1, 0x84, 0x84, 0x97, 0x64, 0x6c, 0x3a, 0x4c, 0xf7, 0x8b, - 0x91, 0xff, 0x96, 0x8c, 0x39, 0x9a, 0x07, 0x26, 0xc4, 0xdf, 0x2d, 0xe0, 0x5c, 0x30, 0x1c, 0x11, - 0x73, 0x69, 0x95, 0x5d, 0x2a, 0x21, 0x33, 0x21, 0x0c, 0x92, 0xb1, 0x45, 0x9a, 0x13, 0x5a, 0x0a, - 0xf3, 0x0d, 0x4d, 0xa0, 0xf4, 0x60, 0x84, 0x8c, 0x48, 0xa4, 0x0a, 0x68, 0x21, 0x83, 0xe0, 0x09, - 0xd8, 0x8a, 0x53, 0xae, 0x9e, 0x17, 0x5c, 0xe0, 0xe8, 0x52, 0xa7, 0x1f, 0xb8, 0xd7, 0x95, 0xd7, - 0x30, 0x8a, 0xb7, 0x52, 0x8e, 0x16, 0x10, 0xfc, 0x05, 0x68, 0xcd, 0xdc, 0xd4, 0x6e, 0xf5, 0xb5, - 0x3e, 0x80, 0xd7, 0x95, 0xd7, 0x9c, 0x9a, 0x2a, 0x0d, 0x5a, 0xc2, 0xf2, 0x8c, 0x63, 0xd2, 0x2b, - 0x13, 0xd5, 0x76, 0x36, 0xd2, 0x40, 0x4a, 0xb3, 0x34, 0x4f, 0x85, 0x6a, 0xb3, 0x75, 0xa4, 0x01, - 0x3c, 0x01, 0x75, 0x3a, 0x24, 0x8c, 0xa5, 0x31, 0xe1, 0xea, 0xd2, 0xf0, 0xf1, 0xb7, 0x09, 0x9a, - 0x59, 0xca, 0xb4, 0xcc, 0xa3, 0x29, 0x27, 0x39, 0x65, 0x63, 0x75, 0x21, 0x30, 0x69, 0x69, 0xc5, - 0x37, 0x4a, 0x8e, 0x16, 0x10, 0x0c, 0x00, 0x34, 0x6e, 0x8c, 0x88, 0x92, 0x15, 0xa1, 0xfa, 0xe6, - 0x1b, 0xca, 0x57, 0x7d, 0x79, 0x5a, 0x8b, 0x94, 0xf2, 0x25, 0x16, 0x18, 0x7d, 0x20, 0x81, 0xbf, - 0x04, 0x50, 0x9f, 0x46, 0xf8, 0x03, 0xa7, 0xd3, 0x67, 0x95, 0x9e, 0xe8, 0x2a, 0xbe, 0xd6, 0x9a, - 0x3d, 0xbb, 0x1a, 0x9d, 0x73, 0x6a, 0xb2, 0x38, 0xb7, 0x6c, 0xcb, 0x5d, 0x3f, 0xb7, 0xec, 0x4d, - 0xd7, 0x9e, 0x56, 0xce, 0x64, 0x81, 0x76, 0x26, 0x78, 0x6e, 0x7b, 0xc1, 0x57, 0x3f, 0x5e, 0xed, - 0xad, 0xfe, 0x74, 0xb5, 0xb7, 0xfa, 0x9f, 0xab, 0xbd, 0xd5, 0xbf, 0xbd, 0xdf, 0x5b, 0xf9, 0xe9, - 0xfd, 0xde, 0xca, 0xbf, 0xde, 0xef, 0xad, 0xfc, 0xf1, 0xd3, 0x24, 0x15, 0xfd, 0xb2, 0x77, 0x18, - 0xd1, 0xbc, 0xfb, 0x3a, 0xed, 0xa5, 0xac, 0x54, 0xe5, 0xea, 0x16, 0x6a, 0xdd, 0x1d, 0xc9, 0x17, - 0x72, 0x6f, 0x43, 0x3d, 0x80, 0x9f, 0xfe, 0x3f, 0x00, 0x00, 0xff, 0xff, 0xe2, 0x48, 0xfd, 0xff, - 0x3a, 0x0f, 0x00, 0x00, + // 1663 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x57, 0xcf, 0x6f, 0xdb, 0xc8, + 0x15, 0xb6, 0x2d, 0xda, 0xa6, 0x46, 0xb2, 0x44, 0x8f, 0x15, 0x47, 0x9b, 0x00, 0xa6, 0xc1, 0xbd, + 0xf8, 0xb0, 0x6b, 0xc5, 0x4e, 0xdd, 0x0d, 0x52, 0x6c, 0x8b, 0x30, 0xf1, 0xb6, 0x76, 0xb3, 0x69, + 0x30, 0xf1, 0x76, 0xd1, 0xa2, 0x00, 0x31, 0x22, 0x67, 0x29, 0xae, 0x49, 0x8e, 0x30, 0x33, 0x54, + 0xa4, 0x5e, 0x8b, 0x02, 0x3d, 0xf6, 0xdc, 0xd3, 0xfe, 0x39, 0x8b, 0x9e, 0xf6, 0x58, 0xf4, 0x40, + 0x14, 0xce, 0xa5, 0x30, 0x7a, 0xf2, 0x5f, 0x50, 0xcc, 0x0f, 0xfd, 0xb4, 0x63, 0xe8, 0xa4, 0xf9, + 0xde, 0x9b, 0xf7, 0x7d, 0x33, 0x6f, 0x1e, 0xf5, 0x66, 0x40, 0x8b, 0x88, 0x5e, 0x87, 0x0c, 0xb2, + 0xce, 0xe0, 0x48, 0xfe, 0x1c, 0xf6, 0x19, 0x15, 0x14, 0x02, 0x22, 0x7a, 0x87, 0x12, 0x0e, 0x8e, + 0x1e, 0xb5, 0x62, 0x1a, 0x53, 0x65, 0xee, 0xc8, 0x91, 0x9e, 0xe1, 0xfd, 0xaf, 0x02, 0x36, 0xde, + 0x62, 0x86, 0x33, 0x0e, 0x8f, 0x40, 0x95, 0x0c, 0xb2, 0x20, 0x22, 0x39, 0xcd, 0xda, 0xab, 0xfb, + 0xab, 0x07, 0x55, 0xbf, 0x75, 0x53, 0xba, 0xce, 0x08, 0x67, 0xe9, 0x73, 0x6f, 0xe2, 0xf2, 0x90, + 0x4d, 0x06, 0xd9, 0x2b, 0x39, 0x84, 0x5f, 0x82, 0x2d, 0x92, 0xe3, 0x6e, 0x4a, 0x82, 0x90, 0x11, + 0x2c, 0x48, 0x7b, 0x6d, 0x7f, 0xf5, 0xc0, 0xf6, 0xdb, 0x37, 0xa5, 0xdb, 0x32, 0x61, 0xb3, 0x6e, + 0x0f, 0xd5, 0x35, 0x7e, 0xa9, 0x20, 0xfc, 0x02, 0xd4, 0xc6, 0x7e, 0x9c, 0xa6, 0xed, 0x8a, 0x0a, + 0xde, 0xbd, 0x29, 0x5d, 0x38, 0x1f, 0x8c, 0xd3, 0xd4, 0x43, 0xc0, 0x84, 0xe2, 0x34, 0x85, 0x2f, + 0x00, 0x20, 0x43, 0xc1, 0x70, 0x40, 0x92, 0x3e, 0x6f, 0x5b, 0xfb, 0x95, 0x83, 0x8a, 0xef, 0x5d, + 0x95, 0x6e, 0xf5, 0x54, 0x5a, 0x4f, 0xcf, 0xde, 0xf2, 0x9b, 0xd2, 0xdd, 0x36, 0x24, 0x93, 0x89, + 0x1e, 0xaa, 0x2a, 0x70, 0x9a, 0xf4, 0x39, 0xfc, 0x16, 0xd4, 0xc3, 0x1e, 0x4e, 0xf2, 0x20, 0xa4, + 0xf9, 0x77, 0x49, 0xdc, 0x5e, 0xdf, 0x5f, 0x3d, 0xa8, 0x1d, 0x3f, 0x3c, 0x9c, 0x66, 0xec, 0xf0, + 0xa5, 0xf4, 0xbf, 0x54, 0x6e, 0xff, 0xf1, 0x8f, 0xa5, 0xbb, 0x72, 0x53, 0xba, 0x3b, 0x9a, 0x74, + 0x36, 0xd4, 0x43, 0xb5, 0x70, 0x3a, 0x13, 0x1e, 0x83, 0x07, 0x38, 0x4d, 0xe9, 0xfb, 0xa0, 0xc8, + 0x65, 0x8a, 0x49, 0x28, 0x48, 0x14, 0x88, 0x21, 0x6f, 0x6f, 0xc8, 0xed, 0xa1, 0x1d, 0xe5, 0xfc, + 0x66, 0xea, 0xbb, 0x18, 0x72, 0xf8, 0x39, 0x80, 0x38, 0x14, 0xc9, 0x80, 0x04, 0x7d, 0x46, 0x42, + 0x9a, 0xf5, 0x93, 0x94, 0xf0, 0xf6, 0xe6, 0x7e, 0xe5, 0xa0, 0x8a, 0xb6, 0xb5, 0xe7, 0xed, 0xd4, + 0x01, 0x8f, 0x41, 0x5d, 0x1e, 0x47, 0xd8, 0xc3, 0x79, 0x4e, 0x52, 0xde, 0xb6, 0xe5, 0x44, 0xbf, + 0x79, 0x55, 0xba, 0xb5, 0xd3, 0xdf, 0x7f, 0xfd, 0xd2, 0x98, 0x51, 0x8d, 0x0c, 0xb2, 0x31, 0x78, + 0x6e, 0xfd, 0xf7, 0x07, 0x77, 0xd5, 0xfb, 0x4b, 0x13, 0xd4, 0x66, 0xb6, 0x05, 0xff, 0x04, 0x9a, + 0x3d, 0x9a, 0x11, 0x2e, 0x08, 0x8e, 0x82, 0x6e, 0x4a, 0xc3, 0x4b, 0x73, 0xf2, 0x4f, 0xff, 0x5d, + 0xba, 0x0f, 0x42, 0xca, 0x33, 0xca, 0x79, 0x74, 0x79, 0x98, 0xd0, 0x4e, 0x86, 0x45, 0xef, 0xf0, + 0x2c, 0x17, 0x37, 0xa5, 0xbb, 0xab, 0x93, 0xb0, 0x10, 0xe9, 0xa1, 0xc6, 0xc4, 0xe2, 0x4b, 0x03, + 0xec, 0x81, 0x46, 0x84, 0x69, 0xf0, 0x1d, 0x65, 0x97, 0x86, 0x7c, 0x4d, 0x91, 0xfb, 0x1f, 0x25, + 0xbf, 0x2a, 0xdd, 0xfa, 0xab, 0x17, 0xbf, 0xfb, 0x8a, 0xb2, 0x4b, 0x45, 0x71, 0x53, 0xba, 0x0f, + 0xb4, 0xd8, 0x3c, 0x91, 0x87, 0xea, 0x11, 0xa6, 0x93, 0x69, 0xf0, 0x5b, 0xe0, 0x4c, 0x26, 0xf0, + 0xa2, 0xdf, 0xa7, 0x4c, 0x98, 0x72, 0xfa, 0xfc, 0xaa, 0x74, 0x1b, 0x86, 0xf2, 0x9d, 0xf6, 0xdc, + 0x94, 0xee, 0xc3, 0x05, 0x52, 0x13, 0xe3, 0xa1, 0x86, 0xa1, 0x35, 0x53, 0x61, 0x17, 0xd4, 0x49, + 0xd2, 0x3f, 0x3a, 0x79, 0x62, 0x36, 0x60, 0xa9, 0x0d, 0xfc, 0xea, 0xbe, 0x0d, 0xd4, 0x4e, 0xcf, + 0xde, 0x1e, 0x9d, 0x3c, 0x19, 0xaf, 0xdf, 0x54, 0xcc, 0x2c, 0x8b, 0x87, 0x6a, 0x1a, 0xea, 0xc5, + 0x9f, 0x01, 0x03, 0x83, 0x1e, 0xe6, 0x3d, 0x55, 0x89, 0x55, 0xff, 0xe0, 0xaa, 0x74, 0x81, 0x66, + 0xfa, 0x0d, 0xe6, 0xbd, 0x69, 0xd6, 0xbb, 0xa3, 0x3f, 0xe3, 0x5c, 0x24, 0x45, 0x36, 0xe6, 0x02, + 0x3a, 0x58, 0xce, 0x9a, 0x2c, 0xf7, 0xc4, 0x2c, 0x77, 0x63, 0xd9, 0xe5, 0x9e, 0xdc, 0xb5, 0xdc, + 0x93, 0xf9, 0xe5, 0xea, 0x39, 0x13, 0x8d, 0x67, 0x46, 0x63, 0x73, 0x59, 0x8d, 0x67, 0x77, 0x69, + 0x3c, 0x9b, 0xd7, 0xd0, 0x73, 0x64, 0x5d, 0x2e, 0xec, 0xb3, 0x6d, 0x2f, 0x5d, 0x97, 0xb7, 0x32, + 0xd4, 0x98, 0x58, 0x34, 0xfb, 0x25, 0x68, 0x85, 0x34, 0xe7, 0x42, 0xda, 0x72, 0xda, 0x4f, 0x89, + 0x91, 0xa8, 0x2a, 0x89, 0x67, 0xf7, 0x49, 0x3c, 0x36, 0xdf, 0xff, 0x1d, 0xe1, 0x1e, 0xda, 0x99, + 0x37, 0x6b, 0xb1, 0x00, 0x38, 0x7d, 0x22, 0x08, 0xe3, 0xdd, 0x82, 0xc5, 0x46, 0x08, 0x28, 0xa1, + 0x9f, 0xdd, 0x27, 0x64, 0x2a, 0x74, 0x31, 0xd4, 0x43, 0xcd, 0xa9, 0x49, 0x0b, 0xfc, 0x01, 0x34, + 0x12, 0xa9, 0xda, 0x2d, 0x52, 0x43, 0x5f, 0x53, 0xf4, 0xc7, 0xf7, 0xd1, 0x9b, 0xaf, 0x6a, 0x3e, + 0xd0, 0x43, 0x5b, 0x63, 0x83, 0xa6, 0x8e, 0x00, 0xcc, 0x8a, 0x84, 0x05, 0x71, 0x8a, 0xc3, 0x84, + 0x30, 0x43, 0x5f, 0x57, 0xf4, 0x3f, 0xbf, 0x8f, 0xfe, 0x13, 0x4d, 0x7f, 0x3b, 0xd8, 0x43, 0x8e, + 0x34, 0xfe, 0x5a, 0xdb, 0xb4, 0xca, 0x3b, 0x50, 0xef, 0x12, 0x96, 0x26, 0xb9, 0xe1, 0xdf, 0x52, + 0xfc, 0x4f, 0xee, 0xe3, 0x37, 0x15, 0x34, 0x1b, 0xe6, 0xa1, 0x9a, 0x86, 0x13, 0xd2, 0x94, 0xe6, + 0x11, 0x1d, 0x93, 0x6e, 0x2f, 0x4d, 0x3a, 0x1b, 0xe6, 0xa1, 0x9a, 0x86, 0x9a, 0x34, 0x06, 0x3b, + 0x98, 0x31, 0xfa, 0x7e, 0x21, 0x21, 0x50, 0x71, 0x7f, 0x71, 0x1f, 0xf7, 0x23, 0xcd, 0x7d, 0x47, + 0xb4, 0x87, 0xb6, 0x95, 0x75, 0x2e, 0x25, 0x11, 0x80, 0x31, 0xc3, 0xa3, 0x05, 0x9d, 0xd6, 0xd2, + 0x89, 0xbf, 0x1d, 0xec, 0x21, 0x47, 0x1a, 0xe7, 0x54, 0xbe, 0x07, 0xad, 0x8c, 0xb0, 0x98, 0x04, + 0x39, 0x11, 0xbc, 0x9f, 0x26, 0xc2, 0xe8, 0x3c, 0x58, 0xfa, 0x3b, 0xb8, 0x2b, 0xdc, 0x43, 0x50, + 0x99, 0xdf, 0x18, 0xeb, 0xa4, 0x4a, 0x79, 0x0f, 0xe7, 0x71, 0x0f, 0x27, 0x46, 0x65, 0x77, 0xe9, + 0x2a, 0x9d, 0x0f, 0xf4, 0xd0, 0xd6, 0xd8, 0x30, 0x39, 0xea, 0x10, 0xe7, 0x61, 0x31, 0x3e, 0xea, + 0x87, 0x4b, 0x1f, 0xf5, 0x6c, 0x98, 0x6c, 0xe3, 0x0a, 0x2a, 0x52, 0xdd, 0x2f, 0xcf, 0x2d, 0xbb, + 0xe1, 0x34, 0xcf, 0x2d, 0xbb, 0xe9, 0x38, 0xe7, 0x96, 0xed, 0x38, 0xdb, 0xe7, 0x96, 0xbd, 0xe3, + 0xb4, 0xd0, 0xd6, 0x88, 0xa6, 0x34, 0x18, 0x3c, 0xd5, 0xa1, 0xa8, 0x46, 0xde, 0x63, 0x6e, 0xfe, + 0x6e, 0x50, 0x23, 0xc4, 0x02, 0xa7, 0x23, 0x6e, 0xd2, 0x81, 0x1c, 0x9d, 0xa4, 0x99, 0xe6, 0xd5, + 0x01, 0xeb, 0xef, 0x84, 0xbc, 0x00, 0x39, 0xa0, 0x72, 0x49, 0x46, 0xba, 0xe5, 0x22, 0x39, 0x84, + 0x2d, 0xb0, 0x3e, 0xc0, 0x69, 0xa1, 0x6f, 0x52, 0x55, 0xa4, 0x81, 0x77, 0x0e, 0x9a, 0x17, 0x0c, + 0xe7, 0x5c, 0x5e, 0x05, 0x68, 0xfe, 0x9a, 0xc6, 0x1c, 0x42, 0x60, 0xa9, 0x6e, 0xa1, 0x63, 0xd5, + 0x18, 0x7e, 0x0a, 0xac, 0x94, 0xc6, 0xbc, 0xbd, 0xb6, 0x5f, 0x39, 0xa8, 0x1d, 0x37, 0x67, 0xef, + 0x32, 0xaf, 0x69, 0x8c, 0x94, 0xd3, 0xfb, 0xe7, 0x1a, 0xa8, 0xbc, 0xa6, 0x31, 0x6c, 0x83, 0x4d, + 0x1c, 0x45, 0x8c, 0x70, 0x6e, 0x38, 0xc6, 0x10, 0xee, 0x82, 0x0d, 0x41, 0xfb, 0x49, 0xa8, 0x89, + 0xaa, 0xc8, 0x20, 0x29, 0x19, 0x61, 0x81, 0x55, 0x63, 0xad, 0x23, 0x35, 0x96, 0x57, 0x11, 0xb5, + 0xa7, 0x20, 0x2f, 0xb2, 0x2e, 0x61, 0xaa, 0x3f, 0x5a, 0x7e, 0xf3, 0xba, 0x74, 0x6b, 0xca, 0xfe, + 0x46, 0x99, 0xd1, 0x2c, 0x80, 0x9f, 0x81, 0x4d, 0x31, 0x9c, 0xed, 0x75, 0x3b, 0xd7, 0xa5, 0xdb, + 0x14, 0xd3, 0x0d, 0xca, 0x56, 0x86, 0x36, 0xc4, 0x50, 0xb5, 0xb4, 0x0e, 0xb0, 0xc5, 0x30, 0x48, + 0xf2, 0x88, 0x0c, 0x55, 0x3b, 0xb3, 0xfc, 0xd6, 0x75, 0xe9, 0x3a, 0x33, 0xd3, 0xcf, 0xa4, 0x0f, + 0x6d, 0x8a, 0xa1, 0x1a, 0xc0, 0xcf, 0x00, 0xd0, 0x4b, 0x52, 0x0a, 0xba, 0x3b, 0x6d, 0x5d, 0x97, + 0x6e, 0x55, 0x59, 0x15, 0xf7, 0x74, 0x08, 0x3d, 0xb0, 0xae, 0xb9, 0x6d, 0xc5, 0x5d, 0xbf, 0x2e, + 0x5d, 0x3b, 0xa5, 0xb1, 0xe6, 0xd4, 0x2e, 0x99, 0x2a, 0x46, 0x32, 0x3a, 0x20, 0x91, 0x6a, 0x11, + 0x36, 0x1a, 0x43, 0xef, 0xaf, 0x6b, 0xc0, 0xbe, 0x18, 0x22, 0xc2, 0x8b, 0x54, 0xc0, 0xaf, 0x80, + 0x13, 0xd2, 0x5c, 0x30, 0x1c, 0x8a, 0x60, 0x2e, 0xb5, 0xfe, 0xe3, 0xe9, 0x1f, 0xfa, 0xe2, 0x0c, + 0x0f, 0x35, 0xc7, 0xa6, 0x17, 0x26, 0xff, 0x2d, 0xb0, 0xde, 0x4d, 0x29, 0xcd, 0x54, 0x0d, 0xd4, + 0x91, 0x06, 0xf0, 0xb5, 0xca, 0x9a, 0x3a, 0xdf, 0x8a, 0xba, 0xab, 0x3e, 0x9e, 0x3d, 0xdf, 0x85, + 0xf2, 0xf0, 0x77, 0xcd, 0x7d, 0xb5, 0xa1, 0x55, 0x4d, 0xa4, 0x27, 0xb3, 0xaa, 0xca, 0xc7, 0x01, + 0x15, 0x46, 0x84, 0x3a, 0xae, 0x3a, 0x92, 0x43, 0xf8, 0x08, 0xd8, 0x8c, 0x0c, 0x08, 0x13, 0x24, + 0x52, 0xc7, 0x62, 0xa3, 0x09, 0x86, 0x9f, 0x00, 0x3b, 0xc6, 0x3c, 0x28, 0x38, 0x89, 0xf4, 0x19, + 0xa0, 0xcd, 0x18, 0xf3, 0x6f, 0x38, 0x89, 0x9e, 0x5b, 0x7f, 0xfb, 0xc1, 0x5d, 0xf1, 0x30, 0xa8, + 0xbd, 0x08, 0x43, 0xc2, 0xf9, 0x45, 0xd1, 0x4f, 0xc9, 0x3d, 0xb5, 0x75, 0x0c, 0xea, 0x5c, 0x50, + 0x86, 0x63, 0x12, 0x5c, 0x92, 0x91, 0xa9, 0x30, 0x5d, 0x2f, 0xc6, 0xfe, 0x5b, 0x32, 0xe2, 0x68, + 0x16, 0x18, 0x89, 0x7f, 0x58, 0xa0, 0x76, 0xc1, 0x70, 0x48, 0xcc, 0xd5, 0x55, 0x56, 0xa9, 0x84, + 0xcc, 0x48, 0x18, 0x24, 0xb5, 0x45, 0x92, 0x11, 0x5a, 0x08, 0xf3, 0x0d, 0x8d, 0xa1, 0x8c, 0x60, + 0x84, 0x0c, 0x49, 0xa8, 0x12, 0x68, 0x21, 0x83, 0xe0, 0x09, 0xd8, 0x8a, 0x12, 0xae, 0x9e, 0x1a, + 0x5c, 0xe0, 0xf0, 0x52, 0x6f, 0xdf, 0x77, 0xae, 0x4b, 0xb7, 0x6e, 0x1c, 0xef, 0xa4, 0x1d, 0xcd, + 0x21, 0xf8, 0x0b, 0xd0, 0x9c, 0x86, 0xa9, 0xd5, 0xea, 0x2b, 0xbe, 0x0f, 0xaf, 0x4b, 0xb7, 0x31, + 0x99, 0xaa, 0x3c, 0x68, 0x01, 0xcb, 0x33, 0x8e, 0x48, 0xb7, 0x88, 0x55, 0xd9, 0xd9, 0x48, 0x03, + 0x69, 0x4d, 0x93, 0x2c, 0x11, 0xaa, 0xcc, 0xd6, 0x91, 0x06, 0xf0, 0x04, 0x54, 0xe9, 0x80, 0x30, + 0x96, 0x44, 0x84, 0xab, 0xab, 0xc3, 0xc7, 0xdf, 0x29, 0x68, 0x3a, 0x53, 0x6e, 0xcb, 0x3c, 0xa0, + 0x32, 0x92, 0x51, 0x36, 0x52, 0xd7, 0x02, 0xb3, 0x2d, 0xed, 0xf8, 0x5a, 0xd9, 0xd1, 0x1c, 0x82, + 0x3e, 0x80, 0x26, 0x8c, 0x11, 0x51, 0xb0, 0x3c, 0x50, 0xdf, 0x7c, 0x5d, 0xc5, 0xaa, 0x2f, 0x4f, + 0x7b, 0x91, 0x72, 0xbe, 0xc2, 0x02, 0xa3, 0x5b, 0x16, 0xf8, 0x4b, 0x00, 0xf5, 0x69, 0x04, 0xdf, + 0x73, 0x3a, 0x79, 0x62, 0xe9, 0xbe, 0xae, 0xf4, 0xb5, 0xd7, 0xac, 0xd9, 0xd1, 0xe8, 0x9c, 0x53, + 0xb3, 0x8b, 0x73, 0xcb, 0xb6, 0x9c, 0xf5, 0x73, 0xcb, 0xde, 0x74, 0xec, 0x49, 0xe6, 0xcc, 0x2e, + 0xd0, 0xce, 0x18, 0xcf, 0x2c, 0xcf, 0xff, 0xf2, 0xc7, 0xab, 0xbd, 0xd5, 0x9f, 0xae, 0xf6, 0x56, + 0xff, 0x73, 0xb5, 0xb7, 0xfa, 0xf7, 0x0f, 0x7b, 0x2b, 0x3f, 0x7d, 0xd8, 0x5b, 0xf9, 0xd7, 0x87, + 0xbd, 0x95, 0x3f, 0x7e, 0x1a, 0x27, 0xa2, 0x57, 0x74, 0x0f, 0x43, 0x9a, 0x75, 0xde, 0x24, 0xdd, + 0x84, 0x15, 0x2a, 0x5d, 0x9d, 0x5c, 0x8d, 0x3b, 0x43, 0xf9, 0x5a, 0xee, 0x6e, 0xa8, 0xc7, 0xf0, + 0xd3, 0xff, 0x07, 0x00, 0x00, 0xff, 0xff, 0x40, 0x7e, 0x3a, 0xea, 0x46, 0x0f, 0x00, 0x00, } +func (this *Params) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*Params) + if !ok { + that2, ok := that.(Params) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.EvmDenom != that1.EvmDenom { + return false + } + if this.EnableCreate != that1.EnableCreate { + return false + } + if this.EnableCall != that1.EnableCall { + return false + } + if len(this.ExtraEIPs) != len(that1.ExtraEIPs) { + return false + } + for i := range this.ExtraEIPs { + if this.ExtraEIPs[i] != that1.ExtraEIPs[i] { + return false + } + } + if !this.ChainConfig.Equal(&that1.ChainConfig) { + return false + } + if this.AllowUnprotectedTxs != that1.AllowUnprotectedTxs { + return false + } + if len(this.ActivePrecompiles) != len(that1.ActivePrecompiles) { + return false + } + for i := range this.ActivePrecompiles { + if this.ActivePrecompiles[i] != that1.ActivePrecompiles[i] { + return false + } + } + if len(this.EVMChannels) != len(that1.EVMChannels) { + return false + } + for i := range this.EVMChannels { + if this.EVMChannels[i] != that1.EVMChannels[i] { + return false + } + } + return true +} +func (this *ChainConfig) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*ChainConfig) + if !ok { + that2, ok := that.(ChainConfig) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if that1.HomesteadBlock == nil { + if this.HomesteadBlock != nil { + return false + } + } else if !this.HomesteadBlock.Equal(*that1.HomesteadBlock) { + return false + } + if that1.DAOForkBlock == nil { + if this.DAOForkBlock != nil { + return false + } + } else if !this.DAOForkBlock.Equal(*that1.DAOForkBlock) { + return false + } + if this.DAOForkSupport != that1.DAOForkSupport { + return false + } + if that1.EIP150Block == nil { + if this.EIP150Block != nil { + return false + } + } else if !this.EIP150Block.Equal(*that1.EIP150Block) { + return false + } + if this.EIP150Hash != that1.EIP150Hash { + return false + } + if that1.EIP155Block == nil { + if this.EIP155Block != nil { + return false + } + } else if !this.EIP155Block.Equal(*that1.EIP155Block) { + return false + } + if that1.EIP158Block == nil { + if this.EIP158Block != nil { + return false + } + } else if !this.EIP158Block.Equal(*that1.EIP158Block) { + return false + } + if that1.ByzantiumBlock == nil { + if this.ByzantiumBlock != nil { + return false + } + } else if !this.ByzantiumBlock.Equal(*that1.ByzantiumBlock) { + return false + } + if that1.ConstantinopleBlock == nil { + if this.ConstantinopleBlock != nil { + return false + } + } else if !this.ConstantinopleBlock.Equal(*that1.ConstantinopleBlock) { + return false + } + if that1.PetersburgBlock == nil { + if this.PetersburgBlock != nil { + return false + } + } else if !this.PetersburgBlock.Equal(*that1.PetersburgBlock) { + return false + } + if that1.IstanbulBlock == nil { + if this.IstanbulBlock != nil { + return false + } + } else if !this.IstanbulBlock.Equal(*that1.IstanbulBlock) { + return false + } + if that1.MuirGlacierBlock == nil { + if this.MuirGlacierBlock != nil { + return false + } + } else if !this.MuirGlacierBlock.Equal(*that1.MuirGlacierBlock) { + return false + } + if that1.BerlinBlock == nil { + if this.BerlinBlock != nil { + return false + } + } else if !this.BerlinBlock.Equal(*that1.BerlinBlock) { + return false + } + if that1.LondonBlock == nil { + if this.LondonBlock != nil { + return false + } + } else if !this.LondonBlock.Equal(*that1.LondonBlock) { + return false + } + if that1.ArrowGlacierBlock == nil { + if this.ArrowGlacierBlock != nil { + return false + } + } else if !this.ArrowGlacierBlock.Equal(*that1.ArrowGlacierBlock) { + return false + } + if that1.GrayGlacierBlock == nil { + if this.GrayGlacierBlock != nil { + return false + } + } else if !this.GrayGlacierBlock.Equal(*that1.GrayGlacierBlock) { + return false + } + if that1.MergeNetsplitBlock == nil { + if this.MergeNetsplitBlock != nil { + return false + } + } else if !this.MergeNetsplitBlock.Equal(*that1.MergeNetsplitBlock) { + return false + } + if that1.ShanghaiBlock == nil { + if this.ShanghaiBlock != nil { + return false + } + } else if !this.ShanghaiBlock.Equal(*that1.ShanghaiBlock) { + return false + } + if that1.CancunBlock == nil { + if this.CancunBlock != nil { + return false + } + } else if !this.CancunBlock.Equal(*that1.CancunBlock) { + return false + } + return true +} func (m *Params) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) diff --git a/x/evm/evmmodule/genesis.go b/x/evm/evmmodule/genesis.go index 70fa0cc77..9be8352d8 100644 --- a/x/evm/evmmodule/genesis.go +++ b/x/evm/evmmodule/genesis.go @@ -23,7 +23,6 @@ func InitGenesis( accountKeeper evm.AccountKeeper, genState evm.GenesisState, ) []abci.ValidatorUpdate { - k.BeginBlock(ctx, abci.RequestBeginBlock{}) k.SetParams(ctx, genState.Params) if addr := accountKeeper.GetModuleAddress(evm.ModuleName); addr == nil { diff --git a/x/evm/evmtest/eth.go b/x/evm/evmtest/eth.go index 133ddd5fa..f525c6184 100644 --- a/x/evm/evmtest/eth.go +++ b/x/evm/evmtest/eth.go @@ -2,10 +2,12 @@ package evmtest import ( + "crypto/ecdsa" "math/big" "testing" cmt "github.com/cometbft/cometbft/types" + "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/stretchr/testify/assert" sdk "github.com/cosmos/cosmos-sdk/types" @@ -22,31 +24,35 @@ import ( "github.com/NibiruChain/nibiru/x/evm" ) -// NewEthAddr generates an Ethereum address. -func NewEthAddr() gethcommon.Address { - ethAddr, _ := PrivKeyEth() - return ethAddr +// NewEthAccInfo returns an Ethereum private key, its corresponding Eth address, +// public key, and Nibiru address. +func NewEthAccInfo() EthPrivKeyAcc { + privkey, _ := ethsecp256k1.GenerateKey() + privKeyE, _ := privkey.ToECDSA() + ethAddr := crypto.PubkeyToAddress(privKeyE.PublicKey) + return EthPrivKeyAcc{ + EthAddr: ethAddr, + NibiruAddr: EthAddrToNibiruAddr(ethAddr), + PrivKey: privkey, + PrivKeyE: privKeyE, + KeyringSigner: NewSigner(privkey), + } } -func NewEthAddrNibiruPair() ( - ethAddr gethcommon.Address, - privKey *ethsecp256k1.PrivKey, - nibiruAddr sdk.AccAddress, -) { - ethAddr, privKey = PrivKeyEth() - return ethAddr, privKey, EthPrivKeyToNibiruAddr(ethAddr) +func EthAddrToNibiruAddr(ethAddr gethcommon.Address) sdk.AccAddress { + return sdk.AccAddress(ethAddr.Bytes()) } -func EthPrivKeyToNibiruAddr(ethAddr gethcommon.Address) sdk.AccAddress { - return sdk.AccAddress(ethAddr.Bytes()) +type EthPrivKeyAcc struct { + EthAddr gethcommon.Address + NibiruAddr sdk.AccAddress + PrivKey *ethsecp256k1.PrivKey + PrivKeyE *ecdsa.PrivateKey + KeyringSigner keyring.Signer } -// PrivKeyEth returns an Ethereum private key and corresponding Eth address. -func PrivKeyEth() (gethcommon.Address, *ethsecp256k1.PrivKey) { - privkey, _ := ethsecp256k1.GenerateKey() - privKeyE, _ := privkey.ToECDSA() - ethAddr := crypto.PubkeyToAddress(privKeyE.PublicKey) - return ethAddr, privkey +func (acc EthPrivKeyAcc) GethSigner(ethChainID *big.Int) gethcore.Signer { + return gethcore.LatestSignerForChainID(ethChainID) } // NewEthTxMsg: Helper that returns a valid instance of [*evm.MsgEthereumTx]. @@ -55,7 +61,7 @@ func NewEthTxMsg() *evm.MsgEthereumTx { } func NewEthTxMsgs(count uint64) (ethTxMsgs []*evm.MsgEthereumTx) { - ethAddr := NewEthAddr() + ethAddr := NewEthAccInfo().EthAddr startIdx := uint64(1) for nonce := startIdx; nonce-startIdx < count; nonce++ { ethTxMsgs = append(ethTxMsgs, evm.NewTx(&evm.EvmTxArgs{ diff --git a/x/evm/evmtest/fixtures.go b/x/evm/evmtest/fixtures.go new file mode 100644 index 000000000..1e901dcda --- /dev/null +++ b/x/evm/evmtest/fixtures.go @@ -0,0 +1,99 @@ +package evmtest + +import ( + "encoding/json" + "fmt" + "os" + "path" + "strings" + "testing" + + gethabi "github.com/ethereum/go-ethereum/accounts/abi" + gethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/stretchr/testify/require" + + "github.com/NibiruChain/nibiru/x/common/testutil" +) + +type SmartContractFixture string + +const ( + SmartContract_FunToken SmartContractFixture = "FunToken.sol" +) + +type CompiledEvmContract struct { + ABI gethabi.ABI `json:"abi"` + Bytecode []byte `json:"bytecode"` +} + +// HardhatOutput: Expected format for smart contract test fixtures. +type HardhatOutput struct { + ABI json.RawMessage `json:"abi"` + Bytecode HexString `json:"bytecode"` +} + +// HexString: Hexadecimal-encoded string +type HexString string + +func (h HexString) Bytes() []byte { + return gethcommon.Hex2Bytes( + strings.TrimPrefix(string(h), "0x"), + ) +} +func (h HexString) String() string { return string(h) } +func (h HexString) FromBytes(bz []byte) HexString { + return HexString(gethcommon.Bytes2Hex(bz)) +} + +func NewHardhatOutputFromJson( + t *testing.T, + jsonBz []byte, +) HardhatOutput { + rawJsonBz := make(map[string]json.RawMessage) + err := json.Unmarshal(jsonBz, &rawJsonBz) + require.NoError(t, err) + var rawBytecodeBz HexString + err = json.Unmarshal(rawJsonBz["bytecode"], &rawBytecodeBz) + require.NoError(t, err) + + return HardhatOutput{ + ABI: rawJsonBz["abi"], + Bytecode: rawBytecodeBz, + } +} + +func (jsonObj HardhatOutput) EvmContract(t *testing.T) CompiledEvmContract { + newAbi := new(gethabi.ABI) + err := newAbi.UnmarshalJSON(jsonObj.ABI) + require.NoError(t, err) + + return CompiledEvmContract{ + ABI: *newAbi, + Bytecode: jsonObj.Bytecode.Bytes(), + } +} + +func (sc SmartContractFixture) Load(t *testing.T) CompiledEvmContract { + contractsDirPath := pathToContractsDir(t) + baseName := strings.TrimSuffix(string(sc), ".sol") + compiledPath := fmt.Sprintf("%s/%sCompiled.json", contractsDirPath, baseName) + + jsonBz, err := os.ReadFile(compiledPath) + require.NoError(t, err) + + compiledJson := NewHardhatOutputFromJson(t, jsonBz) + require.NoError(t, err) + return compiledJson.EvmContract(t) +} + +// pathToContractsDir: Returns the absolute path to the E2E test contract +// directory located at path, "NibiruChain/nibiru/e2e/evm/contracts". +func pathToContractsDir(t *testing.T) string { + dirEvmTest, err := testutil.GetPackageDir() + require.NoError(t, err) + dirOfRepo := path.Dir(path.Dir(path.Dir(dirEvmTest))) + dirEvmE2e := path.Join(dirOfRepo, "e2e/evm") + require.Equal(t, "evm", path.Base(dirEvmE2e)) + return dirEvmE2e + "/contracts" +} diff --git a/x/evm/evmtest/fixtures_test.go b/x/evm/evmtest/fixtures_test.go new file mode 100644 index 000000000..f698c9141 --- /dev/null +++ b/x/evm/evmtest/fixtures_test.go @@ -0,0 +1,11 @@ +package evmtest_test + +import ( + "testing" + + "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func TestLoadContracts(t *testing.T) { + evmtest.SmartContract_FunToken.Load(t) +} diff --git a/x/evm/evmtest/smart_contract.go b/x/evm/evmtest/smart_contract.go new file mode 100644 index 000000000..e043e1b3b --- /dev/null +++ b/x/evm/evmtest/smart_contract.go @@ -0,0 +1,69 @@ +package evmtest + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/params" + gethparams "github.com/ethereum/go-ethereum/params" + + gethcore "github.com/ethereum/go-ethereum/core/types" + + "github.com/NibiruChain/nibiru/x/evm" +) + +// ArgsCreateContract: Arguments to call with `CreateContractTxMsg` and +// `CreateContractGethCoreMsg` to make Ethereum transactions that create +// contracts. +// +// It is recommended to use a gas price of `big.NewInt(1)` for simpler op code +// calculations in gas units. +type ArgsCreateContract struct { + EthAcc EthPrivKeyAcc + EthChainIDInt *big.Int + GasPrice *big.Int + Nonce uint64 + GasLimit *big.Int +} + +func CreateContractTxMsg( + args ArgsCreateContract, +) (ethTxMsg *evm.MsgEthereumTx, err error) { + gasLimit := args.GasLimit + if gasLimit == nil { + gasLimit = new(big.Int).SetUint64(gethparams.TxGasContractCreation) + } + gethTxCreateCntract := &gethcore.AccessListTx{ + GasPrice: args.GasPrice, + Gas: gasLimit.Uint64(), + To: nil, + Data: []byte("contract_data"), + Nonce: args.Nonce, + } + ethTx := gethcore.NewTx(gethTxCreateCntract) + ethTxMsg = new(evm.MsgEthereumTx) + err = ethTxMsg.FromEthereumTx(ethTx) + if err != nil { + return ethTxMsg, err + } + fromAcc := args.EthAcc + ethTxMsg.From = fromAcc.EthAddr.Hex() + + gethSigner := fromAcc.GethSigner(args.EthChainIDInt) + keyringSigner := fromAcc.KeyringSigner + return ethTxMsg, ethTxMsg.Sign(gethSigner, keyringSigner) +} + +func CreateContractGethCoreMsg( + args ArgsCreateContract, + cfg *params.ChainConfig, + blockHeight *big.Int, +) (gethCoreMsg core.Message, err error) { + ethTxMsg, err := CreateContractTxMsg(args) + if err != nil { + return gethCoreMsg, err + } + + signer := gethcore.MakeSigner(cfg, blockHeight) + return ethTxMsg.AsMessage(signer, nil) +} diff --git a/x/evm/evmtest/smart_contract_test.go b/x/evm/evmtest/smart_contract_test.go new file mode 100644 index 000000000..ba725c138 --- /dev/null +++ b/x/evm/evmtest/smart_contract_test.go @@ -0,0 +1,49 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package evmtest_test + +import ( + "math/big" + + "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *SuiteEVMTest) TestCreateContractTxMsg() { + deps := evmtest.NewTestDeps() + ethAcc := evmtest.NewEthAccInfo() + + args := evmtest.ArgsCreateContract{ + EthAcc: ethAcc, + EthChainIDInt: deps.K.EthChainID(deps.Ctx), + GasPrice: big.NewInt(1), + Nonce: deps.StateDB().GetNonce(ethAcc.EthAddr), + } + + ethTxMsg, err := evmtest.CreateContractTxMsg(args) + s.NoError(err) + s.Require().NoError(ethTxMsg.ValidateBasic()) +} + +func (s *SuiteEVMTest) TestCreateContractGethCoreMsg() { + deps := evmtest.NewTestDeps() + ethAcc := evmtest.NewEthAccInfo() + + args := evmtest.ArgsCreateContract{ + EthAcc: ethAcc, + EthChainIDInt: deps.K.EthChainID(deps.Ctx), + GasPrice: big.NewInt(1), + Nonce: deps.StateDB().GetNonce(ethAcc.EthAddr), + } + + // chain config + evmParams, err := deps.Chain.EvmKeeper.EVMState().ModuleParams.Get(deps.Ctx) + s.Require().NoError(err) + cfg := evmParams.ChainConfig.EthereumConfig(args.EthChainIDInt) + + // block height + blockHeight := big.NewInt(deps.Ctx.BlockHeight()) + + _, err = evmtest.CreateContractGethCoreMsg( + args, cfg, blockHeight, + ) + s.NoError(err) +} diff --git a/x/evm/evmtest/test_deps.go b/x/evm/evmtest/test_deps.go new file mode 100644 index 000000000..c02b8527f --- /dev/null +++ b/x/evm/evmtest/test_deps.go @@ -0,0 +1,63 @@ +package evmtest + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + + gethcommon "github.com/ethereum/go-ethereum/common" + + gethcore "github.com/ethereum/go-ethereum/core/types" + + "github.com/NibiruChain/nibiru/app" + "github.com/NibiruChain/nibiru/app/codec" + "github.com/NibiruChain/nibiru/eth" + "github.com/NibiruChain/nibiru/x/common/testutil/testapp" + "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/keeper" + "github.com/NibiruChain/nibiru/x/evm/statedb" +) + +type TestDeps struct { + Chain *app.NibiruApp + Ctx sdk.Context + EncCfg codec.EncodingConfig + K keeper.Keeper + GenState *evm.GenesisState + Sender EthPrivKeyAcc +} + +func (deps TestDeps) GoCtx() context.Context { + return sdk.WrapSDKContext(deps.Ctx) +} + +func NewTestDeps() TestDeps { + testapp.EnsureNibiruPrefix() + encCfg := app.MakeEncodingConfig() + evm.RegisterInterfaces(encCfg.InterfaceRegistry) + eth.RegisterInterfaces(encCfg.InterfaceRegistry) + chain, ctx := testapp.NewNibiruTestAppAndContext() + ctx = ctx.WithChainID(eth.EIP155ChainID_Testnet) + ethAcc := NewEthAccInfo() + return TestDeps{ + Chain: chain, + Ctx: ctx, + EncCfg: encCfg, + K: chain.EvmKeeper, + GenState: evm.DefaultGenesisState(), + Sender: ethAcc, + } +} + +func (deps *TestDeps) StateDB() *statedb.StateDB { + return statedb.New(deps.Ctx, &deps.Chain.EvmKeeper, + statedb.NewEmptyTxConfig( + gethcommon.BytesToHash(deps.Ctx.HeaderHash().Bytes()), + ), + ) +} + +func (deps *TestDeps) GethSigner() gethcore.Signer { + ctx := deps.Ctx + return deps.Sender.GethSigner(deps.Chain.EvmKeeper.EthChainID(ctx)) +} diff --git a/x/evm/evmtest/tx.go b/x/evm/evmtest/tx.go new file mode 100644 index 000000000..ef38c5825 --- /dev/null +++ b/x/evm/evmtest/tx.go @@ -0,0 +1,133 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package evmtest + +import ( + "fmt" + "math/big" + + gethcommon "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" + gethparams "github.com/ethereum/go-ethereum/params" + + "github.com/NibiruChain/nibiru/x/evm" +) + +type GethTxType = uint8 + +var ( + GethTxType_LegacyTx GethTxType = gethcore.LegacyTxType + GethTxType_AccessListTx GethTxType = gethcore.AccessListTxType + GethTxType_DynamicFeeTx GethTxType = gethcore.DynamicFeeTxType +) + +func NewEthTx( + deps *TestDeps, txData gethcore.TxData, nonce uint64, +) (ethCoreTx *gethcore.Transaction, err error) { + ethCoreTx, err = NewEthTxUnsigned(deps, txData, nonce) + if err != nil { + return ethCoreTx, err + } + + sig, _, err := deps.Sender.KeyringSigner.SignByAddress( + deps.Sender.NibiruAddr, ethCoreTx.Hash().Bytes(), + ) + if err != nil { + return ethCoreTx, err + } + + return ethCoreTx.WithSignature(deps.GethSigner(), sig) +} + +func NewEthTxUnsigned( + deps *TestDeps, txData gethcore.TxData, nonce uint64, +) (ethCoreTx *gethcore.Transaction, err error) { + switch typedTxData := txData.(type) { + case *gethcore.LegacyTx: + typedTxData.Nonce = nonce + ethCoreTx = gethcore.NewTx(typedTxData) + case *gethcore.AccessListTx: + typedTxData.Nonce = nonce + ethCoreTx = gethcore.NewTx(typedTxData) + case *gethcore.DynamicFeeTx: + typedTxData.Nonce = nonce + ethCoreTx = gethcore.NewTx(typedTxData) + default: + return ethCoreTx, fmt.Errorf("received unknown tx type in NewEthTxUnsigned") + } + return ethCoreTx, err +} + +func TxTemplateAccessListTx() *gethcore.AccessListTx { + return &gethcore.AccessListTx{ + GasPrice: big.NewInt(1), + Gas: gethparams.TxGas, + To: &gethcommon.Address{}, + Value: big.NewInt(0), + Data: []byte{}, + } +} + +func TxTemplateLegacyTx() *gethcore.LegacyTx { + return &gethcore.LegacyTx{ + GasPrice: big.NewInt(1), + Gas: gethparams.TxGas, + To: &gethcommon.Address{}, + Value: big.NewInt(0), + Data: []byte{}, + } +} + +func TxTemplateDynamicFeeTx() *gethcore.DynamicFeeTx { + return &gethcore.DynamicFeeTx{ + GasFeeCap: big.NewInt(10), + GasTipCap: big.NewInt(2), + Gas: gethparams.TxGas, + To: &gethcommon.Address{}, + Value: big.NewInt(0), + Data: []byte{}, + } +} + +func NewEthTxMsgFromTxData( + deps *TestDeps, + txType GethTxType, + innerTxData []byte, + nonce uint64, + accessList gethcore.AccessList, +) (*evm.MsgEthereumTx, error) { + if innerTxData == nil { + innerTxData = []byte{} + } + + var ethCoreTx *gethcore.Transaction + switch txType { + case gethcore.LegacyTxType: + innerTx := TxTemplateLegacyTx() + innerTx.Nonce = nonce + innerTx.Data = innerTxData + ethCoreTx = gethcore.NewTx(innerTx) + case gethcore.AccessListTxType: + innerTx := TxTemplateAccessListTx() + innerTx.Nonce = nonce + innerTx.Data = innerTxData + innerTx.AccessList = accessList + ethCoreTx = gethcore.NewTx(innerTx) + case gethcore.DynamicFeeTxType: + innerTx := TxTemplateDynamicFeeTx() + innerTx.Nonce = nonce + innerTx.Data = innerTxData + innerTx.AccessList = accessList + ethCoreTx = gethcore.NewTx(innerTx) + default: + return nil, fmt.Errorf( + "received unknown tx type (%v) in NewEthTxMsgFromTxData", txType) + } + + ethTxMsg := new(evm.MsgEthereumTx) + if err := ethTxMsg.FromEthereumTx(ethCoreTx); err != nil { + return ethTxMsg, err + } + + ethTxMsg.From = deps.Sender.EthAddr.Hex() + return ethTxMsg, ethTxMsg.Sign(deps.GethSigner(), deps.Sender.KeyringSigner) +} diff --git a/x/evm/keeper/evm_state.go b/x/evm/keeper/evm_state.go index 5cf7cea3c..715105978 100644 --- a/x/evm/keeper/evm_state.go +++ b/x/evm/keeper/evm_state.go @@ -3,6 +3,7 @@ package keeper import ( "fmt" + "math/big" "slices" "github.com/NibiruChain/collections" @@ -10,6 +11,7 @@ import ( sdkstore "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" gethcommon "github.com/ethereum/go-ethereum/common" + gethcore "github.com/ethereum/go-ethereum/core/types" "github.com/NibiruChain/nibiru/eth" "github.com/NibiruChain/nibiru/x/evm" @@ -145,3 +147,39 @@ func (k *Keeper) GetState(ctx sdk.Context, addr eth.EthAddr, stateKey eth.EthHas []byte{}, )) } + +// GetBlockBloomTransient returns bloom bytes for the current block height +func (state EvmState) GetBlockBloomTransient(ctx sdk.Context) *big.Int { + bloomBz, err := state.BlockBloom.Get(ctx) + if err != nil { + return big.NewInt(0) + } + return new(big.Int).SetBytes(bloomBz) +} + +func (state EvmState) CalcBloomFromLogs( + ctx sdk.Context, newLogs []*gethcore.Log, +) (bloom gethcore.Bloom) { + if len(newLogs) > 0 { + bloomInt := state.GetBlockBloomTransient(ctx) + bloomInt.Or(bloomInt, big.NewInt(0).SetBytes(gethcore.LogsBloom(newLogs))) + bloom = gethcore.BytesToBloom(bloomInt.Bytes()) + } + return bloom +} + +// ResetTransientGasUsed resets gas to prepare for the next block of execution. +// Called in an ante handler. +func (k Keeper) ResetTransientGasUsed(ctx sdk.Context) { + k.EvmState.BlockGasUsed.Set(ctx, 0) +} + +// GetAccNonce returns the sequence number of an account, returns 0 if not exists. +func (k *Keeper) GetAccNonce(ctx sdk.Context, addr gethcommon.Address) uint64 { + nibiruAddr := sdk.AccAddress(addr.Bytes()) + acct := k.accountKeeper.GetAccount(ctx, nibiruAddr) + if acct == nil { + return 0 + } + return acct.GetSequence() +} diff --git a/x/evm/keeper/gas_fees.go b/x/evm/keeper/gas_fees.go new file mode 100644 index 000000000..10d366132 --- /dev/null +++ b/x/evm/keeper/gas_fees.go @@ -0,0 +1,181 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package keeper + +import ( + "math/big" + + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + gethcore "github.com/ethereum/go-ethereum/core/types" + + "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + errortypes "github.com/cosmos/cosmos-sdk/types/errors" + authante "github.com/cosmos/cosmos-sdk/x/auth/ante" + + "github.com/ethereum/go-ethereum/params" + + "github.com/NibiruChain/nibiru/x/evm" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +// GetEthIntrinsicGas returns the intrinsic gas cost for the transaction +func (k *Keeper) GetEthIntrinsicGas(ctx sdk.Context, msg core.Message, cfg *params.ChainConfig, isContractCreation bool) (uint64, error) { + height := big.NewInt(ctx.BlockHeight()) + homestead := cfg.IsHomestead(height) + istanbul := cfg.IsIstanbul(height) + + return core.IntrinsicGas( + msg.Data(), msg.AccessList(), + isContractCreation, homestead, istanbul, + ) +} + +// RefundGas transfers the leftover gas to the sender of the message, caped to half of the total gas +// consumed in the transaction. Additionally, the function sets the total gas consumed to the value +// returned by the EVM execution, thus ignoring the previous intrinsic gas consumed during in the +// AnteHandler. +func (k *Keeper) RefundGas(ctx sdk.Context, msg core.Message, leftoverGas uint64, denom string) error { + // Return EVM tokens for remaining gas, exchanged at the original rate. + remaining := new(big.Int).Mul(new(big.Int).SetUint64(leftoverGas), msg.GasPrice()) + + switch remaining.Sign() { + case -1: + // negative refund errors + return errors.Wrapf(evm.ErrInvalidRefund, "refunded amount value cannot be negative %d", remaining.Int64()) + case 1: + // positive amount refund + refundedCoins := sdk.Coins{sdk.NewCoin(denom, sdkmath.NewIntFromBigInt(remaining))} + + // refund to sender from the fee collector module account, which is the escrow account in charge of collecting tx fees + + err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, authtypes.FeeCollectorName, msg.From().Bytes(), refundedCoins) + if err != nil { + err = errors.Wrapf(errortypes.ErrInsufficientFunds, "fee collector account failed to refund fees: %s", err.Error()) + return errors.Wrapf(err, "failed to refund %d leftover gas (%s)", leftoverGas, refundedCoins.String()) + } + default: + // no refund, consume gas and update the tx gas meter + } + + return nil +} + +// ResetGasMeterAndConsumeGas reset first the gas meter consumed value to zero and set it back to the new value +// 'gasUsed' +func (k *Keeper) ResetGasMeterAndConsumeGas(ctx sdk.Context, gasUsed uint64) { + // reset the gas count + ctx.GasMeter().RefundGas(ctx.GasMeter().GasConsumed(), "reset the gas count") + ctx.GasMeter().ConsumeGas(gasUsed, "apply evm transaction") +} + +// GasToRefund calculates the amount of gas the state machine should refund to the sender. It is +// capped by the refund quotient value. +// Note: do not pass 0 to refundQuotient +func GasToRefund(availableRefund, gasConsumed, refundQuotient uint64) uint64 { + // Apply refund counter + refund := gasConsumed / refundQuotient + if refund > availableRefund { + return availableRefund + } + return refund +} + +// CheckSenderBalance validates that the tx cost value is positive and that the +// sender has enough funds to pay for the fees and value of the transaction. +func CheckSenderBalance( + balance sdkmath.Int, + txData evm.TxData, +) error { + cost := txData.Cost() + + if cost.Sign() < 0 { + return errors.Wrapf( + errortypes.ErrInvalidCoins, + "tx cost (%s) is negative and invalid", cost, + ) + } + + if balance.IsNegative() || balance.BigInt().Cmp(cost) < 0 { + return errors.Wrapf( + errortypes.ErrInsufficientFunds, + "sender balance < tx cost (%s < %s)", balance, txData.Cost(), + ) + } + return nil +} + +// DeductTxCostsFromUserBalance deducts the fees from the user balance. Returns +// an error if the specified sender address does not exist or the account balance +// is not sufficient. +func (k *Keeper) DeductTxCostsFromUserBalance( + ctx sdk.Context, + fees sdk.Coins, + from gethcommon.Address, +) error { + // fetch sender account + signerAcc, err := authante.GetSignerAcc(ctx, k.accountKeeper, from.Bytes()) + if err != nil { + return errors.Wrapf(err, "account not found for sender %s", from) + } + + // deduct the full gas cost from the user balance + if err := authante.DeductFees(k.bankKeeper, ctx, signerAcc, fees); err != nil { + return errors.Wrapf(err, "failed to deduct full gas cost %s from the user %s balance", fees, from) + } + + return nil +} + +// VerifyFee is used to return the fee for the given transaction data in sdk.Coins. It checks that the +// gas limit is not reached, the gas limit is higher than the intrinsic gas and that the +// base fee is higher than the gas fee cap. +func VerifyFee( + txData evm.TxData, + denom string, + baseFee *big.Int, + homestead, istanbul, isCheckTx bool, +) (sdk.Coins, error) { + isContractCreation := txData.GetTo() == nil + + gasLimit := txData.GetGas() + + var accessList gethcore.AccessList + if txData.GetAccessList() != nil { + accessList = txData.GetAccessList() + } + + intrinsicGas, err := core.IntrinsicGas(txData.GetData(), accessList, isContractCreation, homestead, istanbul) + if err != nil { + return nil, errors.Wrapf( + err, + "failed to retrieve intrinsic gas, contract creation = %t; homestead = %t, istanbul = %t", + isContractCreation, homestead, istanbul, + ) + } + + // intrinsic gas verification during CheckTx + if isCheckTx && gasLimit < intrinsicGas { + return nil, errors.Wrapf( + errortypes.ErrOutOfGas, + "gas limit too low: %d (gas limit) < %d (intrinsic gas)", gasLimit, intrinsicGas, + ) + } + + if baseFee != nil && txData.GetGasFeeCap().Cmp(baseFee) < 0 { + return nil, errors.Wrapf(errortypes.ErrInsufficientFee, + "the tx gasfeecap is lower than the tx baseFee: %s (gasfeecap), %s (basefee) ", + txData.GetGasFeeCap(), + baseFee) + } + + feeAmt := txData.EffectiveFee(baseFee) + if feeAmt.Sign() == 0 { + // zero fee, no need to deduct + return sdk.Coins{}, nil + } + + return sdk.Coins{{Denom: denom, Amount: sdkmath.NewIntFromBigInt(feeAmt)}}, nil +} diff --git a/x/evm/keeper/gas_fees_test.go b/x/evm/keeper/gas_fees_test.go new file mode 100644 index 000000000..d9ba98a66 --- /dev/null +++ b/x/evm/keeper/gas_fees_test.go @@ -0,0 +1,2 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package keeper_test diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index ed1c69f16..7792c58a2 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -3,14 +3,34 @@ package keeper import ( "context" + "encoding/json" + "errors" + "fmt" + "math/big" + "time" + + grpccodes "google.golang.org/grpc/codes" + grpcstatus "google.golang.org/grpc/status" sdkmath "cosmossdk.io/math" + storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" - gethcommon "github.com/ethereum/go-ethereum/common" - "github.com/NibiruChain/nibiru/x/common" + "github.com/NibiruChain/nibiru/eth" "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/statedb" + + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + gethparams "github.com/ethereum/go-ethereum/params" + + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" ) // Compile-time interface assertion @@ -78,25 +98,44 @@ func (k Keeper) NibiruAccount( return resp, nil } -// ValidatorAccount: Implements the gRPC query for "/eth.evm.v1.Query/ValidatorAccount". -// ValidatorAccount retrieves the account details for a given validator consensus address. +// ValidatorAccount: Implements the gRPC query for +// "/eth.evm.v1.Query/ValidatorAccount". ValidatorAccount retrieves the account +// details for a given validator consensus address. // // Parameters: // - goCtx: The context.Context object representing the request context. -// - req: The QueryValidatorAccountRequest object containing the validator consensus address. +// - req: Request containing the validator consensus address. // // Returns: -// - A pointer to the QueryValidatorAccountResponse object containing the account details. +// - Response containing the account details. // - An error if the account retrieval process encounters any issues. func (k Keeper) ValidatorAccount( goCtx context.Context, req *evm.QueryValidatorAccountRequest, ) (*evm.QueryValidatorAccountResponse, error) { - // TODO: feat(evm): impl query ValidatorAccount - return &evm.QueryValidatorAccountResponse{ - AccountAddress: "", - Sequence: 0, - AccountNumber: 0, - }, common.ErrNotImplementedGprc() + consAddr, err := req.Validate() + if err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(goCtx) + + validator, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, consAddr) + if !found { + return nil, fmt.Errorf("validator not found for %s", consAddr.String()) + } + + nibiruAddr := sdk.AccAddress(validator.GetOperator()) + res := evm.QueryValidatorAccountResponse{ + AccountAddress: nibiruAddr.String(), + } + + account := k.accountKeeper.GetAccount(ctx, nibiruAddr) + if account != nil { + res.Sequence = account.GetSequence() + res.AccountNumber = account.GetAccountNumber() + } + + return &res, nil } // Balance: Implements the gRPC query for "/eth.evm.v1.Query/Balance". @@ -125,10 +164,13 @@ func (k Keeper) Balance(goCtx context.Context, req *evm.QueryBalanceRequest) (*e func (k Keeper) BaseFee( goCtx context.Context, _ *evm.QueryBaseFeeRequest, ) (*evm.QueryBaseFeeResponse, error) { - // TODO: feat(evm): impl query BaseFee + ctx := sdk.UnwrapSDKContext(goCtx) + params := k.GetParams(ctx) + ethCfg := params.ChainConfig.EthereumConfig(k.EthChainID(ctx)) + baseFee := sdkmath.NewIntFromBigInt(k.GetBaseFee(ctx, ethCfg)) return &evm.QueryBaseFeeResponse{ - BaseFee: &sdkmath.Int{}, - }, common.ErrNotImplementedGprc() + BaseFee: &baseFee, + }, nil } // Storage: Implements the gRPC query for "/eth.evm.v1.Query/Storage". @@ -166,16 +208,31 @@ func (k Keeper) Storage( // // Parameters: // - goCtx: The context.Context object representing the request context. -// - req: The QueryCodeRequest object containing the Ethereum address. +// - req: Request with the Ethereum address of the smart contract bytecode. // // Returns: -// - A pointer to the QueryCodeResponse object containing the code. +// - Response containing the smart contract bytecode. // - An error if the code retrieval process encounters any issues. -func (k Keeper) Code(goCtx context.Context, req *evm.QueryCodeRequest) (*evm.QueryCodeResponse, error) { - // TODO: feat(evm): impl query Code +func (k Keeper) Code( + goCtx context.Context, req *evm.QueryCodeRequest, +) (*evm.QueryCodeResponse, error) { + if err := req.Validate(); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(goCtx) + + address := gethcommon.HexToAddress(req.Address) + acct := k.GetAccountWithoutBalance(ctx, address) + + var code []byte + if acct != nil && acct.IsContract() { + code = k.GetCode(ctx, gethcommon.BytesToHash(acct.CodeHash)) + } + return &evm.QueryCodeResponse{ - Code: []byte{}, - }, common.ErrNotImplementedGprc() + Code: code, + }, nil } // Params: Implements the gRPC query for "/eth.evm.v1.Query/Params". @@ -188,7 +245,9 @@ func (k Keeper) Code(goCtx context.Context, req *evm.QueryCodeRequest) (*evm.Que // Returns: // - A pointer to the QueryParamsResponse object containing the EVM module parameters. // - An error if the parameter retrieval process encounters any issues. -func (k Keeper) Params(goCtx context.Context, _ *evm.QueryParamsRequest) (*evm.QueryParamsResponse, error) { +func (k Keeper) Params( + goCtx context.Context, _ *evm.QueryParamsRequest, +) (*evm.QueryParamsResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) params := k.GetParams(ctx) return &evm.QueryParamsResponse{ @@ -199,9 +258,15 @@ func (k Keeper) Params(goCtx context.Context, _ *evm.QueryParamsRequest) (*evm.Q // EthCall: Implements the gRPC query for "/eth.evm.v1.Query/EthCall". // EthCall performs a smart contract call using the eth_call JSON-RPC method. // +// An "eth_call" is a method from the Ethereum JSON-RPC specification that allows +// one to call a smart contract function without execution a transaction on the +// blockchain. This is useful for simulating transactions and for reading data +// from the chain using responses from smart contract calls. +// // Parameters: -// - goCtx: The context.Context object representing the request context. -// - req: The EthCallRequest object containing the call parameters. +// - goCtx: Request context with information about the current block that +// serves as the main access point to the blockchain state. +// - req: "eth_call" parameters to interact with a smart contract. // // Returns: // - A pointer to the MsgEthereumTxResponse object containing the result of the eth_call. @@ -209,14 +274,41 @@ func (k Keeper) Params(goCtx context.Context, _ *evm.QueryParamsRequest) (*evm.Q func (k Keeper) EthCall( goCtx context.Context, req *evm.EthCallRequest, ) (*evm.MsgEthereumTxResponse, error) { - // TODO: feat(evm): impl query EthCall - return &evm.MsgEthereumTxResponse{ - Hash: "", - Logs: []*evm.Log{}, - Ret: []byte{}, - VmError: "", - GasUsed: 0, - }, common.ErrNotImplementedGprc() + if err := req.Validate(); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(goCtx) + + var args evm.JsonTxArgs + err := json.Unmarshal(req.Args, &args) + if err != nil { + return nil, grpcstatus.Error(grpccodes.InvalidArgument, err.Error()) + } + chainID := k.EthChainID(ctx) + cfg, err := k.GetEVMConfig(ctx, ParseProposerAddr(ctx, req.ProposerAddress), chainID) + if err != nil { + return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + + // ApplyMessageWithConfig expect correct nonce set in msg + nonce := k.GetAccNonce(ctx, args.GetFrom()) + args.Nonce = (*hexutil.Uint64)(&nonce) + + msg, err := args.ToMessage(req.GasCap, cfg.BaseFee) + if err != nil { + return nil, grpcstatus.Error(grpccodes.InvalidArgument, err.Error()) + } + + txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash())) + + // pass false to not commit StateDB + res, err := k.ApplyEvmMsg(ctx, msg, nil, false, cfg, txConfig) + if err != nil { + return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + + return res, nil } // EstimateGas: Implements the gRPC query for "/eth.evm.v1.Query/EstimateGas". @@ -224,12 +316,11 @@ func (k Keeper) EthCall( func (k Keeper) EstimateGas( goCtx context.Context, req *evm.EthCallRequest, ) (*evm.EstimateGasResponse, error) { - // TODO: feat(evm): impl query EstimateGas return k.EstimateGasForEvmCallType(goCtx, req, evm.CallTypeRPC) } // EstimateGasForEvmCallType estimates the gas cost of a transaction. This can be -// called with the "eth_estimateGas" JSON-RPC method or an smart contract query. +// called with the "eth_estimateGas" JSON-RPC method or smart contract query. // // When [EstimateGas] is called from the JSON-RPC client, we need to reset the // gas meter before simulating the transaction (tx) to have an accurate gas @@ -245,10 +336,146 @@ func (k Keeper) EstimateGas( func (k Keeper) EstimateGasForEvmCallType( goCtx context.Context, req *evm.EthCallRequest, fromType evm.CallType, ) (*evm.EstimateGasResponse, error) { - // TODO: feat(evm): impl query EstimateGasForEvmCallType - return &evm.EstimateGasResponse{ - Gas: 0, - }, common.ErrNotImplementedGprc() + if err := req.Validate(); err != nil { + return nil, err + } + + ctx := sdk.UnwrapSDKContext(goCtx) + chainID := k.EthChainID(ctx) + + if req.GasCap < gethparams.TxGas { + return nil, grpcstatus.Errorf(grpccodes.InvalidArgument, "gas cap cannot be lower than %d", gethparams.TxGas) + } + + var args evm.JsonTxArgs + err := json.Unmarshal(req.Args, &args) + if err != nil { + return nil, grpcstatus.Error(grpccodes.InvalidArgument, err.Error()) + } + + // Binary search the gas requirement, as it may be higher than the amount used + var ( + lo = gethparams.TxGas - 1 + hi uint64 + gasCap uint64 + ) + + // Determine the highest gas limit can be used during the estimation. + if args.Gas != nil && uint64(*args.Gas) >= gethparams.TxGas { + hi = uint64(*args.Gas) + } else { + // Query block gas limit + params := ctx.ConsensusParams() + if params != nil && params.Block != nil && params.Block.MaxGas > 0 { + hi = uint64(params.Block.MaxGas) + } else { + hi = req.GasCap + } + } + + // TODO: Recap the highest gas limit with account's available balance. + // Recap the highest gas allowance with specified gascap. + if req.GasCap != 0 && hi > req.GasCap { + hi = req.GasCap + } + + gasCap = hi + cfg, err := k.GetEVMConfig(ctx, ParseProposerAddr(ctx, req.ProposerAddress), chainID) + if err != nil { + return nil, grpcstatus.Error(grpccodes.Internal, "failed to load evm config") + } + + // ApplyMessageWithConfig expect correct nonce set in msg + nonce := k.GetAccNonce(ctx, args.GetFrom()) + args.Nonce = (*hexutil.Uint64)(&nonce) + + txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())) + + // convert the tx args to an ethereum message + msg, err := args.ToMessage(req.GasCap, cfg.BaseFee) + if err != nil { + return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + + // NOTE: the errors from the executable below should be consistent with + // go-ethereum, so we don't wrap them with the gRPC status code Create a + // helper to check if a gas allowance results in an executable transaction. + executable := func(gas uint64) (vmError bool, rsp *evm.MsgEthereumTxResponse, err error) { + // update the message with the new gas value + msg = gethcore.NewMessage( + msg.From(), + msg.To(), + msg.Nonce(), + msg.Value(), + gas, + msg.GasPrice(), + msg.GasFeeCap(), + msg.GasTipCap(), + msg.Data(), + msg.AccessList(), + msg.IsFake(), + ) + + tmpCtx := ctx + if fromType == evm.CallTypeRPC { + tmpCtx, _ = ctx.CacheContext() + + acct := k.GetAccount(tmpCtx, msg.From()) + + from := msg.From() + if acct == nil { + acc := k.accountKeeper.NewAccountWithAddress(tmpCtx, from[:]) + k.accountKeeper.SetAccount(tmpCtx, acc) + acct = statedb.NewEmptyAccount() + } + // When submitting a transaction, the `EthIncrementSenderSequence` ante handler increases the account nonce + acct.Nonce = nonce + 1 + err = k.SetAccount(tmpCtx, from, *acct) + if err != nil { + return true, nil, err + } + // resetting the gasMeter after increasing the sequence to have an accurate gas estimation on EVM extensions transactions + gasMeter := eth.NewInfiniteGasMeterWithLimit(msg.Gas()) + tmpCtx = tmpCtx.WithGasMeter(gasMeter). + WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}) + } + // pass false to not commit StateDB + rsp, err = k.ApplyEvmMsg(tmpCtx, msg, nil, false, cfg, txConfig) + if err != nil { + if errors.Is(err, core.ErrIntrinsicGas) { + return true, nil, nil // Special case, raise gas limit + } + return true, nil, err // Bail out + } + return len(rsp.VmError) > 0, rsp, nil + } + + // Execute the binary search and hone in on an executable gas limit + hi, err = evm.BinSearch(lo, hi, executable) + if err != nil { + return nil, err + } + + // Reject the transaction as invalid if it still fails at the highest allowance + if hi == gasCap { + failed, result, err := executable(hi) + if err != nil { + return nil, err + } + + if failed { + if result != nil && result.VmError != vm.ErrOutOfGas.Error() { + if result.VmError == vm.ErrExecutionReverted.Error() { + return nil, evm.NewExecErrorWithReason(result.Ret) + } + return nil, errors.New(result.VmError) + } + // Otherwise, the specified gas cap is too low + return nil, fmt.Errorf("gas required exceeds allowance (%d)", gasCap) + } + } + return &evm.EstimateGasResponse{Gas: hi}, nil } // TraceTx configures a new tracer according to the provided configuration, and @@ -257,12 +484,98 @@ func (k Keeper) EstimateGasForEvmCallType( func (k Keeper) TraceTx( goCtx context.Context, req *evm.QueryTraceTxRequest, ) (*evm.QueryTraceTxResponse, error) { - // TODO: feat(evm): impl query TraceTx + if err := req.Validate(); err != nil { + return nil, err + } + + // get the context of block beginning + contextHeight := req.BlockNumber + if contextHeight < 1 { + // 0 is a special value in `ContextWithHeight` + contextHeight = 1 + } + + ctx := sdk.UnwrapSDKContext(goCtx) + ctx = ctx.WithBlockHeight(contextHeight) + ctx = ctx.WithBlockTime(req.BlockTime) + ctx = ctx.WithHeaderHash(gethcommon.Hex2Bytes(req.BlockHash)) + + // to get the base fee we only need the block max gas in the consensus params + ctx = ctx.WithConsensusParams(&cmtproto.ConsensusParams{ + Block: &cmtproto.BlockParams{MaxGas: req.BlockMaxGas}, + }) + + chainID := k.EthChainID(ctx) + cfg, err := k.GetEVMConfig(ctx, ParseProposerAddr(ctx, req.ProposerAddress), chainID) + if err != nil { + return nil, grpcstatus.Errorf(grpccodes.Internal, "failed to load evm config: %s", err.Error()) + } + + // compute and use base fee of the height that is being traced + baseFee := k.GetBaseFee(ctx, cfg.ChainConfig) + if baseFee != nil { + cfg.BaseFee = baseFee + } + + signer := gethcore.MakeSigner(cfg.ChainConfig, big.NewInt(ctx.BlockHeight())) + + txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())) + + // gas used at this point corresponds to GetProposerAddress & CalculateBaseFee + // need to reset gas meter per transaction to be consistent with tx execution + // and avoid stacking the gas used of every predecessor in the same gas meter + + for i, tx := range req.Predecessors { + ethTx := tx.AsTransaction() + msg, err := ethTx.AsMessage(signer, cfg.BaseFee) + if err != nil { + continue + } + txConfig.TxHash = ethTx.Hash() + txConfig.TxIndex = uint(i) + // reset gas meter for each transaction + ctx = ctx.WithGasMeter(eth.NewInfiniteGasMeterWithLimit(msg.Gas())). + WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}) + rsp, err := k.ApplyEvmMsg(ctx, msg, evm.NewNoOpTracer(), true, cfg, txConfig) + if err != nil { + continue + } + txConfig.LogIndex += uint(len(rsp.Logs)) + } + + tx := req.Msg.AsTransaction() + txConfig.TxHash = tx.Hash() + if len(req.Predecessors) > 0 { + txConfig.TxIndex++ + } + + var tracerConfig json.RawMessage + if req.TraceConfig != nil && req.TraceConfig.TracerJsonConfig != "" { + // ignore error. default to no traceConfig + _ = json.Unmarshal([]byte(req.TraceConfig.TracerJsonConfig), &tracerConfig) + } + + result, _, err := k.TraceEthTxMsg(ctx, cfg, txConfig, signer, tx, req.TraceConfig, false, tracerConfig) + if err != nil { + // error will be returned with detail status from traceTx + return nil, err + } + + resultData, err := json.Marshal(result) + if err != nil { + return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + return &evm.QueryTraceTxResponse{ - Data: []byte{}, - }, common.ErrNotImplementedGprc() + Data: resultData, + }, nil } +// Re-export of the default tracer timeout from go-ethereum. +// See "geth/eth/tracers/api.go". +const DefaultGethTraceTimeout = 5 * time.Second + // TraceBlock: Implements the gRPC query for "/eth.evm.v1.Query/TraceBlock". // Configures a Nibiru EVM tracer that is used to "trace" and analyze // the execution of transactions within a given block. Block information is read @@ -271,8 +584,165 @@ func (k Keeper) TraceTx( func (k Keeper) TraceBlock( goCtx context.Context, req *evm.QueryTraceBlockRequest, ) (*evm.QueryTraceBlockResponse, error) { - // TODO: feat(evm): impl query TraceBlock + if err := req.Validate(); err != nil { + return nil, err + } + + // get the context of block beginning + contextHeight := req.BlockNumber + if contextHeight < 1 { + // 0 is a special value in `ContextWithHeight` + contextHeight = 1 + } + + ctx := sdk.UnwrapSDKContext(goCtx) + ctx = ctx.WithBlockHeight(contextHeight) + ctx = ctx.WithBlockTime(req.BlockTime) + ctx = ctx.WithHeaderHash(gethcommon.Hex2Bytes(req.BlockHash)) + + // to get the base fee we only need the block max gas in the consensus params + ctx = ctx.WithConsensusParams(&cmtproto.ConsensusParams{ + Block: &cmtproto.BlockParams{MaxGas: req.BlockMaxGas}, + }) + + chainID := k.EthChainID(ctx) + + cfg, err := k.GetEVMConfig(ctx, ParseProposerAddr(ctx, req.ProposerAddress), chainID) + if err != nil { + return nil, grpcstatus.Error(grpccodes.Internal, "failed to load evm config") + } + + // compute and use base fee of height that is being traced + baseFee := k.GetBaseFeeNoCfg(ctx) + if baseFee != nil { + cfg.BaseFee = baseFee + } + + signer := gethcore.MakeSigner(cfg.ChainConfig, big.NewInt(ctx.BlockHeight())) + txsLength := len(req.Txs) + results := make([]*evm.TxTraceResult, 0, txsLength) + + txConfig := statedb.NewEmptyTxConfig(gethcommon.BytesToHash(ctx.HeaderHash().Bytes())) + + for i, tx := range req.Txs { + result := evm.TxTraceResult{} + ethTx := tx.AsTransaction() + txConfig.TxHash = ethTx.Hash() + txConfig.TxIndex = uint(i) + traceResult, logIndex, err := k.TraceEthTxMsg(ctx, cfg, txConfig, signer, ethTx, req.TraceConfig, true, nil) + if err != nil { + result.Error = err.Error() + } else { + txConfig.LogIndex = logIndex + result.Result = traceResult + } + results = append(results, &result) + } + + resultData, err := json.Marshal(results) + if err != nil { + return nil, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + return &evm.QueryTraceBlockResponse{ - Data: []byte{}, - }, common.ErrNotImplementedGprc() + Data: resultData, + }, nil +} + +// TraceEthTxMsg do trace on one transaction, it returns a tuple: (traceResult, +// nextLogIndex, error). +func (k *Keeper) TraceEthTxMsg( + ctx sdk.Context, + cfg *statedb.EVMConfig, + txConfig statedb.TxConfig, + signer gethcore.Signer, + tx *gethcore.Transaction, + traceConfig *evm.TraceConfig, + commitMessage bool, + tracerJSONConfig json.RawMessage, +) (*interface{}, uint, error) { + // Assemble the structured logger or the JavaScript tracer + var ( + tracer tracers.Tracer + overrides *gethparams.ChainConfig + err error + timeout = DefaultGethTraceTimeout + ) + msg, err := tx.AsMessage(signer, cfg.BaseFee) + if err != nil { + return nil, 0, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + + if traceConfig == nil { + traceConfig = &evm.TraceConfig{} + } + + if traceConfig.Overrides != nil { + overrides = traceConfig.Overrides.EthereumConfig(cfg.ChainConfig.ChainID) + } + + logConfig := logger.Config{ + EnableMemory: traceConfig.EnableMemory, + DisableStorage: traceConfig.DisableStorage, + DisableStack: traceConfig.DisableStack, + EnableReturnData: traceConfig.EnableReturnData, + Debug: traceConfig.Debug, + Limit: int(traceConfig.Limit), + Overrides: overrides, + } + + tracer = logger.NewStructLogger(&logConfig) + + tCtx := &tracers.Context{ + BlockHash: txConfig.BlockHash, + TxIndex: int(txConfig.TxIndex), + TxHash: txConfig.TxHash, + } + + if traceConfig.Tracer != "" { + if tracer, err = tracers.New(traceConfig.Tracer, tCtx, tracerJSONConfig); err != nil { + return nil, 0, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + } + + // Define a meaningful timeout of a single transaction trace + if traceConfig.Timeout != "" { + if timeout, err = time.ParseDuration(traceConfig.Timeout); err != nil { + return nil, 0, grpcstatus.Errorf(grpccodes.InvalidArgument, "timeout value: %s", err.Error()) + } + } + + // Handle timeouts and RPC cancellations + deadlineCtx, cancel := context.WithTimeout(ctx.Context(), timeout) + defer cancel() + + go func() { + <-deadlineCtx.Done() + if errors.Is(deadlineCtx.Err(), context.DeadlineExceeded) { + tracer.Stop(errors.New("execution timeout")) + } + }() + + // In order to be on in sync with the tx execution gas meter, + // we need to: + // 1. Reset GasMeter with InfiniteGasMeterWithLimit + // 2. Setup an empty KV gas config for gas to be calculated by opcodes + // and not kvstore actions + // 3. Setup an empty transient KV gas config for transient gas to be + // calculated by opcodes + ctx = ctx.WithGasMeter(eth.NewInfiniteGasMeterWithLimit(msg.Gas())). + WithKVGasConfig(storetypes.GasConfig{}). + WithTransientKVGasConfig(storetypes.GasConfig{}) + res, err := k.ApplyEvmMsg(ctx, msg, tracer, commitMessage, cfg, txConfig) + if err != nil { + return nil, 0, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + + var result interface{} + result, err = tracer.GetResult() + if err != nil { + return nil, 0, grpcstatus.Error(grpccodes.Internal, err.Error()) + } + + return &result, txConfig.LogIndex + uint(len(res.Logs)), nil } diff --git a/x/evm/keeper/grpc_query_test.go b/x/evm/keeper/grpc_query_test.go index 46b726076..1592b58ec 100644 --- a/x/evm/keeper/grpc_query_test.go +++ b/x/evm/keeper/grpc_query_test.go @@ -1,63 +1,31 @@ package keeper_test import ( + "encoding/json" + "fmt" + + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + + "github.com/NibiruChain/collections" - "github.com/NibiruChain/nibiru/app" - "github.com/NibiruChain/nibiru/app/codec" + srvconfig "github.com/NibiruChain/nibiru/app/server/config" "github.com/NibiruChain/nibiru/eth" - "github.com/NibiruChain/nibiru/eth/crypto/ethsecp256k1" + "github.com/NibiruChain/nibiru/x/common" "github.com/NibiruChain/nibiru/x/common/testutil/testapp" "github.com/NibiruChain/nibiru/x/evm" "github.com/NibiruChain/nibiru/x/evm/evmtest" - "github.com/NibiruChain/nibiru/x/evm/keeper" ) -type TestDeps struct { - chain *app.NibiruApp - ctx sdk.Context - encCfg codec.EncodingConfig - k keeper.Keeper - genState *evm.GenesisState - sender Sender -} - -type Sender struct { - EthAddr gethcommon.Address - PrivKey *ethsecp256k1.PrivKey - NibiruAddr sdk.AccAddress -} - -func (s *KeeperSuite) SetupTest() TestDeps { - testapp.EnsureNibiruPrefix() - encCfg := app.MakeEncodingConfig() - evm.RegisterInterfaces(encCfg.InterfaceRegistry) - eth.RegisterInterfaces(encCfg.InterfaceRegistry) - chain, ctx := testapp.NewNibiruTestAppAndContext() - - ethAddr, privKey, nibiruAddr := evmtest.NewEthAddrNibiruPair() - return TestDeps{ - chain: chain, - ctx: ctx, - encCfg: encCfg, - k: chain.EvmKeeper, - genState: evm.DefaultGenesisState(), - sender: Sender{ - EthAddr: ethAddr, - PrivKey: privKey, - NibiruAddr: nibiruAddr, - }, - } -} - func InvalidEthAddr() string { return "0x0000" } type TestCase[In, Out any] struct { name string // setup: Optional setup function to create the scenario - setup func(deps *TestDeps) - scenario func(deps *TestDeps) ( + setup func(deps *evmtest.TestDeps) + scenario func(deps *evmtest.TestDeps) ( req In, wantResp Out, ) @@ -70,7 +38,7 @@ func (s *KeeperSuite) TestQueryNibiruAccount() { testCases := []TestCase[In, Out]{ { name: "sad: msg validation", - scenario: func(deps *TestDeps) (req In, wantResp Out) { + scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { req = &evm.QueryNibiruAccountRequest{ Address: InvalidEthAddr(), } @@ -83,13 +51,13 @@ func (s *KeeperSuite) TestQueryNibiruAccount() { }, { name: "happy", - scenario: func(deps *TestDeps) (req In, wantResp Out) { - ethAddr, _, nibiruAddr := evmtest.NewEthAddrNibiruPair() + scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { + ethAcc := evmtest.NewEthAccInfo() req = &evm.QueryNibiruAccountRequest{ - Address: ethAddr.String(), + Address: ethAcc.EthAddr.String(), } wantResp = &evm.QueryNibiruAccountResponse{ - Address: nibiruAddr.String(), + Address: ethAcc.NibiruAddr.String(), Sequence: 0, AccountNumber: 0, } @@ -101,10 +69,10 @@ func (s *KeeperSuite) TestQueryNibiruAccount() { for _, tc := range testCases { s.Run(tc.name, func() { - deps := s.SetupTest() + deps := evmtest.NewTestDeps() req, wantResp := tc.scenario(&deps) - goCtx := sdk.WrapSDKContext(deps.ctx) - gotResp, err := deps.k.NibiruAccount(goCtx, req) + goCtx := sdk.WrapSDKContext(deps.Ctx) + gotResp, err := deps.K.NibiruAccount(goCtx, req) if tc.wantErr != "" { s.Require().ErrorContains(err, tc.wantErr) return @@ -121,7 +89,7 @@ func (s *KeeperSuite) TestQueryEthAccount() { testCases := []TestCase[In, Out]{ { name: "sad: msg validation", - scenario: func(deps *TestDeps) (req In, wantResp Out) { + scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { req = &evm.QueryEthAccountRequest{ Address: InvalidEthAddr(), } @@ -136,21 +104,21 @@ func (s *KeeperSuite) TestQueryEthAccount() { }, { name: "happy: fund account + query", - setup: func(deps *TestDeps) { - chain := deps.chain - ethAddr := deps.sender.EthAddr + setup: func(deps *evmtest.TestDeps) { + chain := deps.Chain + ethAddr := deps.Sender.EthAddr // fund account with 420 tokens coins := sdk.Coins{sdk.NewInt64Coin(evm.DefaultEVMDenom, 420)} - err := chain.BankKeeper.MintCoins(deps.ctx, evm.ModuleName, coins) + err := chain.BankKeeper.MintCoins(deps.Ctx, evm.ModuleName, coins) s.NoError(err) err = chain.BankKeeper.SendCoinsFromModuleToAccount( - deps.ctx, evm.ModuleName, ethAddr.Bytes(), coins) + deps.Ctx, evm.ModuleName, ethAddr.Bytes(), coins) s.Require().NoError(err) }, - scenario: func(deps *TestDeps) (req In, wantResp Out) { + scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { req = &evm.QueryEthAccountRequest{ - Address: deps.sender.EthAddr.Hex(), + Address: deps.Sender.EthAddr.Hex(), } wantResp = &evm.QueryEthAccountResponse{ Balance: "420", @@ -165,13 +133,110 @@ func (s *KeeperSuite) TestQueryEthAccount() { for _, tc := range testCases { s.Run(tc.name, func() { - deps := s.SetupTest() + deps := evmtest.NewTestDeps() + if tc.setup != nil { + tc.setup(&deps) + } req, wantResp := tc.scenario(&deps) + goCtx := sdk.WrapSDKContext(deps.Ctx) + gotResp, err := deps.K.EthAccount(goCtx, req) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Assert().NoError(err) + s.EqualValues(wantResp, gotResp) + }) + } +} + +func (s *KeeperSuite) TestQueryValidatorAccount() { + type In = *evm.QueryValidatorAccountRequest + type Out = *evm.QueryValidatorAccountResponse + testCases := []TestCase[In, Out]{ + { + name: "sad: msg validation", + scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { + req = &evm.QueryValidatorAccountRequest{ + ConsAddress: "nibi1invalidaddr", + } + wantResp = &evm.QueryValidatorAccountResponse{ + AccountAddress: sdk.AccAddress(gethcommon.Address{}.Bytes()).String(), + } + return req, wantResp + }, + wantErr: "decoding bech32 failed", + }, + { + name: "happy: default values", + setup: func(deps *evmtest.TestDeps) {}, + scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { + valopers := deps.Chain.StakingKeeper.GetValidators(deps.Ctx, 1) + valAddrBz := valopers[0].GetOperator().Bytes() + _, err := sdk.ConsAddressFromBech32(valopers[0].OperatorAddress) + s.ErrorContains(err, "expected nibivalcons, got nibivaloper") + consAddr := sdk.ConsAddress(valAddrBz) + + req = &evm.QueryValidatorAccountRequest{ + ConsAddress: consAddr.String(), + } + wantResp = &evm.QueryValidatorAccountResponse{ + AccountAddress: sdk.AccAddress(valAddrBz).String(), + Sequence: 0, + AccountNumber: 0, + } + return req, wantResp + }, + wantErr: "", + }, + { + name: "happy: with nonce", + setup: func(deps *evmtest.TestDeps) {}, + scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { + valopers := deps.Chain.StakingKeeper.GetValidators(deps.Ctx, 1) + valAddrBz := valopers[0].GetOperator().Bytes() + consAddr := sdk.ConsAddress(valAddrBz) + + s.T().Log( + "Send coins to validator to register in the account keeper.") + coinsToSend := sdk.NewCoins(sdk.NewCoin(eth.EthBaseDenom, math.NewInt(69420))) + valAddr := sdk.AccAddress(valAddrBz) + s.NoError(testapp.FundAccount( + deps.Chain.BankKeeper, + deps.Ctx, valAddr, + coinsToSend, + )) + + req = &evm.QueryValidatorAccountRequest{ + ConsAddress: consAddr.String(), + } + + ak := deps.Chain.AccountKeeper + acc := ak.GetAccount(deps.Ctx, valAddr) + s.NoError(acc.SetAccountNumber(420), "acc: ", acc.String()) + s.NoError(acc.SetSequence(69), "acc: ", acc.String()) + ak.SetAccount(deps.Ctx, acc) + + wantResp = &evm.QueryValidatorAccountResponse{ + AccountAddress: sdk.AccAddress(valAddrBz).String(), + Sequence: 69, + AccountNumber: 420, + } + return req, wantResp + }, + wantErr: "", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() if tc.setup != nil { tc.setup(&deps) } - goCtx := sdk.WrapSDKContext(deps.ctx) - gotResp, err := deps.k.EthAccount(goCtx, req) + req, wantResp := tc.scenario(&deps) + goCtx := sdk.WrapSDKContext(deps.Ctx) + gotResp, err := deps.K.ValidatorAccount(goCtx, req) if tc.wantErr != "" { s.Require().ErrorContains(err, tc.wantErr) return @@ -181,3 +246,239 @@ func (s *KeeperSuite) TestQueryEthAccount() { }) } } + +func (s *KeeperSuite) TestQueryStorage() { + type In = *evm.QueryStorageRequest + type Out = *evm.QueryStorageResponse + testCases := []TestCase[In, Out]{ + { + name: "sad: msg validation", + scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { + req = &evm.QueryStorageRequest{ + Address: InvalidEthAddr(), + } + return req, wantResp + }, + wantErr: "InvalidArgument", + }, + { + name: "happy", + scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { + addr := evmtest.NewEthAccInfo().EthAddr + storageKey := gethcommon.BytesToHash([]byte("storagekey")) + req = &evm.QueryStorageRequest{ + Address: addr.Hex(), + Key: storageKey.String(), + } + + stateDB := deps.StateDB() + storageValue := gethcommon.BytesToHash([]byte("value")) + + stateDB.SetState(addr, storageKey, storageValue) + s.NoError(stateDB.Commit()) + + wantResp = &evm.QueryStorageResponse{ + Value: storageValue.String(), + } + return req, wantResp + }, + wantErr: "", + }, + { + name: "happy: no committed state", + scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { + addr := evmtest.NewEthAccInfo().EthAddr + storageKey := gethcommon.BytesToHash([]byte("storagekey")) + req = &evm.QueryStorageRequest{ + Address: addr.Hex(), + Key: storageKey.String(), + } + + wantResp = &evm.QueryStorageResponse{ + Value: gethcommon.BytesToHash([]byte{}).String(), + } + return req, wantResp + }, + wantErr: "", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + if tc.setup != nil { + tc.setup(&deps) + } + req, wantResp := tc.scenario(&deps) + goCtx := sdk.WrapSDKContext(deps.Ctx) + + gotResp, err := deps.K.Storage(goCtx, req) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Assert().NoError(err) + s.EqualValues(wantResp, gotResp) + }) + } +} + +func (s *KeeperSuite) TestQueryCode() { + type In = *evm.QueryCodeRequest + type Out = *evm.QueryCodeResponse + testCases := []TestCase[In, Out]{ + { + name: "sad: msg validation", + scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { + req = &evm.QueryCodeRequest{ + Address: InvalidEthAddr(), + } + return req, wantResp + }, + wantErr: "InvalidArgument", + }, + { + name: "happy", + scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { + addr := evmtest.NewEthAccInfo().EthAddr + req = &evm.QueryCodeRequest{ + Address: addr.Hex(), + } + + stateDB := deps.StateDB() + contractBytecode := []byte("bytecode") + stateDB.SetCode(addr, contractBytecode) + s.Require().NoError(stateDB.Commit()) + + s.NotNil(stateDB.Keeper().GetAccount(deps.Ctx, addr)) + s.NotNil(stateDB.GetCode(addr)) + + wantResp = &evm.QueryCodeResponse{ + Code: contractBytecode, + } + return req, wantResp + }, + wantErr: "", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + deps := evmtest.NewTestDeps() + if tc.setup != nil { + tc.setup(&deps) + } + req, wantResp := tc.scenario(&deps) + goCtx := sdk.WrapSDKContext(deps.Ctx) + + gotResp, err := deps.K.Code(goCtx, req) + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Assert().NoError(err) + s.EqualValues(wantResp, gotResp, "want hex (%s), got hex (%s)", + collections.HumanizeBytes(wantResp.Code), + collections.HumanizeBytes(gotResp.Code), + ) + }) + } +} + +// AssertModuleParamsEqual errors if the fields don't match. This function avoids +// failing the "EqualValues" check due to comparisons between nil and empty +// slices: `[]string(nil)` and `[]string{}`. +func AssertModuleParamsEqual(want, got evm.Params) error { + errs := []error{} + { + want, got := want.EvmDenom, got.EvmDenom + if want != got { + errs = append(errs, ErrModuleParamsEquality( + "evm_denom", want, got)) + } + } + { + want, got := want.EnableCreate, got.EnableCreate + if want != got { + errs = append(errs, ErrModuleParamsEquality( + "enable_create", want, got)) + } + } + { + want, got := want.EnableCall, got.EnableCall + if want != got { + errs = append(errs, ErrModuleParamsEquality( + "enable_call", want, got)) + } + } + { + want, got := want.ChainConfig, got.ChainConfig + if want != got { + errs = append(errs, ErrModuleParamsEquality( + "chain_config", want, got)) + } + } + return common.CombineErrors(errs...) +} + +func ErrModuleParamsEquality(field string, want, got any) error { + return fmt.Errorf(`failed AssetModuleParamsEqual on field %s: want "%v", got "%v"`, field, want, got) +} + +func (s *KeeperSuite) TestQueryParams() { + deps := evmtest.NewTestDeps() + want := evm.DefaultParams() + deps.K.SetParams(deps.Ctx, want) + gotResp, err := deps.K.Params(deps.GoCtx(), nil) + got := gotResp.Params + s.Require().NoError(err) + + // Note that protobuf equals is more reliable than `s.Equal` + s.Require().True(want.Equal(got), "want %s, got %s", want, got) + + // Empty params to test the setter + want.ActivePrecompiles = []string{"new", "something"} + deps.K.SetParams(deps.Ctx, want) + gotResp, err = deps.K.Params(deps.GoCtx(), nil) + s.Require().NoError(err) + got = gotResp.Params + + // Note that protobuf equals is more reliable than `s.Equal` + s.Require().True(want.Equal(got), "want %s, got %s", want, got) +} + +func (s *KeeperSuite) TestEthCall_ERC20_Happy() { + deps := evmtest.NewTestDeps() + fungibleTokenContract := evmtest.SmartContract_FunToken.Load(s.T()) + + s.T().Log("Populate the supply and acc balance") + contractConstructorArgs, err := fungibleTokenContract.ABI.Pack( + "", + ) + s.Require().NoError(err) + + bytecode := fungibleTokenContract.Bytecode + bytecode = append(bytecode, contractConstructorArgs...) + + jsonTxArgs, err := json.Marshal(&evm.JsonTxArgs{ + From: &deps.Sender.EthAddr, + Data: (*hexutil.Bytes)(&bytecode), + }) + s.Require().NoError(err) + + _, err = deps.Chain.EvmKeeper.EstimateGas(deps.GoCtx(), &evm.EthCallRequest{ + Args: jsonTxArgs, + GasCap: srvconfig.DefaultGasCap, + ProposerAddress: []byte{}, + ChainId: deps.Chain.EvmKeeper.EthChainID(deps.Ctx).Int64(), + }) + s.Require().NoError(err) + + _, err = deps.Chain.EvmKeeper.EthCall(deps.GoCtx(), &evm.EthCallRequest{ + Args: jsonTxArgs, + GasCap: srvconfig.DefaultGasCap, + ProposerAddress: []byte{}, + ChainId: deps.Chain.EvmKeeper.EthChainID(deps.Ctx).Int64(), + }) + s.Require().NoError(err) +} diff --git a/x/evm/keeper/hooks.go b/x/evm/keeper/hooks.go index d3c3cfdf3..ddf2eb1e7 100644 --- a/x/evm/keeper/hooks.go +++ b/x/evm/keeper/hooks.go @@ -10,12 +10,13 @@ import ( // BeginBlock sets the sdk Context and EIP155 chain id to the Keeper. func (k *Keeper) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) { // TODO: feat(evm): impl BeginBlock + // Is it necessary to set a local variable, or can we use ctx everywhere? } // EndBlock also retrieves the bloom filter value from the transient store and commits it to the // KVStore. The EVM end block logic doesn't update the validator set, thus it returns // an empty slice. func (k *Keeper) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { - // TODO: feat(evm): impl EndBlock + // TODO: Do we care about bloom here? return []abci.ValidatorUpdate{} } diff --git a/x/evm/keeper/keeper.go b/x/evm/keeper/keeper.go index 7f3a2a813..82cce516f 100644 --- a/x/evm/keeper/keeper.go +++ b/x/evm/keeper/keeper.go @@ -2,14 +2,23 @@ package keeper import ( - // "github.com/NibiruChain/nibiru/x/evm" "math/big" + "github.com/ethereum/go-ethereum/core" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + gethparams "github.com/ethereum/go-ethereum/params" + + sdkerrors "cosmossdk.io/errors" + "cosmossdk.io/math" + "github.com/cometbft/cometbft/libs/log" + "github.com/cosmos/cosmos-sdk/codec" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/NibiruChain/nibiru/app/appconst" "github.com/NibiruChain/nibiru/x/evm" ) @@ -28,6 +37,16 @@ type Keeper struct { bankKeeper evm.BankKeeper accountKeeper evm.AccountKeeper + stakingKeeper evm.StakingKeeper + + // Integer for the Ethereum EIP155 Chain ID + // eip155ChainIDInt *big.Int + hooks evm.EvmHooks //nolint:unused + precompiles map[gethcommon.Address]vm.PrecompiledContract //nolint:unused + // tracer: Configures the output type for a geth `vm.EVMLogger`. Tracer types + // include "access_list", "json", "struct", and "markdown". If any other + // value is used, a no operation tracer is set. + tracer string } func NewKeeper( @@ -36,6 +55,8 @@ func NewKeeper( authority sdk.AccAddress, accKeeper evm.AccountKeeper, bankKeeper evm.BankKeeper, + stakingKeeper evm.StakingKeeper, + tracer string, ) Keeper { if err := sdk.VerifyAddressFormat(authority); err != nil { panic(err) @@ -48,6 +69,8 @@ func NewKeeper( EvmState: NewEvmState(cdc, storeKey, transientKey), accountKeeper: accKeeper, bankKeeper: bankKeeper, + stakingKeeper: stakingKeeper, + tracer: tracer, } } @@ -65,3 +88,66 @@ func (k *Keeper) GetEvmGasBalance(ctx sdk.Context, addr gethcommon.Address) *big coin := k.bankKeeper.GetBalance(ctx, nibiruAddr, evmDenom) return coin.Amount.BigInt() } + +func (k Keeper) EthChainID(ctx sdk.Context) *big.Int { + return appconst.GetEthChainID(ctx.ChainID()) +} + +// AddToBlockGasUsed accumulate gas used by each eth msgs included in current +// block tx. +func (k Keeper) AddToBlockGasUsed( + ctx sdk.Context, gasUsed uint64, +) (uint64, error) { + result := k.EvmState.BlockGasUsed.GetOr(ctx, 0) + gasUsed + if result < gasUsed { + return 0, sdkerrors.Wrap(evm.ErrGasOverflow, "transient gas used") + } + k.EvmState.BlockGasUsed.Set(ctx, gasUsed) + return result, nil +} + +// GetMinGasMultiplier returns minimum gas multiplier. +func (k Keeper) GetMinGasMultiplier(ctx sdk.Context) math.LegacyDec { + return math.LegacyNewDecWithPrec(50, 2) // 50% +} + +func (k Keeper) GetBaseFee( + ctx sdk.Context, ethCfg *gethparams.ChainConfig, +) *big.Int { + isLondon := evm.IsLondon(ethCfg, ctx.BlockHeight()) + if !isLondon { + return nil + } + return big.NewInt(0) +} + +func (k Keeper) GetBaseFeeNoCfg( + ctx sdk.Context, +) *big.Int { + ethChainId := k.EthChainID(ctx) + ethCfg := k.GetParams(ctx).ChainConfig.EthereumConfig(ethChainId) + return k.GetBaseFee(ctx, ethCfg) +} + +// Logger returns a module-specific logger. +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", evm.ModuleName) +} + +// Tracer return a default vm.Tracer based on current keeper state +func (k Keeper) Tracer( + ctx sdk.Context, msg core.Message, ethCfg *gethparams.ChainConfig, +) vm.EVMLogger { + return evm.NewTracer(k.tracer, msg, ethCfg, ctx.BlockHeight()) +} + +// PostTxProcessing: Called after tx is processed successfully. If it errors, +// the tx will revert. +func (k *Keeper) PostTxProcessing( + ctx sdk.Context, msg core.Message, receipt *gethcore.Receipt, +) error { + if k.hooks == nil { + return nil + } + return k.hooks.PostTxProcessing(ctx, msg, receipt) +} diff --git a/x/evm/keeper/keeper_test.go b/x/evm/keeper/keeper_test.go index 8221cc854..caa757d22 100644 --- a/x/evm/keeper/keeper_test.go +++ b/x/evm/keeper/keeper_test.go @@ -3,11 +3,7 @@ package keeper_test import ( "testing" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/suite" - - "github.com/NibiruChain/nibiru/x/common" - "github.com/NibiruChain/nibiru/x/common/testutil/testapp" ) type KeeperSuite struct { @@ -19,61 +15,3 @@ func TestKeeperSuite(t *testing.T) { s := new(KeeperSuite) suite.Run(t, s) } - -func (s *KeeperSuite) TestMsgServer() { - chain, ctx := testapp.NewNibiruTestAppAndContext() - goCtx := sdk.WrapSDKContext(ctx) - k := chain.EvmKeeper - for _, testCase := range []func() error{ - func() error { - _, err := k.EthereumTx(goCtx, nil) - return err - }, - func() error { - _, err := k.UpdateParams(goCtx, nil) - return err - }, - } { - err := testCase() - s.Require().ErrorContains(err, common.ErrNotImplemented().Error()) - } -} - -func (s *KeeperSuite) TestQuerier() { - chain, ctx := testapp.NewNibiruTestAppAndContext() - goCtx := sdk.WrapSDKContext(ctx) - k := chain.EvmKeeper - for _, testCase := range []func() error{ - func() error { - _, err := k.ValidatorAccount(goCtx, nil) - return err - }, - func() error { - _, err := k.BaseFee(goCtx, nil) - return err - }, - func() error { - _, err := k.Code(goCtx, nil) - return err - }, - func() error { - _, err := k.EthCall(goCtx, nil) - return err - }, - func() error { - _, err := k.EstimateGas(goCtx, nil) - return err - }, - func() error { - _, err := k.TraceTx(goCtx, nil) - return err - }, - func() error { - _, err := k.TraceBlock(goCtx, nil) - return err - }, - } { - err := testCase() - s.Require().ErrorContains(err, common.ErrNotImplemented().Error()) - } -} diff --git a/x/evm/keeper/msg_ethereum_tx_test.go b/x/evm/keeper/msg_ethereum_tx_test.go new file mode 100644 index 000000000..ee8682f38 --- /dev/null +++ b/x/evm/keeper/msg_ethereum_tx_test.go @@ -0,0 +1,137 @@ +package keeper_test + +import ( + "math/big" + "strconv" + + "github.com/ethereum/go-ethereum/core" + gethcore "github.com/ethereum/go-ethereum/core/types" + gethparams "github.com/ethereum/go-ethereum/params" + + "github.com/NibiruChain/nibiru/x/evm/evmtest" +) + +func (s *KeeperSuite) TestMsgEthereumTx_CreateContract() { + testCases := []struct { + name string + scenario func() + }{ + { + name: "happy: deploy contract, sufficient gas limit", + scenario: func() { + deps := evmtest.NewTestDeps() + ethAcc := deps.Sender + + s.T().Log("create eth tx msg, increase gas limit") + gasLimit := new(big.Int).SetUint64( + gethparams.TxGasContractCreation + 100_000, + ) + args := evmtest.ArgsCreateContract{ + EthAcc: ethAcc, + EthChainIDInt: deps.K.EthChainID(deps.Ctx), + GasPrice: big.NewInt(1), + Nonce: deps.StateDB().GetNonce(ethAcc.EthAddr), + GasLimit: gasLimit, + } + ethTxMsg, err := evmtest.CreateContractTxMsg(args) + s.NoError(err) + s.Require().NoError(ethTxMsg.ValidateBasic()) + s.Equal(ethTxMsg.GetGas(), gasLimit.Uint64()) + + resp, err := deps.Chain.EvmKeeper.EthereumTx(deps.GoCtx(), ethTxMsg) + s.Require().NoError(err, "resp: %s\nblock header: %s", resp, deps.Ctx.BlockHeader().ProposerAddress) + }, + }, + { + name: "sad: deploy contract, exceed gas limit", + scenario: func() { + deps := evmtest.NewTestDeps() + ethAcc := deps.Sender + + s.T().Log("create eth tx msg, default create contract gas") + gasLimit := gethparams.TxGasContractCreation + args := evmtest.ArgsCreateContract{ + EthAcc: ethAcc, + EthChainIDInt: deps.K.EthChainID(deps.Ctx), + GasPrice: big.NewInt(1), + Nonce: deps.StateDB().GetNonce(ethAcc.EthAddr), + } + ethTxMsg, err := evmtest.CreateContractTxMsg(args) + s.NoError(err) + s.Require().NoError(ethTxMsg.ValidateBasic()) + s.Equal(ethTxMsg.GetGas(), gasLimit) + + resp, err := deps.Chain.EvmKeeper.EthereumTx(deps.GoCtx(), ethTxMsg) + s.Require().ErrorContains(err, core.ErrIntrinsicGas.Error(), "resp: %s\nblock header: %s", resp, deps.Ctx.BlockHeader().ProposerAddress) + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, tc.scenario) + } +} + +func (s *KeeperSuite) TestMsgEthereumTx_SimpleTransfer() { + testCases := []struct { + name string + scenario func() + }{ + { + name: "happy: AccessListTx", + scenario: func() { + deps := evmtest.NewTestDeps() + ethAcc := deps.Sender + + s.T().Log("create eth tx msg") + var innerTxData []byte = nil + var accessList gethcore.AccessList = nil + ethTxMsg, err := evmtest.NewEthTxMsgFromTxData( + &deps, + gethcore.AccessListTxType, + innerTxData, + deps.StateDB().GetNonce(ethAcc.EthAddr), + accessList, + ) + s.NoError(err) + + resp, err := deps.Chain.EvmKeeper.EthereumTx(deps.GoCtx(), ethTxMsg) + s.Require().NoError(err) + + gasUsed := strconv.FormatUint(resp.GasUsed, 10) + wantGasUsed := strconv.FormatUint(gethparams.TxGas, 10) + s.Equal(gasUsed, wantGasUsed) + }, + }, + { + name: "happy: LegacyTx", + scenario: func() { + deps := evmtest.NewTestDeps() + ethAcc := deps.Sender + + s.T().Log("create eth tx msg") + var innerTxData []byte = nil + var accessList gethcore.AccessList = nil + ethTxMsg, err := evmtest.NewEthTxMsgFromTxData( + &deps, + gethcore.LegacyTxType, + innerTxData, + deps.StateDB().GetNonce(ethAcc.EthAddr), + accessList, + ) + s.NoError(err) + + resp, err := deps.Chain.EvmKeeper.EthereumTx(deps.GoCtx(), ethTxMsg) + s.Require().NoError(err) + + gasUsed := strconv.FormatUint(resp.GasUsed, 10) + wantGasUsed := strconv.FormatUint(gethparams.TxGas, 10) + s.Equal(gasUsed, wantGasUsed) + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, tc.scenario) + } +} diff --git a/x/evm/keeper/msg_server.go b/x/evm/keeper/msg_server.go index ec94c9e4a..2b5221954 100644 --- a/x/evm/keeper/msg_server.go +++ b/x/evm/keeper/msg_server.go @@ -3,9 +3,27 @@ package keeper import ( "context" + "encoding/json" + "fmt" + "math/big" + "slices" + "strconv" - "github.com/NibiruChain/nibiru/x/common" + "cosmossdk.io/errors" + "cosmossdk.io/math" + tmbytes "github.com/cometbft/cometbft/libs/bytes" + tmtypes "github.com/cometbft/cometbft/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + gethcore "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" + + "github.com/NibiruChain/nibiru/eth" "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/statedb" ) var _ evm.MsgServer = &Keeper{} @@ -13,13 +31,483 @@ var _ evm.MsgServer = &Keeper{} func (k *Keeper) EthereumTx( goCtx context.Context, msg *evm.MsgEthereumTx, ) (resp *evm.MsgEthereumTxResponse, err error) { - // TODO: feat(evm): EthereumTx impl - return resp, common.ErrNotImplemented() + if err := msg.ValidateBasic(); err != nil { + return resp, errors.Wrap(err, "EthereumTx validate basic failed") + } + ctx := sdk.UnwrapSDKContext(goCtx) + + sender := msg.From + tx := msg.AsTransaction() + txIndex := k.EvmState.BlockTxIndex.GetOr(ctx, 0) + + resp, err = k.ApplyEvmTx(ctx, tx) + if err != nil { + return nil, errors.Wrap(err, "failed to apply transaction") + } + + attrs := []sdk.Attribute{ + sdk.NewAttribute(sdk.AttributeKeyAmount, tx.Value().String()), + // add event for ethereum transaction hash format + sdk.NewAttribute(evm.AttributeKeyEthereumTxHash, resp.Hash), + // add event for index of valid ethereum tx + sdk.NewAttribute(evm.AttributeKeyTxIndex, strconv.FormatUint(txIndex, 10)), + // add event for eth tx gas used, we can't get it from cosmos tx result when it contains multiple eth tx msgs. + sdk.NewAttribute(evm.AttributeKeyTxGasUsed, strconv.FormatUint(resp.GasUsed, 10)), + } + + if len(ctx.TxBytes()) > 0 { + // add event for tendermint transaction hash format + hash := tmbytes.HexBytes(tmtypes.Tx(ctx.TxBytes()).Hash()) + attrs = append(attrs, sdk.NewAttribute(evm.AttributeKeyTxHash, hash.String())) + } + + if to := tx.To(); to != nil { + attrs = append(attrs, sdk.NewAttribute(evm.AttributeKeyRecipient, to.Hex())) + } + + if resp.Failed() { + attrs = append(attrs, sdk.NewAttribute(evm.AttributeKeyEthereumTxFailed, resp.VmError)) + } + + txLogAttrs := make([]sdk.Attribute, len(resp.Logs)) + for i, log := range resp.Logs { + value, err := json.Marshal(log) + if err != nil { + return nil, errors.Wrap(err, "failed to encode log") + } + txLogAttrs[i] = sdk.NewAttribute(evm.AttributeKeyTxLog, string(value)) + } + + // emit events + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + evm.EventTypeEthereumTx, + attrs..., + ), + sdk.NewEvent( + evm.EventTypeTxLog, + txLogAttrs..., + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, evm.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, sender), + sdk.NewAttribute(evm.AttributeKeyTxType, fmt.Sprintf("%d", tx.Type())), + ), + }) + + return resp, nil +} + +func (k *Keeper) ApplyEvmTx( + ctx sdk.Context, tx *gethcore.Transaction, +) (*evm.MsgEthereumTxResponse, error) { + ethChainId := k.EthChainID(ctx) + cfg, err := k.GetEVMConfig(ctx, sdk.ConsAddress(ctx.BlockHeader().ProposerAddress), ethChainId) + if err != nil { + return nil, errors.Wrap(err, "failed to load evm config") + } + txConfig := k.TxConfig(ctx, tx.Hash()) + + // get the signer according to the chain rules from the config and block height + signer := gethcore.MakeSigner(cfg.ChainConfig, big.NewInt(ctx.BlockHeight())) + msg, err := tx.AsMessage(signer, cfg.BaseFee) + if err != nil { + return nil, errors.Wrap(err, "failed to return ethereum transaction as core message") + } + + // snapshot to contain the tx processing and post processing in same scope + var commit func() + tmpCtx := ctx + if k.hooks != nil { + // Create a cache context to revert state when tx hooks fails, + // the cache context is only committed when both tx and hooks executed successfully. + // Didn't use `Snapshot` because the context stack has exponential complexity on certain operations, + // thus restricted to be used only inside `ApplyMessage`. + tmpCtx, commit = ctx.CacheContext() + } + + // pass true to commit the StateDB + res, err := k.ApplyEvmMsg(tmpCtx, msg, nil, true, cfg, txConfig) + if err != nil { + // when a transaction contains multiple msg, as long as one of the msg fails + // all gas will be deducted. so is not msg.Gas() + k.ResetGasMeterAndConsumeGas(ctx, ctx.GasMeter().Limit()) + return nil, errors.Wrap(err, "failed to apply ethereum core message") + } + + logs := evm.LogsToEthereum(res.Logs) + + cumulativeGasUsed := res.GasUsed + if ctx.BlockGasMeter() != nil { + limit := ctx.BlockGasMeter().Limit() + cumulativeGasUsed += ctx.BlockGasMeter().GasConsumed() + if cumulativeGasUsed > limit { + cumulativeGasUsed = limit + } + } + + var contractAddr common.Address + if msg.To() == nil { + contractAddr = crypto.CreateAddress(msg.From(), msg.Nonce()) + } + + receipt := &gethcore.Receipt{ + Type: tx.Type(), + PostState: nil, // TODO: intermediate state root + CumulativeGasUsed: cumulativeGasUsed, + Bloom: k.EvmState.CalcBloomFromLogs(ctx, logs), + Logs: logs, + TxHash: txConfig.TxHash, + ContractAddress: contractAddr, + GasUsed: res.GasUsed, + BlockHash: txConfig.BlockHash, + BlockNumber: big.NewInt(ctx.BlockHeight()), + TransactionIndex: txConfig.TxIndex, + } + + if !res.Failed() { + receipt.Status = gethcore.ReceiptStatusSuccessful + // Only call hooks if tx executed successfully. + if err = k.PostTxProcessing(tmpCtx, msg, receipt); err != nil { + // If hooks return error, revert the whole tx. + res.VmError = evm.ErrPostTxProcessing.Error() + k.Logger(ctx).Error("tx post processing failed", "error", err) + + // If the tx failed in post processing hooks, we should clear the logs + res.Logs = nil + } else if commit != nil { + // PostTxProcessing is successful, commit the tmpCtx + commit() + // Since the post-processing can alter the log, we need to update the result + res.Logs = evm.NewLogsFromEth(receipt.Logs) + ctx.EventManager().EmitEvents(tmpCtx.EventManager().Events()) + } + } + + // refund gas in order to match the Ethereum gas consumption instead of the default SDK one. + if err = k.RefundGas(ctx, msg, msg.Gas()-res.GasUsed, cfg.Params.EvmDenom); err != nil { + return nil, errors.Wrapf(err, "failed to refund gas leftover gas to sender %s", msg.From()) + } + + if len(receipt.Logs) > 0 { + // Update transient block bloom filter + k.EvmState.BlockBloom.Set(ctx, receipt.Bloom.Bytes()) + blockLogSize := uint64(txConfig.LogIndex) + uint64(len(receipt.Logs)) + k.EvmState.BlockLogSize.Set(ctx, blockLogSize) + } + + blockTxIdx := uint64(txConfig.TxIndex) + 1 + k.EvmState.BlockTxIndex.Set(ctx, blockTxIdx) + + totalGasUsed, err := k.AddToBlockGasUsed(ctx, res.GasUsed) + if err != nil { + return nil, errors.Wrap(err, "failed to add transient gas used") + } + + // reset the gas meter for current cosmos transaction + k.ResetGasMeterAndConsumeGas(ctx, totalGasUsed) + return res, nil +} + +// ApplyEvmMsgWithEmptyTxConfig; Computes new state by applyig the EVM +// message to the given state. This function calls [Keeper.ApplyEvmMsg] with +// and empty`statedb.TxConfig`. +// See [Keeper.ApplyEvmMsg]. +func (k *Keeper) ApplyEvmMsgWithEmptyTxConfig( + ctx sdk.Context, msg core.Message, tracer vm.EVMLogger, commit bool, +) (*evm.MsgEthereumTxResponse, error) { + cfg, err := k.GetEVMConfig(ctx, sdk.ConsAddress(ctx.BlockHeader().ProposerAddress), k.EthChainID(ctx)) + if err != nil { + return nil, errors.Wrap(err, "failed to load evm config") + } + + txConfig := statedb.NewEmptyTxConfig(common.BytesToHash(ctx.HeaderHash())) + return k.ApplyEvmMsg(ctx, msg, tracer, commit, cfg, txConfig) +} + +// NewEVM generates a go-ethereum VM. +// +// Args: +// - ctx: Consensus and KV store info for the current block. +// - msg: Ethereum message sent to a contract +// - cfg: Encapsulates params required to construct an EVM. +// - tracer: Collects execution traces for EVM transaction logging. +// - stateDB: Holds the EVM state. +// +// [NewEVM] sets the validator operator address as the coinbase address to make +// it available for the COINBASE opcode. This is done for backwards +// compatibility. There is no benficiary of the COINBASE tx opcode because we use +// post-merge Ethereum (Proof of Stake rather than Proof of Work). +func (k *Keeper) NewEVM( + ctx sdk.Context, + msg core.Message, + cfg *statedb.EVMConfig, + tracer vm.EVMLogger, + stateDB vm.StateDB, +) *vm.EVM { + blockCtx := vm.BlockContext{ + CanTransfer: core.CanTransfer, + Transfer: core.Transfer, + GetHash: k.GetHashFn(ctx), + Coinbase: cfg.CoinBase, + GasLimit: eth.BlockGasLimit(ctx), + BlockNumber: big.NewInt(ctx.BlockHeight()), + Time: big.NewInt(ctx.BlockHeader().Time.Unix()), + Difficulty: big.NewInt(0), // unused. Only required in PoW context + BaseFee: cfg.BaseFee, + Random: nil, // not supported + } + + txCtx := core.NewEVMTxContext(msg) + if tracer == nil { + tracer = k.Tracer(ctx, msg, cfg.ChainConfig) + } + vmConfig := k.VMConfig(ctx, msg, cfg, tracer) + return vm.NewEVM(blockCtx, txCtx, stateDB, cfg.ChainConfig, vmConfig) } -func (k *Keeper) UpdateParams( - goCtx context.Context, msg *evm.MsgUpdateParams, -) (resp *evm.MsgUpdateParamsResponse, err error) { - // TODO: feat(evm): UpdateParams impl - return resp, common.ErrNotImplemented() +// GetHashFn implements vm.GetHashFunc for Ethermint. It handles 3 cases: +// 1. The requested height matches the current height from context (and thus same epoch number) +// 2. The requested height is from an previous height from the same chain epoch +// 3. The requested height is from a height greater than the latest one +func (k Keeper) GetHashFn(ctx sdk.Context) vm.GetHashFunc { + return func(height uint64) common.Hash { + h, err := eth.SafeInt64(height) + if err != nil { + k.Logger(ctx).Error("failed to cast height to int64", "error", err) + return common.Hash{} + } + + switch { + case ctx.BlockHeight() == h: + // Case 1: The requested height matches the one from the context so we can retrieve the header + // hash directly from the context. + // Note: The headerHash is only set at begin block, it will be nil in case of a query context + headerHash := ctx.HeaderHash() + if len(headerHash) != 0 { + return common.BytesToHash(headerHash) + } + + // only recompute the hash if not set (eg: checkTxState) + contextBlockHeader := ctx.BlockHeader() + header, err := tmtypes.HeaderFromProto(&contextBlockHeader) + if err != nil { + k.Logger(ctx).Error("failed to cast tendermint header from proto", "error", err) + return common.Hash{} + } + + headerHash = header.Hash() + return common.BytesToHash(headerHash) + + case ctx.BlockHeight() > h: + // Case 2: if the chain is not the current height we need to retrieve the hash from the store for the + // current chain epoch. This only applies if the current height is greater than the requested height. + histInfo, found := k.stakingKeeper.GetHistoricalInfo(ctx, h) + if !found { + k.Logger(ctx).Debug("historical info not found", "height", h) + return common.Hash{} + } + + header, err := tmtypes.HeaderFromProto(&histInfo.Header) + if err != nil { + k.Logger(ctx).Error("failed to cast tendermint header from proto", "error", err) + return common.Hash{} + } + + return common.BytesToHash(header.Hash()) + default: + // Case 3: heights greater than the current one returns an empty hash. + return common.Hash{} + } + } +} + +// ApplyEvmMsg computes the new state by applying the given message against the existing state. +// If the message fails, the VM execution error with the reason will be returned to the client +// and the transaction won't be committed to the store. +// +// # Reverted state +// +// The snapshot and rollback are supported by the `statedb.StateDB`. +// +// # Different Callers +// +// It's called in three scenarios: +// 1. `ApplyTransaction`, in the transaction processing flow. +// 2. `EthCall/EthEstimateGas` grpc query handler. +// 3. Called by other native modules directly. +// +// # Prechecks and Preprocessing +// +// All relevant state transition prechecks for the MsgEthereumTx are performed on the AnteHandler, +// prior to running the transaction against the state. The prechecks run are the following: +// +// 1. the nonce of the message caller is correct +// 2. caller has enough balance to cover transaction fee(gaslimit * gasprice) +// 3. the amount of gas required is available in the block +// 4. the purchased gas is enough to cover intrinsic usage +// 5. there is no overflow when calculating intrinsic gas +// 6. caller has enough balance to cover asset transfer for **topmost** call +// +// The preprocessing steps performed by the AnteHandler are: +// +// 1. set up the initial access list (iff fork > Berlin) +// +// # Tracer parameter +// +// It should be a `vm.Tracer` object or nil, if pass `nil`, it'll create a default one based on keeper options. +// +// # Commit parameter +// +// If commit is true, the `StateDB` will be committed, otherwise discarded. +func (k *Keeper) ApplyEvmMsg(ctx sdk.Context, + msg core.Message, + tracer vm.EVMLogger, + commit bool, + cfg *statedb.EVMConfig, + txConfig statedb.TxConfig, +) (*evm.MsgEthereumTxResponse, error) { + var ( + ret []byte // return bytes from evm execution + vmErr error // vm errors do not effect consensus and are therefore not assigned to err + ) + + // return error if contract creation or call are disabled through governance + if !cfg.Params.EnableCreate && msg.To() == nil { + return nil, errors.Wrap(evm.ErrCreateDisabled, "failed to create new contract") + } else if !cfg.Params.EnableCall && msg.To() != nil { + return nil, errors.Wrap(evm.ErrCallDisabled, "failed to call contract") + } + + stateDB := statedb.New(ctx, k, txConfig) + evmObj := k.NewEVM(ctx, msg, cfg, tracer, stateDB) + + // set the custom precompiles to the EVM (if any) + if cfg.Params.HasCustomPrecompiles() { + customPrecompiles := cfg.Params.GetActivePrecompilesAddrs() + + activePrecompiles := make([]common.Address, len(vm.PrecompiledAddressesBerlin)+len(customPrecompiles)) + copy(activePrecompiles[:len(vm.PrecompiledAddressesBerlin)], vm.PrecompiledAddressesBerlin) + copy(activePrecompiles[len(vm.PrecompiledAddressesBerlin):], customPrecompiles) + + // Check if the transaction is sent to an inactive precompile + // + // NOTE: This has to be checked here instead of in the actual evm.Call method + // because evm.WithPrecompiles only populates the EVM with the active precompiles, + // so there's no telling if the To address is an inactive precompile further down the call stack. + toAddr := msg.To() + if toAddr != nil && + slices.Contains(evm.AvailableEVMExtensions, toAddr.String()) && + !slices.Contains(activePrecompiles, *toAddr) { + return nil, errors.Wrap(evm.ErrInactivePrecompile, "failed to call precompile") + } + + // NOTE: this only adds active precompiles to the EVM. + // This means that evm.Precompile(addr) will return false for inactive precompiles + // even though this is actually a reserved address. + precompileMap := k.Precompiles(activePrecompiles...) + evmObj.WithPrecompiles(precompileMap, activePrecompiles) + } + + leftoverGas := msg.Gas() + + // Allow the tracer captures the tx level events, mainly the gas consumption. + vmCfg := evmObj.Config + if vmCfg.Debug { + vmCfg.Tracer.CaptureTxStart(leftoverGas) + defer func() { + vmCfg.Tracer.CaptureTxEnd(leftoverGas) + }() + } + + sender := vm.AccountRef(msg.From()) + contractCreation := msg.To() == nil + isLondon := cfg.ChainConfig.IsLondon(evmObj.Context.BlockNumber) + + intrinsicGas, err := k.GetEthIntrinsicGas(ctx, msg, cfg.ChainConfig, contractCreation) + if err != nil { + // should have already been checked on Ante Handler + return nil, errors.Wrap(err, "intrinsic gas failed") + } + + // Should check again even if it is checked on Ante Handler, because eth_call don't go through Ante Handler. + if leftoverGas < intrinsicGas { + // eth_estimateGas will check for this exact error + return nil, errors.Wrap(core.ErrIntrinsicGas, "apply message") + } + leftoverGas -= intrinsicGas + + // access list preparation is moved from ante handler to here, because it's needed when `ApplyMessage` is called + // under contexts where ante handlers are not run, for example `eth_call` and `eth_estimateGas`. + if rules := cfg.ChainConfig.Rules(big.NewInt(ctx.BlockHeight()), cfg.ChainConfig.MergeNetsplitBlock != nil); rules.IsBerlin { + stateDB.PrepareAccessList(msg.From(), msg.To(), evmObj.ActivePrecompiles(rules), msg.AccessList()) + } + + if contractCreation { + // take over the nonce management from evm: + // - reset sender's nonce to msg.Nonce() before calling evm. + // - increase sender's nonce by one no matter the result. + stateDB.SetNonce(sender.Address(), msg.Nonce()) + ret, _, leftoverGas, vmErr = evmObj.Create(sender, msg.Data(), leftoverGas, msg.Value()) + stateDB.SetNonce(sender.Address(), msg.Nonce()+1) + } else { + ret, leftoverGas, vmErr = evmObj.Call(sender, *msg.To(), msg.Data(), leftoverGas, msg.Value()) + } + + refundQuotient := params.RefundQuotient + + // After EIP-3529: refunds are capped to gasUsed / 5 + if isLondon { + refundQuotient = params.RefundQuotientEIP3529 + } + + // calculate gas refund + if msg.Gas() < leftoverGas { + return nil, errors.Wrap(evm.ErrGasOverflow, "apply message") + } + // refund gas + temporaryGasUsed := msg.Gas() - leftoverGas + refund := GasToRefund(stateDB.GetRefund(), temporaryGasUsed, refundQuotient) + + // update leftoverGas and temporaryGasUsed with refund amount + leftoverGas += refund + temporaryGasUsed -= refund + + // EVM execution error needs to be available for the JSON-RPC client + var vmError string + if vmErr != nil { + vmError = vmErr.Error() + } + + // The dirty states in `StateDB` is either committed or discarded after return + if commit { + if err := stateDB.Commit(); err != nil { + return nil, errors.Wrap(err, "failed to commit stateDB") + } + } + + gasLimit := math.LegacyNewDec(int64(msg.Gas())) + minGasMultiplier := k.GetMinGasMultiplier(ctx) + minimumGasUsed := gasLimit.Mul(minGasMultiplier) + + if !minimumGasUsed.TruncateInt().IsUint64() { + return nil, errors.Wrapf(evm.ErrGasOverflow, "minimumGasUsed(%s) is not a uint64", minimumGasUsed.TruncateInt().String()) + } + + if msg.Gas() < leftoverGas { + return nil, errors.Wrapf(evm.ErrGasOverflow, "message gas limit < leftover gas (%d < %d)", msg.Gas(), leftoverGas) + } + + gasUsed := math.LegacyMaxDec(minimumGasUsed, math.LegacyNewDec(int64(temporaryGasUsed))).TruncateInt().Uint64() + // reset leftoverGas, to be used by the tracer + leftoverGas = msg.Gas() - gasUsed + + return &evm.MsgEthereumTxResponse{ + GasUsed: gasUsed, + VmError: vmError, + Ret: ret, + Logs: evm.NewLogsFromEth(stateDB.Logs()), + Hash: txConfig.TxHash.Hex(), + }, nil } diff --git a/x/evm/keeper/msg_update_params.go b/x/evm/keeper/msg_update_params.go new file mode 100644 index 000000000..c4a49c1c3 --- /dev/null +++ b/x/evm/keeper/msg_update_params.go @@ -0,0 +1,23 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package keeper + +import ( + "context" + + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/NibiruChain/nibiru/x/evm" +) + +func (k *Keeper) UpdateParams( + goCtx context.Context, req *evm.MsgUpdateParams, +) (resp *evm.MsgUpdateParamsResponse, err error) { + if k.authority.String() != req.Authority { + return nil, errors.Wrapf(govtypes.ErrInvalidSigner, "invalid authority, expected %s, got %s", k.authority.String(), req.Authority) + } + ctx := sdk.UnwrapSDKContext(goCtx) + k.SetParams(ctx, req.Params) + return &evm.MsgUpdateParamsResponse{}, nil +} diff --git a/x/evm/keeper/precompiles.go b/x/evm/keeper/precompiles.go new file mode 100644 index 000000000..be7b0d0be --- /dev/null +++ b/x/evm/keeper/precompiles.go @@ -0,0 +1,113 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package keeper + +import ( + "bytes" + "fmt" + "maps" + "sort" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" +) + +func AvailablePrecompiles() map[common.Address]vm.PrecompiledContract { + contractMap := make(map[common.Address]vm.PrecompiledContract) + // The following TODOs can go in an epic together. + // TODO: feat(evm): implement precompiled contracts for fungible tokens + // TODO: feat(evm): implement precompiled contracts for ibc transfer + // TODO: feat(evm): implement precompiled contracts for staking + // TODO: feat(evm): implement precompiled contracts for wasm calls + return contractMap +} + +// WithPrecompiles sets the available precompiled contracts. +func (k *Keeper) WithPrecompiles(precompiles map[common.Address]vm.PrecompiledContract) *Keeper { + if k.precompiles != nil { + panic("available precompiles map already set") + } + + k.precompiles = precompiles + return k +} + +// Precompiles returns the subset of the available precompiled contracts that +// are active given the current parameters. +func (k Keeper) Precompiles( + activePrecompiles ...common.Address, +) map[common.Address]vm.PrecompiledContract { + activePrecompileMap := make(map[common.Address]vm.PrecompiledContract) + + for _, address := range activePrecompiles { + precompile, ok := k.precompiles[address] + if !ok { + panic(fmt.Sprintf("precompiled contract not initialized: %s", address)) + } + + activePrecompileMap[address] = precompile + } + + return activePrecompileMap +} + +// AddEVMExtensions adds the given precompiles to the list of active precompiles in the EVM parameters +// and to the available precompiles map in the Keeper. This function returns an error if +// the precompiles are invalid or duplicated. +func (k *Keeper) AddEVMExtensions( + ctx sdk.Context, precompiles ...vm.PrecompiledContract, +) error { + params := k.GetParams(ctx) + + addresses := make([]string, len(precompiles)) + precompilesMap := maps.Clone(k.precompiles) + + for i, precompile := range precompiles { + // add to active precompiles + address := precompile.Address() + addresses[i] = address.String() + + // add to available precompiles, but check for duplicates + if _, ok := precompilesMap[address]; ok { + return fmt.Errorf("precompile already registered: %s", address) + } + precompilesMap[address] = precompile + } + + params.ActivePrecompiles = append(params.ActivePrecompiles, addresses...) + + // NOTE: the active precompiles are sorted and validated before setting them + // in the params + k.SetParams(ctx, params) + // update the pointer to the map with the newly added EVM Extensions + k.precompiles = precompilesMap + return nil +} + +// IsAvailablePrecompile returns true if the given precompile address is contained in the +// EVM keeper's available precompiles map. +func (k Keeper) IsAvailablePrecompile(address common.Address) bool { + _, ok := k.precompiles[address] + return ok +} + +// GetAvailablePrecompileAddrs returns the list of available precompile addresses. +// +// NOTE: uses index based approach instead of append because it's supposed to be faster. +// Check https://stackoverflow.com/questions/21362950/getting-a-slice-of-keys-from-a-map. +func (k Keeper) GetAvailablePrecompileAddrs() []common.Address { + addresses := make([]common.Address, len(k.precompiles)) + i := 0 + + //#nosec G705 -- two operations in for loop here are fine + for address := range k.precompiles { + addresses[i] = address + i++ + } + + sort.Slice(addresses, func(i, j int) bool { + return bytes.Compare(addresses[i].Bytes(), addresses[j].Bytes()) == -1 + }) + + return addresses +} diff --git a/x/evm/keeper/statedb.go b/x/evm/keeper/statedb.go index bb243c806..3eb751999 100644 --- a/x/evm/keeper/statedb.go +++ b/x/evm/keeper/statedb.go @@ -4,6 +4,8 @@ package keeper import ( "math/big" + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" gethcommon "github.com/ethereum/go-ethereum/common" @@ -12,6 +14,127 @@ import ( "github.com/NibiruChain/nibiru/x/evm/statedb" ) +var _ statedb.Keeper = &Keeper{} + +// ---------------------------------------------------------------------------- +// StateDB Keeper implementation +// ---------------------------------------------------------------------------- + +// GetAccount: Ethereum account getter for a [statedb.Account]. +// Implements the `statedb.Keeper` interface. +// Returns nil if the account does not not exist or has the wrong type. +func (k *Keeper) GetAccount(ctx sdk.Context, addr gethcommon.Address) *statedb.Account { + acct := k.GetAccountWithoutBalance(ctx, addr) + if acct == nil { + return nil + } + + acct.Balance = k.GetEvmGasBalance(ctx, addr) + return acct +} + +// GetCode: Loads smart contract bytecode. +// Implements the `statedb.Keeper` interface. +func (k *Keeper) GetCode(ctx sdk.Context, codeHash gethcommon.Hash) []byte { + codeBz, err := k.EvmState.ContractBytecode.Get(ctx, codeHash.Bytes()) + if err != nil { + panic(err) // TODO: We don't like to panic. + } + return codeBz +} + +// ForEachStorage: Iterator over contract storage. +// Implements the `statedb.Keeper` interface. +func (k *Keeper) ForEachStorage( + ctx sdk.Context, + addr gethcommon.Address, + stopIter func(key, value gethcommon.Hash) bool, +) { + store := ctx.KVStore(k.storeKey) + prefix := evm.PrefixAccStateEthAddr(addr) + + iterator := sdk.KVStorePrefixIterator(store, prefix) + defer iterator.Close() + + for ; iterator.Valid(); iterator.Next() { + key := gethcommon.BytesToHash(iterator.Key()) + value := gethcommon.BytesToHash(iterator.Value()) + + // check if iteration stops + if !stopIter(key, value) { + return + } + } +} + +// SetAccBalance update account's balance, compare with current balance first, then decide to mint or burn. +func (k *Keeper) SetAccBalance( + ctx sdk.Context, addr gethcommon.Address, amount *big.Int, +) error { + nativeAddr := sdk.AccAddress(addr.Bytes()) + params := k.GetParams(ctx) + coin := k.bankKeeper.GetBalance(ctx, nativeAddr, params.EvmDenom) + balance := coin.Amount.BigInt() + delta := new(big.Int).Sub(amount, balance) + switch delta.Sign() { + case 1: + // mint + coins := sdk.NewCoins(sdk.NewCoin(params.EvmDenom, sdkmath.NewIntFromBigInt(delta))) + if err := k.bankKeeper.MintCoins(ctx, evm.ModuleName, coins); err != nil { + return err + } + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, evm.ModuleName, nativeAddr, coins); err != nil { + return err + } + case -1: + // burn + coins := sdk.NewCoins(sdk.NewCoin(params.EvmDenom, sdkmath.NewIntFromBigInt(new(big.Int).Neg(delta)))) + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, nativeAddr, evm.ModuleName, coins); err != nil { + return err + } + if err := k.bankKeeper.BurnCoins(ctx, evm.ModuleName, coins); err != nil { + return err + } + default: + // not changed + } + return nil +} + +// SetAccount: Updates nonce, balance, and codeHash. +// Implements the `statedb.Keeper` interface. +// Only called by `StateDB.Commit()`. +func (k *Keeper) SetAccount( + ctx sdk.Context, addr gethcommon.Address, account statedb.Account, +) error { + // update account + nibiruAddr := sdk.AccAddress(addr.Bytes()) + acct := k.accountKeeper.GetAccount(ctx, nibiruAddr) + if acct == nil { + acct = k.accountKeeper.NewAccountWithAddress(ctx, nibiruAddr) + } + + if err := acct.SetSequence(account.Nonce); err != nil { + return err + } + + codeHash := gethcommon.BytesToHash(account.CodeHash) + + if ethAcct, ok := acct.(eth.EthAccountI); ok { + if err := ethAcct.SetCodeHash(codeHash); err != nil { + return err + } + } + + k.accountKeeper.SetAccount(ctx, acct) + + if err := k.SetAccBalance(ctx, addr, account.Balance); err != nil { + return err + } + + return nil +} + // SetState: Update contract storage, delete if value is empty. // Implements the `statedb.Keeper` interface. // Only called by `StateDB.Commit()`. @@ -28,32 +151,36 @@ func (k *Keeper) SetCode(ctx sdk.Context, codeHash, code []byte) { k.EvmState.SetAccCode(ctx, codeHash, code) } -// GetAccount: Ethereum account getter for a [statedb.Account]. +// DeleteAccount handles contract's suicide call, clearing the balance, contract +// bytecode, contract state, and its native account. // Implements the `statedb.Keeper` interface. -// Returns nil if the account does not not exist or has the wrong type. -func (k *Keeper) GetAccount(ctx sdk.Context, addr gethcommon.Address) *statedb.Account { - acct := k.GetAccountWithoutBalance(ctx, addr) +// Only called by `StateDB.Commit()`. +func (k *Keeper) DeleteAccount(ctx sdk.Context, addr gethcommon.Address) error { + nibiruAddr := sdk.AccAddress(addr.Bytes()) + acct := k.accountKeeper.GetAccount(ctx, nibiruAddr) if acct == nil { return nil } - acct.Balance = k.GetEvmGasBalance(ctx, addr) - return acct -} - -// GetAccountOrEmpty returns empty account if not exist, returns error if it's not `EthAccount` -func (k *Keeper) GetAccountOrEmpty( - ctx sdk.Context, addr gethcommon.Address, -) statedb.Account { - acct := k.GetAccount(ctx, addr) - if acct != nil { - return *acct + _, ok := acct.(eth.EthAccountI) + if !ok { + return evm.ErrInvalidAccount.Wrapf("type %T, address %s", acct, addr) } - // empty account - return statedb.Account{ - Balance: new(big.Int), - CodeHash: evm.EmptyCodeHash, + + // clear balance + if err := k.SetAccBalance(ctx, addr, new(big.Int)); err != nil { + return err } + + // clear storage + k.ForEachStorage(ctx, addr, func(key, _ gethcommon.Hash) bool { + k.SetState(ctx, addr, key, nil) + return true + }) + + k.accountKeeper.RemoveAccount(ctx, acct) + + return nil } // GetAccountWithoutBalance load nonce and codehash without balance, @@ -76,3 +203,19 @@ func (k *Keeper) GetAccountWithoutBalance(ctx sdk.Context, addr gethcommon.Addre CodeHash: codeHash, } } + +// GetAccountOrEmpty returns empty account if not exist, returns error if it's not `EthAccount` +func (k *Keeper) GetAccountOrEmpty( + ctx sdk.Context, addr gethcommon.Address, +) statedb.Account { + acct := k.GetAccount(ctx, addr) + if acct != nil { + return *acct + } + + // empty account + return statedb.Account{ + Balance: new(big.Int), + CodeHash: evm.EmptyCodeHash, + } +} diff --git a/x/evm/keeper/vm_config.go b/x/evm/keeper/vm_config.go new file mode 100644 index 000000000..dd26702ef --- /dev/null +++ b/x/evm/keeper/vm_config.go @@ -0,0 +1,99 @@ +// Copyright (c) 2023-2024 Nibi, Inc. +package keeper + +import ( + "math/big" + + "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/statedb" +) + +func (k *Keeper) GetEVMConfig( + ctx sdk.Context, proposerAddress sdk.ConsAddress, chainID *big.Int, +) (*statedb.EVMConfig, error) { + params := k.GetParams(ctx) + ethCfg := params.ChainConfig.EthereumConfig(chainID) + + // get the coinbase address from the block proposer + coinbase, err := k.GetCoinbaseAddress(ctx, proposerAddress) + if err != nil { + return nil, errors.Wrap(err, "failed to obtain coinbase address") + } + + baseFee := k.GetBaseFee(ctx, ethCfg) + return &statedb.EVMConfig{ + Params: params, + ChainConfig: ethCfg, + CoinBase: coinbase, + BaseFee: baseFee, + }, nil +} + +// TxConfig loads `TxConfig` from current transient storage +func (k *Keeper) TxConfig( + ctx sdk.Context, txHash common.Hash, +) statedb.TxConfig { + return statedb.NewTxConfig( + common.BytesToHash(ctx.HeaderHash()), // BlockHash + txHash, // TxHash + uint(k.EvmState.BlockTxIndex.GetOr(ctx, 0)), // TxIndex + uint(k.EvmState.BlockLogSize.GetOr(ctx, 0)), // LogIndex + ) +} + +// DEFAULT_NO_BASE_FEE: Toggles whether or not a base fee will be used. It should +// always be on, since Nibiru EVM starts from a post-London upgrade state. +const DEFAULT_NO_BASE_FEE = false + +// VMConfig creates an EVM configuration from the debug setting and the extra +// EIPs enabled on the module parameters. The config generated uses the default +// JumpTable from the EVM. +func (k Keeper) VMConfig( + ctx sdk.Context, _ core.Message, cfg *statedb.EVMConfig, tracer vm.EVMLogger, +) vm.Config { + noBaseFee := DEFAULT_NO_BASE_FEE + var debug bool + if _, ok := tracer.(evm.NoOpTracer); !ok { + debug = true + } + + return vm.Config{ + Debug: debug, + Tracer: tracer, + NoBaseFee: noBaseFee, + ExtraEips: cfg.Params.EIPs(), + } +} + +// GetCoinbaseAddress returns the block proposer's validator operator address. +func (k Keeper) GetCoinbaseAddress(ctx sdk.Context, proposerAddress sdk.ConsAddress) (common.Address, error) { + validator, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, ParseProposerAddr(ctx, proposerAddress)) + if !found { + return common.Address{}, errors.Wrapf( + stakingtypes.ErrNoValidatorFound, + "failed to retrieve validator from block proposer address %s", + proposerAddress.String(), + ) + } + + coinbase := common.BytesToAddress(validator.GetOperator()) + return coinbase, nil +} + +// ParseProposerAddr returns current block proposer's address when provided +// proposer address is empty. +func ParseProposerAddr( + ctx sdk.Context, proposerAddress sdk.ConsAddress, +) sdk.ConsAddress { + if len(proposerAddress) == 0 { + proposerAddress = ctx.BlockHeader().ProposerAddress + } + return proposerAddress +} diff --git a/x/evm/logs_test.go b/x/evm/logs_test.go index 123ddcf17..9d83d311c 100644 --- a/x/evm/logs_test.go +++ b/x/evm/logs_test.go @@ -12,7 +12,7 @@ import ( ) func TestTransactionLogsValidate(t *testing.T) { - addr := evmtest.NewEthAddr().String() + addr := evmtest.NewEthAccInfo().EthAddr.String() testCases := []struct { name string @@ -96,7 +96,7 @@ func TestTransactionLogsValidate(t *testing.T) { } func TestValidateLog(t *testing.T) { - addr := evmtest.NewEthAddr().String() + addr := evmtest.NewEthAccInfo().EthAddr.String() testCases := []struct { name string @@ -169,7 +169,7 @@ func TestValidateLog(t *testing.T) { } func TestConversionFunctions(t *testing.T) { - addr := evmtest.NewEthAddr().String() + addr := evmtest.NewEthAccInfo().EthAddr.String() txLogs := evm.TransactionLogs{ Hash: common.BytesToHash([]byte("tx_hash")).String(), diff --git a/x/evm/msg.go b/x/evm/msg.go index ba8a9e830..d67ba6a97 100644 --- a/x/evm/msg.go +++ b/x/evm/msg.go @@ -449,3 +449,26 @@ func DecodeTxResponse(in []byte) (*MsgEthereumTxResponse, error) { } var EmptyCodeHash = crypto.Keccak256(nil) + +// BinSearch executes the binary search and hone in on an executable gas limit +func BinSearch( + lo, hi uint64, executable func(uint64) (bool, *MsgEthereumTxResponse, error), +) (uint64, error) { + for lo+1 < hi { + mid := (hi + lo) / 2 + failed, _, err := executable(mid) + // If this errors, there was a consensus error, and the provided message + // call or tx will never be accepted, regardless of how high we set the + // gas limit. + // Return the error directly, don't struggle any more. + if err != nil { + return 0, err + } + if failed { + lo = mid + } else { + hi = mid + } + } + return hi, nil +} diff --git a/x/evm/msg_test.go b/x/evm/msg_test.go index 0bdaa24c9..bdc06d906 100644 --- a/x/evm/msg_test.go +++ b/x/evm/msg_test.go @@ -45,11 +45,12 @@ func TestMsgsSuite(t *testing.T) { } func (s *MsgsSuite) SetupTest() { - from, privFrom := evmtest.PrivKeyEth() + ethAcc := evmtest.NewEthAccInfo() + from, privFrom := ethAcc.EthAddr, ethAcc.PrivKey s.signer = evmtest.NewSigner(privFrom) s.from = from - s.to = evmtest.NewEthAddr() + s.to = evmtest.NewEthAccInfo().EthAddr s.chainID = big.NewInt(1) s.hundredBigInt = big.NewInt(100) @@ -957,7 +958,7 @@ func (s *MsgsSuite) TestUnwrapEthererumMsg() { } func (s *MsgsSuite) TestTransactionLogsEncodeDecode() { - addr := evmtest.NewEthAddr().String() + addr := evmtest.NewEthAccInfo().EthAddr.String() txLogs := evm.TransactionLogs{ Hash: common.BytesToHash([]byte("tx_hash")).String(), diff --git a/x/evm/query.go b/x/evm/query.go index 79235797d..f6d9cfde6 100644 --- a/x/evm/query.go +++ b/x/evm/query.go @@ -31,7 +31,7 @@ func (m QueryTraceBlockRequest) UnpackInterfaces(unpacker codectypes.AnyUnpacker func (req *QueryEthAccountRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if err := eth.ValidateAddress(req.Address); err != nil { return status.Error( @@ -43,7 +43,7 @@ func (req *QueryEthAccountRequest) Validate() error { func (req *QueryNibiruAccountRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if err := eth.ValidateAddress(req.Address); err != nil { @@ -72,7 +72,7 @@ func (req *QueryValidatorAccountRequest) Validate() ( func (req *QueryBalanceRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if err := eth.ValidateAddress(req.Address); err != nil { @@ -86,7 +86,7 @@ func (req *QueryBalanceRequest) Validate() error { func (req *QueryStorageRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if err := eth.ValidateAddress(req.Address); err != nil { return status.Error( @@ -99,7 +99,7 @@ func (req *QueryStorageRequest) Validate() error { func (req *QueryCodeRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if err := eth.ValidateAddress(req.Address); err != nil { @@ -113,14 +113,14 @@ func (req *QueryCodeRequest) Validate() error { func (req *EthCallRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } return nil } func (req *QueryTraceTxRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if req.TraceConfig != nil && req.TraceConfig.Limit < 0 { @@ -131,7 +131,7 @@ func (req *QueryTraceTxRequest) Validate() error { func (req *QueryTraceBlockRequest) Validate() error { if req == nil { - return common.ErrNilGrpcMsg() + return common.ErrNilGrpcMsg } if req.TraceConfig != nil && req.TraceConfig.Limit < 0 { diff --git a/x/evm/query_test.go b/x/evm/query_test.go index f32a9704b..7c6e54992 100644 --- a/x/evm/query_test.go +++ b/x/evm/query_test.go @@ -53,6 +53,6 @@ func (s *TestSuite) TestNilQueries() { }, } { err := testCase() - s.Require().ErrorContains(err, common.ErrNilGrpcMsg().Error()) + s.Require().ErrorContains(err, common.ErrNilGrpcMsg.Error()) } } diff --git a/x/evm/statedb/statedb.go b/x/evm/statedb/statedb.go index e06ef3708..4beaf40ae 100644 --- a/x/evm/statedb/statedb.go +++ b/x/evm/statedb/statedb.go @@ -467,3 +467,12 @@ func (s *StateDB) Commit() error { } return nil } + +// StateObjects: Returns a copy of the [StateDB.stateObjects] map. +func (s *StateDB) StateObjects() map[common.Address]*stateObject { + copyOfMap := make(map[common.Address]*stateObject) + for key, val := range s.stateObjects { + copyOfMap[key] = val + } + return copyOfMap +} diff --git a/x/evm/tx_test.go b/x/evm/tx_test.go index 929a599c2..12837a51d 100644 --- a/x/evm/tx_test.go +++ b/x/evm/tx_test.go @@ -45,7 +45,7 @@ func (suite *TxDataTestSuite) SetupTest() { suite.sdkMinusOneInt = sdkmath.NewInt(-1) suite.invalidAddr = "123456" - suite.addr = evmtest.NewEthAddr() + suite.addr = evmtest.NewEthAccInfo().EthAddr suite.hexAddr = suite.addr.Hex() suite.hexDataBytes = hexutil.Bytes([]byte("data")) diff --git a/x/evm/vmtracer.go b/x/evm/vmtracer.go index 5438d778d..3eeddde9f 100644 --- a/x/evm/vmtracer.go +++ b/x/evm/vmtracer.go @@ -11,6 +11,8 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/tracers/logger" "github.com/ethereum/go-ethereum/params" + + "github.com/NibiruChain/nibiru/x/common/set" ) const ( @@ -20,6 +22,10 @@ const ( TracerMarkdown = "markdown" ) +func TracerTypes() set.Set[string] { + return set.New(TracerAccessList, TracerJSON, TracerStruct, TracerMarkdown) +} + // NewTracer creates a new Logger tracer to collect execution traces from an // EVM transaction. func NewTracer(tracer string, msg core.Message, cfg *params.ChainConfig, height int64) vm.EVMLogger { diff --git a/x/tokenfactory/keeper/msg_server.go b/x/tokenfactory/keeper/msg_server.go index 279d6c4ba..68c762381 100644 --- a/x/tokenfactory/keeper/msg_server.go +++ b/x/tokenfactory/keeper/msg_server.go @@ -13,7 +13,7 @@ import ( var _ types.MsgServer = (*Keeper)(nil) -var errNilMsg error = common.ErrNilGrpcMsg() +var errNilMsg error = common.ErrNilGrpcMsg func (k Keeper) CreateDenom( goCtx context.Context, txMsg *types.MsgCreateDenom,