Merge PR #3698: Prompt User Confirmation Prior to Signing & Broadcasting

* Prompt user confirmation prior to sign & broadcasting
* Update confirmation message
* Update and fix existing CLI integration tests
* Implement CLI integration test for tx confirmation
* Fix order of input into tx send
This commit is contained in:
Alexander Bezobchuk 2019-02-26 06:34:01 -05:00 committed by Christopher Goes
parent 250dc9807b
commit feb98bcd05
8 changed files with 105 additions and 37 deletions

View File

@ -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.

View File

@ -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),
}
}

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 {