diff --git a/CHANGELOG.md b/CHANGELOG.md index 313f0d2df..d79e5fe69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,10 +105,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1997](https://github.com/NibiruChain/nibiru/pull/1997) - refactor(evm): Remove unnecessary params: "enable_call", "enable_create". - [#2000](https://github.com/NibiruChain/nibiru/pull/2000) - refactor(evm): simplify ERC-20 keeper methods - [#2001](https://github.com/NibiruChain/nibiru/pull/2001) - refactor(evm): simplify FunToken methods and tests +- [#2002](https://github.com/NibiruChain/nibiru/pull/2002) - feat(evm): Add the account query to the EVM command. Cover the CLI with tests. - [#2003](https://github.com/NibiruChain/nibiru/pull/2003) - fix(evm): fix FunToken conversions between Cosmos and EVM - [#2004](https://github.com/NibiruChain/nibiru/pull/2004) - refactor(evm)!: replace `HexAddr` with `EIP55Addr` - [#2006](https://github.com/NibiruChain/nibiru/pull/2006) - test(evm): e2e tests for eth_* endpoints -- [#2008](https://github.com/NibiruChain/nibiru/pull/2008) - refactor(evm): clean up precompile setups +- [#2008](https://github.com/NibiruChain/nibiru/pull/2008) - refactor(evm): clean up precompile setups - [#2013](https://github.com/NibiruChain/nibiru/pull/2013) - chore(evm): Set appropriate gas value for the required gas of the "IFunToken.sol" precompile. - [#2014](https://github.com/NibiruChain/nibiru/pull/2014) - feat(evm): Emit block bloom event in EndBlock hook. diff --git a/app/evmante/evmante_sigverify.go b/app/evmante/evmante_sigverify.go index 6b858066e..a19c664c8 100644 --- a/app/evmante/evmante_sigverify.go +++ b/app/evmante/evmante_sigverify.go @@ -24,11 +24,12 @@ func NewEthSigVerificationDecorator(k EVMKeeper) EthSigVerificationDecorator { } } -// 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. +// 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) { diff --git a/x/evm/cli/cli_setup_test.go b/x/evm/cli/cli_setup_test.go new file mode 100644 index 000000000..21b11786e --- /dev/null +++ b/x/evm/cli/cli_setup_test.go @@ -0,0 +1,115 @@ +package cli_test + +import ( + "context" + "io" + "testing" + + "github.com/stretchr/testify/suite" + + rpcclientmock "github.com/cometbft/cometbft/rpc/client/mock" + sdkclient "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdktestutil "github.com/cosmos/cosmos-sdk/testutil" + sdktestutilcli "github.com/cosmos/cosmos-sdk/testutil/cli" + testutilmod "github.com/cosmos/cosmos-sdk/types/module/testutil" + + svrcmd "github.com/cosmos/cosmos-sdk/server/cmd" + + "github.com/NibiruChain/nibiru/v2/x/evm/cli" + "github.com/NibiruChain/nibiru/v2/x/evm/evmmodule" +) + +type Suite struct { + suite.Suite + + keyring keyring.Keyring + encCfg testutilmod.TestEncodingConfig + baseCtx sdkclient.Context + clientCtx sdkclient.Context + + testAcc sdktestutil.TestAccount +} + +func (s *Suite) SetupSuite() { + s.encCfg = testutilmod.MakeTestEncodingConfig(evmmodule.AppModuleBasic{}) + s.keyring = keyring.NewInMemory(s.encCfg.Codec) + s.baseCtx = sdkclient.Context{}. + WithKeyring(s.keyring). + WithTxConfig(s.encCfg.TxConfig). + WithCodec(s.encCfg.Codec). + WithClient(sdktestutilcli.MockTendermintRPC{Client: rpcclientmock.Client{}}). + WithAccountRetriever(sdkclient.MockAccountRetriever{}). + WithOutput(io.Discard). + WithChainID("test-chain") + + s.clientCtx = s.baseCtx + + testAccs := sdktestutil.CreateKeyringAccounts(s.T(), s.keyring, 1) + s.testAcc = testAccs[0] +} + +func TestSuite(t *testing.T) { + suite.Run(t, new(Suite)) +} + +// Flags for broadcasting transactions +func commonTxArgs() []string { + return []string{ + "--yes=true", // skip confirmation + "--broadcast-mode=sync", + "--fees=1unibi", + "--chain-id=test-chain", + } +} + +type TestCase struct { + name string + args []string + extraArgs []string + wantErr string +} + +func (tc TestCase) NewCtx(s *Suite) sdkclient.Context { + return s.baseCtx +} + +func (tc TestCase) RunTxCmd(s *Suite) { + s.Run(tc.name, func() { + ctx := svrcmd.CreateExecuteContext(context.Background()) + + cmd := cli.GetTxCmd() + cmd.SetContext(ctx) + args := append(tc.args, commonTxArgs()...) + cmd.SetArgs(append(args, tc.extraArgs...)) + + s.Require().NoError(sdkclient.SetCmdClientContextHandler(tc.NewCtx(s), cmd)) + + err := cmd.Execute() + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) +} + +func (tc TestCase) RunQueryCmd(s *Suite) { + s.Run(tc.name, func() { + ctx := svrcmd.CreateExecuteContext(context.Background()) + + cmd := cli.GetQueryCmd() + cmd.SetContext(ctx) + args := tc.args // don't append common tx args + cmd.SetArgs(append(args, tc.extraArgs...)) + + s.Require().NoError(sdkclient.SetCmdClientContextHandler(tc.NewCtx(s), cmd)) + + err := cmd.Execute() + if tc.wantErr != "" { + s.Require().ErrorContains(err, tc.wantErr) + return + } + s.Require().NoError(err) + }) +} diff --git a/x/evm/cli/cli_test.go b/x/evm/cli/cli_test.go new file mode 100644 index 000000000..99b8b684e --- /dev/null +++ b/x/evm/cli/cli_test.go @@ -0,0 +1,173 @@ +package cli_test + +import ( + "fmt" + "math/big" + + gethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/NibiruChain/nibiru/v2/x/evm" + "github.com/NibiruChain/nibiru/v2/x/evm/evmtest" +) + +var ( + dummyAccs = evmtest.NewEthPrivAccs(3) + dummyEthAddr = dummyAccs[1].EthAddr.Hex() + dummyFuntoken = evm.NewFunToken( + gethcommon.BigToAddress(big.NewInt(123)), + "ibc/testtoken", + false, + ) +) + +func (s *Suite) TestCmdConvertCoinToEvm() { + testCases := []TestCase{ + { + name: "happy: convert-coin-to-evm", + args: []string{ + "convert-coin-to-evm", + dummyEthAddr, + fmt.Sprintf("%d%s", 123, dummyFuntoken.BankDenom), + }, + extraArgs: []string{fmt.Sprintf("--from=%s", s.testAcc.Address)}, + wantErr: "", + }, + { + name: "sad: coin format", + args: []string{ + "convert-coin-to-evm", + dummyAccs[1].EthAddr.Hex(), + fmt.Sprintf("%s %d", dummyFuntoken.BankDenom, 123), + }, + extraArgs: []string{fmt.Sprintf("--from=%s", s.testAcc.Address)}, + wantErr: "invalid decimal coin expression", + }, + } + + for _, tc := range testCases { + tc.RunTxCmd(s) + } +} + +func (s *Suite) TestCmdCreateFunToken() { + testCases := []TestCase{ + { + name: "happy: create-funtoken (erc20)", + args: []string{ + "create-funtoken", + fmt.Sprintf("--erc20=%s", dummyEthAddr), + }, + extraArgs: []string{fmt.Sprintf("--from=%s", s.testAcc.Address)}, + wantErr: "", + }, + { + name: "happy: create-funtoken (bank coin)", + args: []string{ + "create-funtoken", + fmt.Sprintf("--bank-denom=%s", dummyFuntoken.BankDenom), + }, + extraArgs: []string{fmt.Sprintf("--from=%s", s.testAcc.Address)}, + wantErr: "", + }, + { + name: "sad: too many args", + args: []string{ + "create-funtoken", + fmt.Sprintf("--erc20=%s", dummyEthAddr), + fmt.Sprintf("--bank-denom=%s", dummyFuntoken.BankDenom), + }, + extraArgs: []string{fmt.Sprintf("--from=%s", s.testAcc.Address)}, + wantErr: "exactly one of the flags --bank-denom or --erc20 must be specified", + }, + } + + for _, tc := range testCases { + tc.RunTxCmd(s) + } +} + +func (s *Suite) TestCmdQueryAccount() { + testCases := []TestCase{ + { + name: "happy: query account (bech32)", + args: []string{ + "account", + dummyAccs[0].NibiruAddr.String(), + }, + wantErr: "", + }, + { + name: "happy: query account (eth hex)", + args: []string{ + "account", + dummyAccs[0].EthAddr.Hex(), + }, + wantErr: "", + }, + { + name: "happy: query account (eth hex) --offline", + args: []string{ + "account", + dummyAccs[0].EthAddr.Hex(), + "--offline", + }, + wantErr: "", + }, + { + name: "happy: query account (bech32) --offline", + args: []string{ + "account", + dummyAccs[0].NibiruAddr.String(), + "--offline", + }, + wantErr: "", + }, + { + name: "sad: too many args", + args: []string{ + "funtoken", + "arg1", + "arg2", + }, + wantErr: "accepts 1 arg", + }, + } + + for _, tc := range testCases { + tc.RunQueryCmd(s) + } +} + +func (s *Suite) TestCmdQueryFunToken() { + testCases := []TestCase{ + { + name: "happy: query funtoken (bank coin denom)", + args: []string{ + "funtoken", + dummyFuntoken.BankDenom, + }, + wantErr: "", + }, + { + name: "happy: query funtoken (erc20 addr)", + args: []string{ + "funtoken", + dummyFuntoken.Erc20Addr.String(), + }, + wantErr: "", + }, + { + name: "sad: too many args", + args: []string{ + "funtoken", + "arg1", + "arg2", + }, + wantErr: "accepts 1 arg", + }, + } + + for _, tc := range testCases { + tc.RunQueryCmd(s) + } +} diff --git a/x/evm/cli/query.go b/x/evm/cli/query.go index 1c580e49e..349ee7bb2 100644 --- a/x/evm/cli/query.go +++ b/x/evm/cli/query.go @@ -9,6 +9,10 @@ import ( "github.com/cosmos/cosmos-sdk/version" "github.com/spf13/cobra" + sdk "github.com/cosmos/cosmos-sdk/types" + gethcommon "github.com/ethereum/go-ethereum/common" + + "github.com/NibiruChain/nibiru/v2/eth" "github.com/NibiruChain/nibiru/v2/x/evm" ) @@ -25,7 +29,8 @@ func GetQueryCmd() *cobra.Command { // Add subcommands cmds := []*cobra.Command{ - GetCmdFunToken(), + CmdQueryFunToken(), + CmdQueryAccount(), } for _, cmd := range cmds { moduleQueryCmd.AddCommand(cmd) @@ -33,8 +38,8 @@ func GetQueryCmd() *cobra.Command { return moduleQueryCmd } -// GetCmdFunToken returns fungible token mapping for either bank coin or erc20 addr -func GetCmdFunToken() *cobra.Command { +// CmdQueryFunToken returns fungible token mapping for either bank coin or erc20 addr +func CmdQueryFunToken() *cobra.Command { cmd := &cobra.Command{ Use: "funtoken [coin-or-erc20addr]", Short: "Query evm fungible token mapping", @@ -69,3 +74,60 @@ $ %s query %s get-fun-token 0x7D4B7B8CA7E1a24928Bb96D59249c7a5bd1DfBe6 flags.AddQueryFlagsToCmd(cmd) return cmd } + +func CmdQueryAccount() *cobra.Command { + cmd := &cobra.Command{ + Use: "account [address]", + Short: "Query account by its hex address or bech32", + Long: strings.TrimSpace(""), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + queryClient := evm.NewQueryClient(clientCtx) + + req := &evm.QueryEthAccountRequest{ + Address: args[0], + } + + isBech32, err := req.Validate() + fmt.Printf("TODO: UD-DEBUG: req.String(): %v\n", req.String()) + fmt.Printf("TODO: UD-DEBUG: err: %v\n", err) + if err != nil { + return err + } + + offline, _ := cmd.Flags().GetBool("offline") + + if offline { + var addrEth gethcommon.Address + var addrBech32 sdk.AccAddress + + if isBech32 { + addrBech32 = sdk.MustAccAddressFromBech32(req.Address) + addrEth = eth.NibiruAddrToEthAddr(addrBech32) + } else { + addrEth = gethcommon.HexToAddress(req.Address) + addrBech32 = eth.EthAddrToNibiruAddr(addrEth) + } + + resp := new(evm.QueryEthAccountResponse) + resp.EthAddress = addrEth.Hex() + resp.Bech32Address = addrBech32.String() + return clientCtx.PrintProto(resp) + } + + resp, err := queryClient.EthAccount(cmd.Context(), req) + if err != nil { + return fmt.Errorf("consider using the \"--offline\" flag: %w", err) + } + + return clientCtx.PrintProto(resp) + }, + } + cmd.Flags().Bool("offline", false, "Skip the query and only return addresses.") + flags.AddQueryFlagsToCmd(cmd) + return cmd +} diff --git a/x/evm/cli/tx.go b/x/evm/cli/tx.go index c8a625a21..dbda5f7fd 100644 --- a/x/evm/cli/tx.go +++ b/x/evm/cli/tx.go @@ -3,6 +3,7 @@ package cli import ( "fmt" + "github.com/MakeNowJust/heredoc/v2" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" @@ -25,9 +26,8 @@ func GetTxCmd() *cobra.Command { } cmds := []*cobra.Command{ - CmdCreateFunTokenFromBankCoin(), - CmdCreateFunTokenFromERC20(), - ConvertCoinToEvm(), + CmdCreateFunToken(), + CmdConvertCoinToEvm(), } for _, cmd := range cmds { txCmd.AddCommand(cmd) @@ -36,87 +36,72 @@ func GetTxCmd() *cobra.Command { return txCmd } -// CmdCreateFunTokenFromBankCoin broadcast MsgCreateFunToken -func CmdCreateFunTokenFromBankCoin() *cobra.Command { +// CmdCreateFunToken broadcast MsgCreateFunToken +func CmdCreateFunToken() *cobra.Command { cmd := &cobra.Command{ - Use: "create-funtoken-from-bank-coin [denom] [flags]", - Short: `Create an erc20 fungible token from bank coin [denom]"`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - clientCtx, err := client.GetClientTxContext(cmd) - if err != nil { - return err - } - txFactory, err := tx.NewFactoryCLI(clientCtx, cmd.Flags()) - if err != nil { - return err - } - txFactory = txFactory. - WithTxConfig(clientCtx.TxConfig). - WithAccountRetriever(clientCtx.AccountRetriever) + Use: "create-funtoken [flags]", + Short: `Create a fungible token mapping between a bank coin and erc20 contract"`, + Long: heredoc.Doc(` + Example: Creating a fungible token mapping from bank coin. - msg := &evm.MsgCreateFunToken{ - Sender: clientCtx.GetFromAddress().String(), - FromBankDenom: args[0], - } - return tx.GenerateOrBroadcastTxWithFactory(clientCtx, txFactory, msg) - }, - } - flags.AddTxFlagsToCmd(cmd) - return cmd -} + create-funtoken --bank-denom="ibc/..." -// CmdCreateFunTokenFromERC20 broadcast MsgCreateFunToken -func CmdCreateFunTokenFromERC20() *cobra.Command { - cmd := &cobra.Command{ - Use: "create-funtoken-from-erc20 [erc20addr] [flags]", - Short: `Create a fungible token from erc20 contract [erc20addr]"`, - Args: cobra.ExactArgs(1), + Example: Creating a fungible token mapping from an ERC20. + + create-funtoken --erc20=[erc20-address] + `), + Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) error { clientCtx, err := client.GetClientTxContext(cmd) if err != nil { return err } - txFactory, err := tx.NewFactoryCLI(clientCtx, cmd.Flags()) - if err != nil { - return err - } - txFactory = txFactory. - WithTxConfig(clientCtx.TxConfig). - WithAccountRetriever(clientCtx.AccountRetriever) - erc20Addr, err := eth.NewEIP55AddrFromStr(args[0]) - if err != nil { - return err + + bankDenom, _ := cmd.Flags().GetString("bank-denom") + erc20AddrStr, _ := cmd.Flags().GetString("erc20") + + if (bankDenom == "" && erc20AddrStr == "") || + (bankDenom != "" && erc20AddrStr != "") { + return fmt.Errorf("exactly one of the flags --bank-denom or --erc20 must be specified") } + msg := &evm.MsgCreateFunToken{ - Sender: clientCtx.GetFromAddress().String(), - FromErc20: &erc20Addr, + Sender: clientCtx.GetFromAddress().String(), } - return tx.GenerateOrBroadcastTxWithFactory(clientCtx, txFactory, msg) + if bankDenom != "" { + if err := sdk.ValidateDenom(bankDenom); err != nil { + return err + } + msg.FromBankDenom = bankDenom + } else { + erc20Addr, err := eth.NewEIP55AddrFromStr(erc20AddrStr) + if err != nil { + return err + } + msg.FromErc20 = &erc20Addr + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } flags.AddTxFlagsToCmd(cmd) + cmd.Flags().String("bank-denom", "", "The bank denom to create a fungible token from") + cmd.Flags().String("erc20", "", "The ERC20 address to create a fungible token from") + return cmd } -// ConvertCoinToEvm broadcast MsgConvertCoinToEvm -func ConvertCoinToEvm() *cobra.Command { +// CmdConvertCoinToEvm broadcast MsgConvertCoinToEvm +func CmdConvertCoinToEvm() *cobra.Command { cmd := &cobra.Command{ - Use: "send-funtoken-to-erc20 [to_eth_addr] [coin] [flags]", - Short: `Send bank [coin] to its erc20 representation for the user [to_eth_addr]"`, + Use: "convert-coin-to-evm [to_eth_addr] [coin] [flags]", + Short: `Convert bank [coin] to its erc20 representation and send to the [to_eth_addr] account"`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { clientCtx, err := client.GetClientTxContext(cmd) if err != nil { return err } - txFactory, err := tx.NewFactoryCLI(clientCtx, cmd.Flags()) - if err != nil { - return err - } - txFactory = txFactory. - WithTxConfig(clientCtx.TxConfig). - WithAccountRetriever(clientCtx.AccountRetriever) eip55Addr, err := eth.NewEIP55AddrFromStr(args[0]) if err != nil { @@ -132,7 +117,7 @@ func ConvertCoinToEvm() *cobra.Command { BankCoin: coin, ToEthAddr: eip55Addr, } - return tx.GenerateOrBroadcastTxWithFactory(clientCtx, txFactory, msg) + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } flags.AddTxFlagsToCmd(cmd) diff --git a/x/evm/evmtest/eth.go b/x/evm/evmtest/eth.go index 05792bb7e..f6037cfc5 100644 --- a/x/evm/evmtest/eth.go +++ b/x/evm/evmtest/eth.go @@ -36,6 +36,15 @@ func NewEthPrivAcc() EthPrivKeyAcc { } } +// NewEthPrivAccs calls [NewEthAccInfo] n times. +func NewEthPrivAccs(n int) []EthPrivKeyAcc { + infos := make([]EthPrivKeyAcc, n) + for idx := 0; idx < n; idx++ { + infos[idx] = NewEthPrivAcc() + } + return infos +} + type EthPrivKeyAcc struct { EthAddr gethcommon.Address NibiruAddr sdk.AccAddress diff --git a/x/evm/keeper/grpc_query.go b/x/evm/keeper/grpc_query.go index b0f703ae5..fae288468 100644 --- a/x/evm/keeper/grpc_query.go +++ b/x/evm/keeper/grpc_query.go @@ -67,12 +67,12 @@ func (k Keeper) EthAccount( acct := k.GetAccountOrEmpty(ctx, addrEth) return &evm.QueryEthAccountResponse{ + EthAddress: addrEth.Hex(), + Bech32Address: addrBech32.String(), Balance: acct.BalanceNative.String(), BalanceWei: evm.NativeToWei(acct.BalanceNative).String(), CodeHash: gethcommon.BytesToHash(acct.CodeHash).Hex(), Nonce: acct.Nonce, - EthAddress: addrEth.Hex(), - Bech32Address: addrBech32.String(), }, nil } diff --git a/x/evm/keeper/grpc_query_test.go b/x/evm/keeper/grpc_query_test.go index 2a1e2b86b..9234bbe6b 100644 --- a/x/evm/keeper/grpc_query_test.go +++ b/x/evm/keeper/grpc_query_test.go @@ -138,7 +138,7 @@ func (s *Suite) TestQueryEvmAccount() { wantErr: "not a valid ethereum hex addr", }, { - name: "happy: not existing account", + name: "happy: nonexistent account (hex addr input)", scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { ethAcc := evmtest.NewEthPrivAcc() req = &evm.QueryEthAccountRequest{ @@ -156,6 +156,25 @@ func (s *Suite) TestQueryEvmAccount() { }, wantErr: "", }, + { + name: "happy: nonexistent account (bech32 input)", + scenario: func(deps *evmtest.TestDeps) (req In, wantResp Out) { + ethAcc := evmtest.NewEthPrivAcc() + req = &evm.QueryEthAccountRequest{ + Address: ethAcc.NibiruAddr.String(), + } + wantResp = &evm.QueryEthAccountResponse{ + Balance: "0", + BalanceWei: "0", + CodeHash: gethcommon.BytesToHash(evm.EmptyCodeHash).Hex(), + Nonce: 0, + EthAddress: ethAcc.EthAddr.String(), + Bech32Address: ethAcc.NibiruAddr.String(), + } + return req, wantResp + }, + wantErr: "", + }, } for _, tc := range testCases {