diff --git a/app/evmante/evmante_sigverify.go b/app/evmante/evmante_sigverify.go index 2c41da448..2b8bb4ad6 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..fe92d577c --- /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/x/evm/cli" + "github.com/NibiruChain/nibiru/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..7ffef1941 --- /dev/null +++ b/x/evm/cli/cli_test.go @@ -0,0 +1,173 @@ +package cli_test + +import ( + "fmt" + "math/big" + + // "github.com/NibiruChain/nibiru/x/common/testutil" + "github.com/NibiruChain/nibiru/x/evm" + "github.com/NibiruChain/nibiru/x/evm/evmtest" + gethcommon "github.com/ethereum/go-ethereum/common" +) + +var ( + dummyAccs = evmtest.NewEthAccInfos(3) + dummyEthAddr = dummyAccs[1].EthAddr.Hex() + dummyFuntoken = evm.NewFunToken( + gethcommon.BigToAddress(big.NewInt(123)), + "ibc/testtoken", + false, + ) +) + +func (s *Suite) TestCmdSendFunTokenToEvm() { + testCases := []TestCase{ + { + name: "happy: send-funtoken-to-erc20", + args: []string{ + "send-funtoken-to-erc20", + dummyEthAddr, + fmt.Sprintf("%d%s", 123, dummyFuntoken.BankDenom), + }, + extraArgs: []string{fmt.Sprintf("--from=%s", s.testAcc.Address)}, + wantErr: "", + }, + { + name: "sad: coin format", + args: []string{ + "send-funtoken-to-erc20", + 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 24ea122a7..bca39b0e2 100644 --- a/x/evm/cli/query.go +++ b/x/evm/cli/query.go @@ -9,7 +9,10 @@ import ( "github.com/cosmos/cosmos-sdk/version" "github.com/spf13/cobra" + "github.com/NibiruChain/nibiru/eth" "github.com/NibiruChain/nibiru/x/evm" + sdk "github.com/cosmos/cosmos-sdk/types" + gethcommon "github.com/ethereum/go-ethereum/common" ) // GetQueryCmd returns a cli command for this module's queries @@ -84,15 +87,46 @@ func CmdQueryAccount() *cobra.Command { } queryClient := evm.NewQueryClient(clientCtx) - res, err := queryClient.EthAccount(cmd.Context(), &evm.QueryEthAccountRequest{ + 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 } - return clientCtx.PrintProto(res) + + 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 3c21bfe07..ddedeed83 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(), - SendFunTokenToEvm(), + CmdCreateFunToken(), + CmdSendFunTokenToEvm(), } for _, cmd := range cmds { txCmd.AddCommand(cmd) @@ -36,71 +36,63 @@ 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 from erc20 contract [erc20addr]"`, + 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.NewHexAddrFromStr(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.NewHexAddrFromStr(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 } -// SendFunTokenToEvm broadcast MsgSendFunTokenToEvm -func SendFunTokenToEvm() *cobra.Command { +// CmdSendFunTokenToEvm broadcast MsgSendFunTokenToEvm +func CmdSendFunTokenToEvm() *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]"`, @@ -110,13 +102,6 @@ func SendFunTokenToEvm() *cobra.Command { 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) coin, err := sdk.ParseCoinNormalized(args[1]) if err != nil { @@ -127,7 +112,7 @@ func SendFunTokenToEvm() *cobra.Command { BankCoin: coin, ToEthAddr: eth.MustNewHexAddrFromStr(args[0]), } - 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 a90fd408f..bd3a61e71 100644 --- a/x/evm/evmtest/eth.go +++ b/x/evm/evmtest/eth.go @@ -39,6 +39,15 @@ func NewEthAccInfo() EthPrivKeyAcc { } } +// NewEthAccInfos calls [NewEthAccInfo] n times. +func NewEthAccInfos(n int) []EthPrivKeyAcc { + infos := make([]EthPrivKeyAcc, n) + for idx := 0; idx < n; idx++ { + infos[idx] = NewEthAccInfo() + } + 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 63a121510..b1c734e44 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 cb697b45d..c3efb054a 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.NewEthAccInfo() 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.NewEthAccInfo() + 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 {