diff --git a/CHANGELOG.md b/CHANGELOG.md index a00fe6f11dd6..d9b05b4b6591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* (cli) [#16887](https://github.com/cosmos/cosmos-sdk/pull/16887) Add two new CLI commands: `tx simulate` for simulating a transaction; `query block-results` for querying CometBFT RPC for block results. + ### Improvements * (types) [#16890](https://github.com/cosmos/cosmos-sdk/pull/16890) Remove `GetTxCmd() *cobra.Command` and `GetQueryCmd() *cobra.Command` from `module.AppModuleBasic` interface. diff --git a/client/cometbft.go b/client/cometbft.go index 595ea6492d24..fcf9ca64b08b 100644 --- a/client/cometbft.go +++ b/client/cometbft.go @@ -16,6 +16,7 @@ type CometRPC interface { Status(context.Context) (*coretypes.ResultStatus, error) Block(ctx context.Context, height *int64) (*coretypes.ResultBlock, error) BlockByHash(ctx context.Context, hash []byte) (*coretypes.ResultBlock, error) + BlockResults(ctx context.Context, height *int64) (*coretypes.ResultBlockResults, error) BlockchainInfo(ctx context.Context, minHeight, maxHeight int64) (*coretypes.ResultBlockchainInfo, error) Commit(ctx context.Context, height *int64) (*coretypes.ResultCommit, error) Tx(ctx context.Context, hash []byte, prove bool) (*coretypes.ResultTx, error) diff --git a/server/cmt_cmds.go b/server/cmt_cmds.go index 733067815eb7..47a4e64e18ff 100644 --- a/server/cmt_cmds.go +++ b/server/cmt_cmds.go @@ -1,6 +1,8 @@ package server import ( + "context" + "encoding/json" "fmt" "strconv" "strings" @@ -206,18 +208,13 @@ $ %s query block --%s=%s return fmt.Errorf("argument should be a block height") } - var height *int64 - // optional height + var height *int64 if len(args) > 0 { - h, err := strconv.Atoi(args[0]) + height, err = parseOptionalHeight(args[0]) if err != nil { return err } - if h > 0 { - tmp := int64(h) - height = &tmp - } } output, err := rpc.GetBlockByHeight(clientCtx, height) @@ -261,6 +258,70 @@ $ %s query block --%s=%s return cmd } +// QueryBlockResultCmd implements the default command for a BlockResults query. +func QueryBlockResultsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "block-results [height]", + Short: "Query for a committed block's results by height", + Long: "Query for a specific committed block's results using the CometBFT RPC `block_results` method", + Args: cobra.RangeArgs(0, 1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + node, err := clientCtx.GetNode() + if err != nil { + return err + } + + // optional height + var height *int64 + if len(args) > 0 { + height, err = parseOptionalHeight(args[0]) + if err != nil { + return err + } + } + + blockRes, err := node.BlockResults(context.Background(), height) + if err != nil { + return err + } + + // coretypes.ResultBlockResults doesn't implement proto.Message interface + // so we can't print it using clientCtx.PrintProto + // we choose to serialize it to json and print the json instead + blockResStr, err := json.Marshal(blockRes) + if err != nil { + return err + } + + return clientCtx.PrintString(string(blockResStr) + "\n") + }, + } + + flags.AddQueryFlagsToCmd(cmd) + + return cmd +} + +func parseOptionalHeight(heightStr string) (*int64, error) { + h, err := strconv.Atoi(heightStr) + if err != nil { + return nil, err + } + + if h == 0 { + return nil, nil + } + + tmp := int64(h) + + return &tmp, nil +} + func BootstrapStateCmd(appCreator types.AppCreator) *cobra.Command { cmd := &cobra.Command{ Use: "bootstrap-state", diff --git a/simapp/simd/cmd/root.go b/simapp/simd/cmd/root.go index 6d2cfc34e3b2..dba942219e5b 100644 --- a/simapp/simd/cmd/root.go +++ b/simapp/simd/cmd/root.go @@ -246,6 +246,7 @@ func queryCommand() *cobra.Command { authcmd.QueryTxsByEventsCmd(), server.QueryBlocksCmd(), authcmd.QueryTxCmd(), + server.QueryBlockResultsCmd(), ) return cmd @@ -270,6 +271,7 @@ func txCommand() *cobra.Command { authcmd.GetEncodeCommand(), authcmd.GetDecodeCommand(), authcmd.GetAuxToFeeCommand(), + authcmd.GetSimulateCmd(), ) return cmd diff --git a/simapp/simd/cmd/root_v2.go b/simapp/simd/cmd/root_v2.go index c7fad78a06de..0689cca50fbe 100644 --- a/simapp/simd/cmd/root_v2.go +++ b/simapp/simd/cmd/root_v2.go @@ -263,6 +263,7 @@ func queryCommand() *cobra.Command { authcmd.QueryTxsByEventsCmd(), server.QueryBlocksCmd(), authcmd.QueryTxCmd(), + server.QueryBlockResultsCmd(), ) return cmd @@ -287,6 +288,7 @@ func txCommand() *cobra.Command { authcmd.GetEncodeCommand(), authcmd.GetDecodeCommand(), authcmd.GetAuxToFeeCommand(), + authcmd.GetSimulateCmd(), ) return cmd diff --git a/x/auth/client/cli/tx_simulate.go b/x/auth/client/cli/tx_simulate.go new file mode 100644 index 000000000000..9cb49e4778d6 --- /dev/null +++ b/x/auth/client/cli/tx_simulate.go @@ -0,0 +1,101 @@ +package cli + +import ( + "strings" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client" +) + +// GetSimulateCmd returns a command that simulates whether a transaction will be +// successful. +func GetSimulateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "simulate /path/to/unsigned-tx.json --from keyname", + Short: "Simulate the gas usage of a transaction", + Long: strings.TrimSpace(`Simulate whether a transaction will be successful: + +- if successful, the simulation result is printed, which includes the gas + consumption, message response data, and events emitted; +- if unsuccessful, the error message is printed. + +The user must provide the path to a JSON-encoded unsigned transaction, typically +generated by any transaction command with the --generate-only flag. It should +look like below. Note that the "signer_infos" and "signatures" fields are left +empty; they will be auto-populated by dummy data for simulation purpose. + +{ + "body": { + "messages": [ + { + "@type": "/cosmos.bank.v1beta1.MsgSend", + "from_address": "cosmos1...", + "to_address": "cosmos1...", + "amount": [ + { + "denom": "utoken", + "amount": "12345" + } + ] + } + ], + "memo": "", + "timeout_height": "0", + "extension_options": [], + "non_critical_extension_options": [] + }, + "auth_info": { + "signer_infos": [], + "fee": { + "amount": [], + "gas_limit": "200000", + "payer": "", + "granter": "" + }, + "tip": null + }, + "signatures": [] +} + +The --from flag is mandatory, as the signer account's correct sequence number is +necessary for simulation. +`), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + txf, err := tx.NewFactoryCLI(clientCtx, cmd.Flags()) + if err != nil { + return err + } + + txf, err = txf.Prepare(clientCtx) + if err != nil { + return err + } + + stdTx, err := authclient.ReadTxFromFile(clientCtx, args[0]) + if err != nil { + return err + } + + simRes, _, err := tx.CalculateGas(clientCtx, txf, stdTx.GetMsgs()...) + if err != nil { + return err + } + + return clientCtx.PrintProto(simRes) + }, + } + + flags.AddTxFlagsToCmd(cmd) + + return cmd +}