diff --git a/PENDING.md b/PENDING.md index 63731e554910..498383be513d 100644 --- a/PENDING.md +++ b/PENDING.md @@ -47,6 +47,7 @@ decoded automatically. ### Gaia CLI +* [\#3653] Prompt user confirmation prior to signing and broadcasting a transaction. * [\#3670] CLI support for showing bech32 addresses in Ledger devices * [\#3711] Update `tx sign` to use `--from` instead of the deprecated `--name` CLI flag. diff --git a/client/context/context.go b/client/context/context.go index 05ae3455d5e6..beffad0c41a8 100644 --- a/client/context/context.go +++ b/client/context/context.go @@ -53,6 +53,7 @@ type CLIContext struct { FromAddress sdk.AccAddress FromName string Indent bool + SkipConfirm bool } // NewCLIContext returns a new initialized CLIContext with parameters from the @@ -96,6 +97,7 @@ func NewCLIContext() CLIContext { FromAddress: fromAddress, FromName: fromName, Indent: viper.GetBool(client.FlagIndentResponse), + SkipConfirm: viper.GetBool(client.FlagSkipConfirmation), } } diff --git a/client/flags.go b/client/flags.go index c87a8933c59b..62d912908ea1 100644 --- a/client/flags.go +++ b/client/flags.go @@ -45,6 +45,7 @@ const ( FlagSSLCertFile = "ssl-certfile" FlagSSLKeyFile = "ssl-keyfile" FlagOutputDocument = "output-document" // inspired by wget -O + FlagSkipConfirmation = "yes" ) // LineBreak can be included in a command list to provide a blank line @@ -89,9 +90,14 @@ func PostCommands(cmds ...*cobra.Command) []*cobra.Command { c.Flags().Bool(FlagTrustNode, true, "Trust connected full node (don't verify proofs for responses)") c.Flags().Bool(FlagDryRun, false, "ignore the --gas flag and perform a simulation of a transaction, but don't broadcast it") c.Flags().Bool(FlagGenerateOnly, false, "build an unsigned transaction and write it to STDOUT") + c.Flags().BoolP(FlagSkipConfirmation, "y", false, "Skip tx broadcasting prompt confirmation") + // --gas can accept integers and "simulate" c.Flags().Var(&GasFlagVar, "gas", fmt.Sprintf( - "gas limit to set per-transaction; set to %q to calculate required gas automatically (default %d)", GasFlagAuto, DefaultGasLimit)) + "gas limit to set per-transaction; set to %q to calculate required gas automatically (default %d)", + GasFlagAuto, DefaultGasLimit, + )) + viper.BindPFlag(FlagTrustNode, c.Flags().Lookup(FlagTrustNode)) viper.BindPFlag(FlagUseLedger, c.Flags().Lookup(FlagUseLedger)) viper.BindPFlag(FlagNode, c.Flags().Lookup(FlagNode)) diff --git a/client/input.go b/client/input.go index f229d7d3b0a1..5e041ff5912f 100644 --- a/client/input.go +++ b/client/input.go @@ -9,7 +9,7 @@ import ( "errors" "github.com/bgentry/speakeasy" - "github.com/mattn/go-isatty" + isatty "github.com/mattn/go-isatty" ) // MinPassLength is the minimum acceptable password length @@ -90,8 +90,9 @@ func GetCheckPassword(prompt, prompt2 string, buf *bufio.Reader) (string, error) func GetConfirmation(prompt string, buf *bufio.Reader) (bool, error) { for { if inputIsTty() { - fmt.Print(fmt.Sprintf("%s [y/n]:", prompt)) + fmt.Print(fmt.Sprintf("%s [Y/n]: ", prompt)) } + response, err := readLineFromBuf(buf) if err != nil { return false, err diff --git a/client/utils/utils.go b/client/utils/utils.go index 55ce058ff868..633a7e28a3ac 100644 --- a/client/utils/utils.go +++ b/client/utils/utils.go @@ -62,6 +62,22 @@ func CompleteAndBroadcastTxCLI(txBldr authtxb.TxBuilder, cliCtx context.CLIConte return nil } + if !cliCtx.SkipConfirm { + stdSignMsg, err := txBldr.BuildSignMsg(msgs) + if err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "%s\n\n", cliCtx.Codec.MustMarshalJSON(stdSignMsg)) + + buf := client.BufferStdin() + ok, err := client.GetConfirmation("confirm transaction before signing and broadcasting", buf) + if err != nil || !ok { + fmt.Fprintf(os.Stderr, "%s\n", "cancelled transaction") + return err + } + } + passphrase, err := keys.GetPassphrase(fromName) if err != nil { return err diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index 52b68a6ac76d..0c270e58723c 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -86,19 +86,19 @@ func TestGaiaCLIMinimumFees(t *testing.T) { barAddr := f.KeyAddress(keyBar) // Send a transaction that will get rejected - success, _, _ := f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fee2Denom, 10)) + success, _, _ := f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fee2Denom, 10), "-y") require.False(f.T, success) tests.WaitForNextNBlocksTM(1, f.Port) // Ensure tx w/ correct fees pass txFees := fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 2)) - success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fee2Denom, 10), txFees) + success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fee2Denom, 10), txFees, "-y") require.True(f.T, success) tests.WaitForNextNBlocksTM(1, f.Port) // Ensure tx w/ improper fees fails txFees = fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 1)) - success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 10), txFees) + success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 10), txFees, "-y") require.False(f.T, success) // Cleanup testing directories @@ -120,7 +120,7 @@ func TestGaiaCLIGasPrices(t *testing.T) { badGasPrice, _ := sdk.NewDecFromStr("0.000003") success, _, _ := f.TxSend( keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 50), - fmt.Sprintf("--gas-prices=%s", sdk.NewDecCoinFromDec(feeDenom, badGasPrice))) + fmt.Sprintf("--gas-prices=%s", sdk.NewDecCoinFromDec(feeDenom, badGasPrice)), "-y") require.False(t, success) // wait for a block confirmation @@ -129,7 +129,7 @@ func TestGaiaCLIGasPrices(t *testing.T) { // sufficient gas prices (tx passes) success, _, _ = f.TxSend( keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 50), - fmt.Sprintf("--gas-prices=%s", sdk.NewDecCoinFromDec(feeDenom, minGasPrice))) + fmt.Sprintf("--gas-prices=%s", sdk.NewDecCoinFromDec(feeDenom, minGasPrice)), "-y") require.True(t, success) // wait for a block confirmation @@ -171,7 +171,7 @@ func TestGaiaCLIFeesDeduction(t *testing.T) { largeCoins := sdk.TokensFromTendermintPower(10000000) success, _, _ = f.TxSend( keyFoo, barAddr, sdk.NewCoin(fooDenom, largeCoins), - fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 2))) + fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 2)), "-y") require.False(t, success) // Wait for a block @@ -184,7 +184,7 @@ func TestGaiaCLIFeesDeduction(t *testing.T) { // test success (transfer = coins + fees) success, _, _ = f.TxSend( keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, 500), - fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 2))) + fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 2)), "-y") require.True(t, success) f.Cleanup() @@ -208,7 +208,7 @@ func TestGaiaCLISend(t *testing.T) { // Send some tokens from one account to the other sendTokens := sdk.TokensFromTendermintPower(10) - f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens)) + f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "-y") tests.WaitForNextNBlocksTM(1, f.Port) // Ensure account balances match expected @@ -226,7 +226,7 @@ func TestGaiaCLISend(t *testing.T) { require.Equal(t, startTokens.Sub(sendTokens), fooAcc.GetCoins().AmountOf(denom)) // test autosequencing - f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens)) + f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "-y") tests.WaitForNextNBlocksTM(1, f.Port) // Ensure account balances match expected @@ -236,7 +236,7 @@ func TestGaiaCLISend(t *testing.T) { require.Equal(t, startTokens.Sub(sendTokens.MulRaw(2)), fooAcc.GetCoins().AmountOf(denom)) // test memo - f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--memo='testmemo'") + f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--memo='testmemo'", "-y") tests.WaitForNextNBlocksTM(1, f.Port) // Ensure account balances match expected @@ -248,6 +248,40 @@ func TestGaiaCLISend(t *testing.T) { f.Cleanup() } +func TestGaiaCLIConfirmTx(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + + // start gaiad server + proc := f.GDStart() + defer proc.Stop(false) + + // Save key addresses for later use + fooAddr := f.KeyAddress(keyFoo) + barAddr := f.KeyAddress(keyBar) + + fooAcc := f.QueryAccount(fooAddr) + startTokens := sdk.TokensFromTendermintPower(50) + require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(denom)) + + // send some tokens from one account to the other + sendTokens := sdk.TokensFromTendermintPower(10) + f.txSendWithConfirm(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "Y") + tests.WaitForNextNBlocksTM(1, f.Port) + + // ensure account balances match expected + barAcc := f.QueryAccount(barAddr) + require.Equal(t, sendTokens, barAcc.GetCoins().AmountOf(denom)) + + // send some tokens from one account to the other (cancelling confirmation) + f.txSendWithConfirm(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "n") + tests.WaitForNextNBlocksTM(1, f.Port) + + // ensure account balances match expected + barAcc = f.QueryAccount(barAddr) + require.Equal(t, sendTokens, barAcc.GetCoins().AmountOf(denom)) +} + func TestGaiaCLIGasAuto(t *testing.T) { t.Parallel() f := InitFixtures(t) @@ -265,7 +299,7 @@ func TestGaiaCLIGasAuto(t *testing.T) { // Test failure with auto gas disabled and very little gas set by hand sendTokens := sdk.TokensFromTendermintPower(10) - success, _, _ := f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=10") + success, _, _ := f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=10", "-y") require.False(t, success) // Check state didn't change @@ -273,7 +307,7 @@ func TestGaiaCLIGasAuto(t *testing.T) { require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(denom)) // Test failure with negative gas - success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=-100") + success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=-100", "-y") require.False(t, success) // Check state didn't change @@ -281,7 +315,7 @@ func TestGaiaCLIGasAuto(t *testing.T) { require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(denom)) // Test failure with 0 gas - success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=0") + success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=0", "-y") require.False(t, success) // Check state didn't change @@ -289,7 +323,7 @@ func TestGaiaCLIGasAuto(t *testing.T) { require.Equal(t, startTokens, fooAcc.GetCoins().AmountOf(denom)) // Enable auto gas - success, stdout, stderr := f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=auto") + success, stdout, stderr := f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "--gas=auto", "-y") require.NotEmpty(t, stderr) require.True(t, success) cdc := app.MakeCodec() @@ -320,7 +354,7 @@ func TestGaiaCLICreateValidator(t *testing.T) { consPubKey := sdk.MustBech32ifyConsPub(ed25519.GenPrivKey().PubKey()) sendTokens := sdk.TokensFromTendermintPower(10) - f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens)) + f.TxSend(keyFoo, barAddr, sdk.NewCoin(denom, sendTokens), "-y") tests.WaitForNextNBlocksTM(1, f.Port) barAcc := f.QueryAccount(barAddr) @@ -342,7 +376,7 @@ func TestGaiaCLICreateValidator(t *testing.T) { require.True(t, success) // Create the validator - f.TxStakingCreateValidator(keyBar, consPubKey, sdk.NewCoin(denom, newValTokens)) + f.TxStakingCreateValidator(keyBar, consPubKey, sdk.NewCoin(denom, newValTokens), "-y") tests.WaitForNextNBlocksTM(1, f.Port) // Ensure funds were deducted properly @@ -361,7 +395,7 @@ func TestGaiaCLICreateValidator(t *testing.T) { // unbond a single share unbondTokens := sdk.TokensFromTendermintPower(1) - success = f.TxStakingUnbond(keyBar, unbondTokens.String(), barVal) + success = f.TxStakingUnbond(keyBar, unbondTokens.String(), barVal, "-y") require.True(t, success) tests.WaitForNextNBlocksTM(1, f.Port) @@ -403,7 +437,7 @@ func TestGaiaCLISubmitProposal(t *testing.T) { // Test submit generate only for submit proposal proposalTokens := sdk.TokensFromTendermintPower(5) success, stdout, stderr := f.TxGovSubmitProposal( - keyFoo, "Text", "Test", "test", sdk.NewCoin(denom, proposalTokens), "--generate-only") + keyFoo, "Text", "Test", "test", sdk.NewCoin(denom, proposalTokens), "--generate-only", "-y") require.True(t, success) require.Empty(t, stderr) msg := unmarshalStdTx(t, stdout) @@ -416,7 +450,7 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.True(t, success) // Create the proposal - f.TxGovSubmitProposal(keyFoo, "Text", "Test", "test", sdk.NewCoin(denom, proposalTokens)) + f.TxGovSubmitProposal(keyFoo, "Text", "Test", "test", sdk.NewCoin(denom, proposalTokens), "-y") tests.WaitForNextNBlocksTM(1, f.Port) // Ensure transaction tags can be queried @@ -451,7 +485,7 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.Equal(t, 0, len(msg.GetSignatures())) // Run the deposit transaction - f.TxGovDeposit(1, keyFoo, sdk.NewCoin(denom, depositTokens)) + f.TxGovDeposit(1, keyFoo, sdk.NewCoin(denom, depositTokens), "-y") tests.WaitForNextNBlocksTM(1, f.Port) // test query deposit @@ -486,7 +520,7 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.Equal(t, 0, len(msg.GetSignatures())) // Vote on the proposal - f.TxGovVote(1, gov.OptionYes, keyFoo) + f.TxGovVote(1, gov.OptionYes, keyFoo, "-y") tests.WaitForNextNBlocksTM(1, f.Port) // Query the vote @@ -513,7 +547,7 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.Equal(t, uint64(1), proposalsQuery[0].GetProposalID()) // submit a second test proposal - f.TxGovSubmitProposal(keyFoo, "Text", "Apples", "test", sdk.NewCoin(denom, proposalTokens)) + f.TxGovSubmitProposal(keyFoo, "Text", "Apples", "test", sdk.NewCoin(denom, proposalTokens), "-y") tests.WaitForNextNBlocksTM(1, f.Port) // Test limit on proposals query @@ -538,7 +572,7 @@ func TestGaiaCLIQueryTxPagination(t *testing.T) { seq := accFoo.GetSequence() for i := 1; i <= 30; i++ { - success, _, _ := f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, int64(i)), fmt.Sprintf("--sequence=%d", seq)) + success, _, _ := f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(fooDenom, int64(i)), fmt.Sprintf("--sequence=%d", seq), "-y") require.True(t, success) seq++ } @@ -725,7 +759,7 @@ func TestGaiaCLIMultisignInsufficientCosigners(t *testing.T) { barAddr := f.KeyAddress(keyBar) // Send some tokens from one account to the other - success, _, _ := f.TxSend(keyFoo, fooBarBazAddr, sdk.NewInt64Coin(denom, 10)) + success, _, _ := f.TxSend(keyFoo, fooBarBazAddr, sdk.NewInt64Coin(denom, 10), "-y") require.True(t, success) tests.WaitForNextNBlocksTM(1, f.Port) @@ -738,7 +772,7 @@ func TestGaiaCLIMultisignInsufficientCosigners(t *testing.T) { defer os.Remove(unsignedTxFile.Name()) // Sign with foo's key - success, stdout, _ = f.TxSign(keyFoo, unsignedTxFile.Name(), "--multisig", fooBarBazAddr.String()) + success, stdout, _ = f.TxSign(keyFoo, unsignedTxFile.Name(), "--multisig", fooBarBazAddr.String(), "-y") require.True(t, success) // Write the output to disk @@ -810,7 +844,7 @@ func TestGaiaCLIMultisignSortSignatures(t *testing.T) { barAddr := f.KeyAddress(keyBar) // Send some tokens from one account to the other - success, _, _ := f.TxSend(keyFoo, fooBarBazAddr, sdk.NewInt64Coin(denom, 10)) + success, _, _ := f.TxSend(keyFoo, fooBarBazAddr, sdk.NewInt64Coin(denom, 10), "-y") require.True(t, success) tests.WaitForNextNBlocksTM(1, f.Port) @@ -872,7 +906,7 @@ func TestGaiaCLIMultisign(t *testing.T) { bazAddr := f.KeyAddress(keyBaz) // Send some tokens from one account to the other - success, _, _ := f.TxSend(keyFoo, fooBarBazAddr, sdk.NewInt64Coin(denom, 10)) + success, _, _ := f.TxSend(keyFoo, fooBarBazAddr, sdk.NewInt64Coin(denom, 10), "-y") require.True(t, success) tests.WaitForNextNBlocksTM(1, f.Port) @@ -890,7 +924,7 @@ func TestGaiaCLIMultisign(t *testing.T) { defer os.Remove(unsignedTxFile.Name()) // Sign with foo's key - success, stdout, _ = f.TxSign(keyFoo, unsignedTxFile.Name(), "--multisig", fooBarBazAddr.String()) + success, stdout, _ = f.TxSign(keyFoo, unsignedTxFile.Name(), "--multisig", fooBarBazAddr.String(), "-y") require.True(t, success) // Write the output to disk @@ -898,7 +932,7 @@ func TestGaiaCLIMultisign(t *testing.T) { defer os.Remove(fooSignatureFile.Name()) // Sign with bar's key - success, stdout, _ = f.TxSign(keyBar, unsignedTxFile.Name(), "--multisig", fooBarBazAddr.String()) + success, stdout, _ = f.TxSign(keyBar, unsignedTxFile.Name(), "--multisig", fooBarBazAddr.String(), "-y") require.True(t, success) // Write the output to disk @@ -915,7 +949,7 @@ func TestGaiaCLIMultisign(t *testing.T) { defer os.Remove(signedTxFile.Name()) // Validate the multisignature - success, _, _ = f.TxSign(keyFooBarBaz, signedTxFile.Name(), "--validate-signatures") + success, _, _ = f.TxSign(keyFooBarBaz, signedTxFile.Name(), "--validate-signatures", "-y") require.True(t, success) // Broadcast the transaction diff --git a/cmd/gaia/cli_test/test_helpers.go b/cmd/gaia/cli_test/test_helpers.go index 0edd298b70f9..82da204d20ec 100644 --- a/cmd/gaia/cli_test/test_helpers.go +++ b/cmd/gaia/cli_test/test_helpers.go @@ -288,6 +288,14 @@ func (f *Fixtures) TxSend(from string, to sdk.AccAddress, amount sdk.Coin, flags return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), app.DefaultKeyPass) } +func (f *Fixtures) txSendWithConfirm( + from string, to sdk.AccAddress, amount sdk.Coin, confirm string, flags ...string, +) (bool, string, string) { + + cmd := fmt.Sprintf("gaiacli tx send %s %s %v --from=%s", to, amount, f.Flags(), from) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), confirm, app.DefaultKeyPass) +} + // TxSign is gaiacli tx sign func (f *Fixtures) TxSign(signer, fileName string, flags ...string) (bool, string, string) { cmd := fmt.Sprintf("gaiacli tx sign %v --from=%s %v", f.Flags(), signer, fileName) diff --git a/x/auth/client/txbuilder/txbuilder.go b/x/auth/client/txbuilder/txbuilder.go index 7ac59426e7eb..3ea6989e6547 100644 --- a/x/auth/client/txbuilder/txbuilder.go +++ b/x/auth/client/txbuilder/txbuilder.go @@ -174,8 +174,9 @@ func (bldr TxBuilder) WithAccountNumber(accnum uint64) TxBuilder { return bldr } -// BuildSignMsg builds a single message to be signed from a TxBuilder given a set of -// messages. It returns an error if a fee is supplied but cannot be parsed. +// BuildSignMsg builds a single message to be signed from a TxBuilder given a +// set of messages. It returns an error if a fee is supplied but cannot be +// parsed. func (bldr TxBuilder) BuildSignMsg(msgs []sdk.Msg) (StdSignMsg, error) { chainID := bldr.chainID if chainID == "" { @@ -221,8 +222,7 @@ func (bldr TxBuilder) Sign(name, passphrase string, msg StdSignMsg) ([]byte, err } // BuildAndSign builds a single message to be signed, and signs a transaction -// with the built message given a name, passphrase, and a set of -// messages. +// with the built message given a name, passphrase, and a set of messages. func (bldr TxBuilder) BuildAndSign(name, passphrase string, msgs []sdk.Msg) ([]byte, error) { msg, err := bldr.BuildSignMsg(msgs) if err != nil {