diff --git a/client/context/context.go b/client/context/context.go index 743c923552c8..52894521493b 100644 --- a/client/context/context.go +++ b/client/context/context.go @@ -32,6 +32,7 @@ type CLIContext struct { Async bool JSON bool PrintResponse bool + GenerateOnly bool } // NewCLIContext returns a new initialized CLIContext with parameters from the @@ -57,6 +58,7 @@ func NewCLIContext() CLIContext { Async: viper.GetBool(client.FlagAsync), JSON: viper.GetBool(client.FlagJson), PrintResponse: viper.GetBool(client.FlagPrintResponse), + GenerateOnly: viper.GetBool(client.FlagGenerateOnly), } } diff --git a/client/flags.go b/client/flags.go index 81e06706784a..aa9ad7a1b09a 100644 --- a/client/flags.go +++ b/client/flags.go @@ -23,6 +23,7 @@ const ( FlagAsync = "async" FlagJson = "json" FlagPrintResponse = "print-response" + FlagGenerateOnly = "generate-only" ) // LineBreak can be included in a command list to provide a blank line @@ -58,6 +59,7 @@ func PostCommands(cmds ...*cobra.Command) []*cobra.Command { c.Flags().Bool(FlagAsync, false, "broadcast transactions asynchronously") c.Flags().Bool(FlagJson, false, "return output in json format") c.Flags().Bool(FlagPrintResponse, true, "return tx response (only works with async = false)") + c.Flags().Bool(FlagGenerateOnly, false, "build an unsigned transaction and write it to STDOUT") } return cmds } diff --git a/client/utils/utils.go b/client/utils/utils.go index fb5d6198871d..e03fe827ddb1 100644 --- a/client/utils/utils.go +++ b/client/utils/utils.go @@ -7,6 +7,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/keys" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" authctx "github.com/cosmos/cosmos-sdk/x/auth/client/context" amino "github.com/tendermint/go-amino" "github.com/tendermint/tendermint/libs/common" @@ -23,37 +24,11 @@ const DefaultGasAdjustment = 1.2 // addition, it builds and signs a transaction with the supplied messages. // Finally, it broadcasts the signed transaction to a node. func SendTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg) error { - if err := cliCtx.EnsureAccountExists(); err != nil { - return err - } - - from, err := cliCtx.GetFromAddress() + txCtx, err := prepareTxContext(txCtx, cliCtx) if err != nil { return err } - // TODO: (ref #1903) Allow for user supplied account number without - // automatically doing a manual lookup. - if txCtx.AccountNumber == 0 { - accNum, err := cliCtx.GetAccountNumber(from) - if err != nil { - return err - } - - txCtx = txCtx.WithAccountNumber(accNum) - } - - // TODO: (ref #1903) Allow for user supplied account sequence without - // automatically doing a manual lookup. - if txCtx.Sequence == 0 { - accSeq, err := cliCtx.GetAccountSequence(from) - if err != nil { - return err - } - - txCtx = txCtx.WithSequence(accSeq) - } - passphrase, err := keys.GetPassphrase(cliCtx.FromAddressName) if err != nil { return err @@ -75,6 +50,25 @@ func SendTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg) return cliCtx.EnsureBroadcastTx(txBytes) } +// BuildStdSignMsg builds a StdSignMsg as per the parameters passed in the contexts. +func BuildStdSignMsg(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg) (auth.StdSignMsg, error) { + txCtx, err := prepareTxContext(txCtx, cliCtx) + if err != nil { + return auth.StdSignMsg{}, err + } + stdMsg, err := txCtx.Build(msgs) + return stdMsg, err +} + +// MarshalStdSignMsgJSON builds a StdSignMsg and returns its JSON formatting. +func MarshalStdSignMsgJSON(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg) ([]byte, error) { + stdTx, err := BuildStdSignMsg(txCtx, cliCtx, msgs) + if err != nil { + return nil, err + } + return cliCtx.Codec.MarshalJSON(stdTx) +} + // EnrichCtxWithGas calculates the gas estimate that would be consumed by the // transaction and set the transaction's respective value accordingly. func EnrichCtxWithGas(txCtx authctx.TxContext, cliCtx context.CLIContext, name, passphrase string, msgs []sdk.Msg) (authctx.TxContext, error) { @@ -113,6 +107,40 @@ func CalculateGas(queryFunc func(string, common.HexBytes) ([]byte, error), cdc * return } +func prepareTxContext(txCtx authctx.TxContext, cliCtx context.CLIContext) (authctx.TxContext, error) { + if err := cliCtx.EnsureAccountExists(); err != nil { + return txCtx, err + } + + from, err := cliCtx.GetFromAddress() + if err != nil { + return txCtx, err + } + + if txCtx.AccountNumber == 0 { + // TODO: (ref #1903) Allow for user supplied account number without + // automatically doing a manual lookup. + accNum, err := cliCtx.GetAccountNumber(from) + if err != nil { + return txCtx, err + } + + txCtx = txCtx.WithAccountNumber(accNum) + } + + // TODO: (ref #1903) Allow for user supplied account sequence without + // automatically doing a manual lookup. + if txCtx.Sequence == 0 { + accSeq, err := cliCtx.GetAccountSequence(from) + if err != nil { + return txCtx, err + } + + txCtx = txCtx.WithSequence(accSeq) + } + return txCtx, nil +} + func adjustGasEstimate(estimate int64, adjustment float64) int64 { if adjustment == 0 { return int64(DefaultGasAdjustment * float64(estimate)) diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index f794e5140df7..ea1227863858 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -276,6 +276,25 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.Equal(t, " 2 - Apples", proposalsQuery) } +func TestGaiaCLISendGenerateOnly(t *testing.T) { + chainID, servAddr, port := initializeFixtures(t) + flags := fmt.Sprintf("--home=%s --node=%v --chain-id=%v", gaiacliHome, servAddr, chainID) + + // start gaiad server + proc := tests.GoExecuteTWithStdout(t, fmt.Sprintf("gaiad start --home=%s --rpc.laddr=%v", gaiadHome, servAddr)) + + defer proc.Stop(false) + tests.WaitForTMStart(port) + tests.WaitForNextNBlocksTM(2, port) + + barAddr, _ := executeGetAddrPK(t, fmt.Sprintf("gaiacli keys show bar --output=json --home=%s", gaiacliHome)) + // Test send + success := executeWrite(t, fmt.Sprintf( + "gaiacli send %v --amount=10steak --to=%s --from=foo --generate-only", + flags, barAddr), []string{}...) + require.True(t, success) +} + //___________________________________________________________________________________ // helper methods diff --git a/x/auth/stdtx.go b/x/auth/stdtx.go index c6e280157b2f..69627b31a3d0 100644 --- a/x/auth/stdtx.go +++ b/x/auth/stdtx.go @@ -143,12 +143,12 @@ func StdSignBytes(chainID string, accnum int64, sequence int64, fee StdFee, msgs // a Msg with the other requirements for a StdSignDoc before // it is signed. For use in the CLI. type StdSignMsg struct { - ChainID string - AccountNumber int64 - Sequence int64 - Fee StdFee - Msgs []sdk.Msg - Memo string + ChainID string `json:"chain_id"` + AccountNumber int64 `json:"account_number"` + Sequence int64 `json:"sequence"` + Fee StdFee `json:"fee"` + Msgs []sdk.Msg `json:"msgs"` + Memo string `json:"memo"` } // get message bytes diff --git a/x/bank/client/cli/sendtx.go b/x/bank/client/cli/sendtx.go index 92ac37c1e973..a931711cf0ef 100644 --- a/x/bank/client/cli/sendtx.go +++ b/x/bank/client/cli/sendtx.go @@ -1,6 +1,7 @@ package cli import ( + "fmt" "os" "github.com/cosmos/cosmos-sdk/client/context" @@ -68,6 +69,13 @@ func SendTxCmd(cdc *wire.Codec) *cobra.Command { // build and sign the transaction, then broadcast to Tendermint msg := client.BuildMsg(from, to, coins) + if cliCtx.GenerateOnly { + json, err := utils.MarshalStdSignMsgJSON(txCtx, cliCtx, []sdk.Msg{msg}) + if err == nil { + fmt.Printf("%s\n", json) + } + return err + } return utils.SendTx(txCtx, cliCtx, []sdk.Msg{msg}) }, diff --git a/x/gov/client/cli/tx.go b/x/gov/client/cli/tx.go index 65c922c6c133..5fc9d99a9014 100644 --- a/x/gov/client/cli/tx.go +++ b/x/gov/client/cli/tx.go @@ -99,6 +99,13 @@ $ gaiacli gov submit-proposal --title="Test Proposal" --description="My awesome } msg := gov.NewMsgSubmitProposal(proposal.Title, proposal.Description, proposalType, fromAddr, amount) + if cliCtx.GenerateOnly { + json, err := utils.MarshalStdSignMsgJSON(txCtx, cliCtx, []sdk.Msg{msg}) + if err == nil { + fmt.Printf("%s\n", json) + } + return err + } err = msg.ValidateBasic() if err != nil { @@ -177,7 +184,13 @@ func GetCmdDeposit(cdc *wire.Codec) *cobra.Command { } msg := gov.NewMsgDeposit(depositerAddr, proposalID, amount) - + if cliCtx.GenerateOnly { + json, err := utils.MarshalStdSignMsgJSON(txCtx, cliCtx, []sdk.Msg{msg}) + if err == nil { + fmt.Printf("%s\n", json) + } + return err + } err = msg.ValidateBasic() if err != nil { return err @@ -221,7 +234,13 @@ func GetCmdVote(cdc *wire.Codec) *cobra.Command { } msg := gov.NewMsgVote(voterAddr, proposalID, byteVoteOption) - + if cliCtx.GenerateOnly { + json, err := utils.MarshalStdSignMsgJSON(txCtx, cliCtx, []sdk.Msg{msg}) + if err == nil { + fmt.Printf("%s\n", json) + } + return err + } err = msg.ValidateBasic() if err != nil { return err diff --git a/x/gov/msgs.go b/x/gov/msgs.go index 5d85f689e58c..dcd7112aa148 100644 --- a/x/gov/msgs.go +++ b/x/gov/msgs.go @@ -12,11 +12,11 @@ const MsgType = "gov" //----------------------------------------------------------- // MsgSubmitProposal type MsgSubmitProposal struct { - Title string // Title of the proposal - Description string // Description of the proposal - ProposalType ProposalKind // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal} - Proposer sdk.AccAddress // Address of the proposer - InitialDeposit sdk.Coins // Initial deposit paid by sender. Must be strictly positive. + Title string `json:"title"` // Title of the proposal + Description string `json:"description"` // Description of the proposal + ProposalType ProposalKind `json:"proposal_type"` // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal} + Proposer sdk.AccAddress `json:"proposer"` // Address of the proposer + InitialDeposit sdk.Coins `json:"initial_deposit"` // Initial deposit paid by sender. Must be strictly positive. } func NewMsgSubmitProposal(title string, description string, proposalType ProposalKind, proposer sdk.AccAddress, initialDeposit sdk.Coins) MsgSubmitProposal { diff --git a/x/ibc/client/cli/ibctx.go b/x/ibc/client/cli/ibctx.go index f44c736d891c..adda4bdac8fe 100644 --- a/x/ibc/client/cli/ibctx.go +++ b/x/ibc/client/cli/ibctx.go @@ -2,6 +2,7 @@ package cli import ( "encoding/hex" + "fmt" "os" "github.com/cosmos/cosmos-sdk/client" @@ -43,6 +44,13 @@ func IBCTransferCmd(cdc *wire.Codec) *cobra.Command { if err != nil { return err } + if cliCtx.GenerateOnly { + json, err := utils.MarshalStdSignMsgJSON(txCtx, cliCtx, []sdk.Msg{msg}) + if err == nil { + fmt.Printf("%s\n", json) + } + return err + } return utils.SendTx(txCtx, cliCtx, []sdk.Msg{msg}) }, diff --git a/x/ibc/types.go b/x/ibc/types.go index 5f9ee611bb13..b3fe6fc39240 100644 --- a/x/ibc/types.go +++ b/x/ibc/types.go @@ -22,11 +22,11 @@ func init() { // IBCPacket defines a piece of data that can be send between two separate // blockchains. type IBCPacket struct { - SrcAddr sdk.AccAddress - DestAddr sdk.AccAddress - Coins sdk.Coins - SrcChain string - DestChain string + SrcAddr sdk.AccAddress `json:"src_addr"` + DestAddr sdk.AccAddress `json:"dest_addr"` + Coins sdk.Coins `json:"coins"` + SrcChain string `json:"src_chain"` + DestChain string `json:"dest_chain"` } func NewIBCPacket(srcAddr sdk.AccAddress, destAddr sdk.AccAddress, coins sdk.Coins, diff --git a/x/slashing/client/cli/tx.go b/x/slashing/client/cli/tx.go index 2fdd65c6c3ff..a3417638ad98 100644 --- a/x/slashing/client/cli/tx.go +++ b/x/slashing/client/cli/tx.go @@ -1,6 +1,7 @@ package cli import ( + "fmt" "os" "github.com/cosmos/cosmos-sdk/client/context" @@ -33,7 +34,13 @@ func GetCmdUnjail(cdc *wire.Codec) *cobra.Command { } msg := slashing.NewMsgUnjail(validatorAddr) - + if cliCtx.GenerateOnly { + json, err := utils.MarshalStdSignMsgJSON(txCtx, cliCtx, []sdk.Msg{msg}) + if err == nil { + fmt.Printf("%s\n", json) + } + return err + } return utils.SendTx(txCtx, cliCtx, []sdk.Msg{msg}) }, } diff --git a/x/stake/client/cli/tx.go b/x/stake/client/cli/tx.go index 9f04f2ed2d38..aba04259e9e4 100644 --- a/x/stake/client/cli/tx.go +++ b/x/stake/client/cli/tx.go @@ -77,7 +77,13 @@ func GetCmdCreateValidator(cdc *wire.Codec) *cobra.Command { } else { msg = stake.NewMsgCreateValidator(validatorAddr, pk, amount, description) } - + if cliCtx.GenerateOnly { + json, err := utils.MarshalStdSignMsgJSON(txCtx, cliCtx, []sdk.Msg{msg}) + if err == nil { + fmt.Printf("%s\n", json) + } + return err + } // build and sign the transaction, then broadcast to Tendermint return utils.SendTx(txCtx, cliCtx, []sdk.Msg{msg}) }, @@ -116,6 +122,13 @@ func GetCmdEditValidator(cdc *wire.Codec) *cobra.Command { } msg := stake.NewMsgEditValidator(validatorAddr, description) + if cliCtx.GenerateOnly { + json, err := utils.MarshalStdSignMsgJSON(txCtx, cliCtx, []sdk.Msg{msg}) + if err == nil { + fmt.Printf("%s\n", json) + } + return err + } // build and sign the transaction, then broadcast to Tendermint return utils.SendTx(txCtx, cliCtx, []sdk.Msg{msg}) }, @@ -155,6 +168,13 @@ func GetCmdDelegate(cdc *wire.Codec) *cobra.Command { msg := stake.NewMsgDelegate(delegatorAddr, validatorAddr, amount) + if cliCtx.GenerateOnly { + json, err := utils.MarshalStdSignMsgJSON(txCtx, cliCtx, []sdk.Msg{msg}) + if err == nil { + fmt.Printf("%s\n", json) + } + return err + } // build and sign the transaction, then broadcast to Tendermint return utils.SendTx(txCtx, cliCtx, []sdk.Msg{msg}) }, @@ -223,6 +243,13 @@ func GetCmdBeginRedelegate(storeName string, cdc *wire.Codec) *cobra.Command { msg := stake.NewMsgBeginRedelegate(delegatorAddr, validatorSrcAddr, validatorDstAddr, sharesAmount) + if cliCtx.GenerateOnly { + json, err := utils.MarshalStdSignMsgJSON(txCtx, cliCtx, []sdk.Msg{msg}) + if err == nil { + fmt.Printf("%s\n", json) + } + return err + } // build and sign the transaction, then broadcast to Tendermint return utils.SendTx(txCtx, cliCtx, []sdk.Msg{msg}) }, @@ -311,6 +338,13 @@ func GetCmdCompleteRedelegate(cdc *wire.Codec) *cobra.Command { msg := stake.NewMsgCompleteRedelegate(delegatorAddr, validatorSrcAddr, validatorDstAddr) + if cliCtx.GenerateOnly { + json, err := utils.MarshalStdSignMsgJSON(txCtx, cliCtx, []sdk.Msg{msg}) + if err == nil { + fmt.Printf("%s\n", json) + } + return err + } // build and sign the transaction, then broadcast to Tendermint return utils.SendTx(txCtx, cliCtx, []sdk.Msg{msg}) }, @@ -372,6 +406,13 @@ func GetCmdBeginUnbonding(storeName string, cdc *wire.Codec) *cobra.Command { msg := stake.NewMsgBeginUnbonding(delegatorAddr, validatorAddr, sharesAmount) + if cliCtx.GenerateOnly { + json, err := utils.MarshalStdSignMsgJSON(txCtx, cliCtx, []sdk.Msg{msg}) + if err == nil { + fmt.Printf("%s\n", json) + } + return err + } // build and sign the transaction, then broadcast to Tendermint return utils.SendTx(txCtx, cliCtx, []sdk.Msg{msg}) }, @@ -407,6 +448,13 @@ func GetCmdCompleteUnbonding(cdc *wire.Codec) *cobra.Command { msg := stake.NewMsgCompleteUnbonding(delegatorAddr, validatorAddr) + if cliCtx.GenerateOnly { + json, err := utils.MarshalStdSignMsgJSON(txCtx, cliCtx, []sdk.Msg{msg}) + if err == nil { + fmt.Printf("%s\n", json) + } + return err + } // build and sign the transaction, then broadcast to Tendermint return utils.SendTx(txCtx, cliCtx, []sdk.Msg{msg}) },