Skip to content

Commit

Permalink
Merge PR #2165: Implement generate-only option for commands that crea…
Browse files Browse the repository at this point in the history
…te txs
  • Loading branch information
cwgoes authored Sep 4, 2018
2 parents e783b90 + 8378f3e commit a612068
Show file tree
Hide file tree
Showing 22 changed files with 281 additions and 31 deletions.
2 changes: 2 additions & 0 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ FEATURES
* Gaia REST API (`gaiacli advanced rest-server`)
* [lcd] Endpoints to query staking pool and params
* [lcd] \#2110 Add support for `simulate=true` requests query argument to endpoints that send txs to run simulations of transactions
* [lcd] \#966 Add support for `generate_only=true` query argument to generate offline unsigned transactions

* Gaia CLI (`gaiacli`)
* [cli] Cmds to query staking pool and params
Expand All @@ -55,6 +56,7 @@ FEATURES
* [cli] \#2047 Setting the --gas flag value to 0 triggers a simulation of the tx before the actual execution. The gas estimate obtained via the simulation will be used as gas limit in the actual execution.
* [cli] \#2047 The --gas-adjustment flag can be used to adjust the estimate obtained via the simulation triggered by --gas=0.
* [cli] \#2110 Add --dry-run flag to perform a simulation of a transaction without broadcasting it. The --gas flag is ignored as gas would be automatically estimated.
* [cli] \#966 Add --generate-only flag to build an unsigned transaction and write it to STDOUT.

* Gaia
* [cli] #2170 added ability to show the node's address via `gaiad tendermint show-address`
Expand Down
5 changes: 4 additions & 1 deletion client/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package context
import (
"bytes"
"fmt"
"io"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/wire"
"github.com/cosmos/cosmos-sdk/x/auth"
"io"

"github.com/spf13/viper"

Expand Down Expand Up @@ -38,6 +39,7 @@ type CLIContext struct {
PrintResponse bool
Certifier tmlite.Certifier
DryRun bool
GenerateOnly bool
}

// NewCLIContext returns a new initialized CLIContext with parameters from the
Expand Down Expand Up @@ -65,6 +67,7 @@ func NewCLIContext() CLIContext {
PrintResponse: viper.GetBool(client.FlagPrintResponse),
Certifier: createCertifier(),
DryRun: viper.GetBool(client.FlagDryRun),
GenerateOnly: viper.GetBool(client.FlagGenerateOnly),
}
}

Expand Down
2 changes: 2 additions & 0 deletions client/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
FlagJson = "json"
FlagPrintResponse = "print-response"
FlagDryRun = "dry-run"
FlagGenerateOnly = "generate-only"
)

// LineBreak can be included in a command list to provide a blank line
Expand Down Expand Up @@ -64,6 +65,7 @@ func PostCommands(cmds ...*cobra.Command) []*cobra.Command {
c.Flags().Bool(FlagPrintResponse, true, "return tx response (only works with async = false)")
c.Flags().Bool(FlagTrustNode, true, "Don't verify proofs for query 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")
}
return cmds
}
16 changes: 16 additions & 0 deletions client/lcd/lcd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,22 @@ func TestIBCTransfer(t *testing.T) {
// TODO: query ibc egress packet state
}

func TestCoinSendGenerateOnly(t *testing.T) {
name, password := "test", "1234567890"
addr, seed := CreateAddr(t, "test", password, GetKeyBase(t))
cleanup, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr})
defer cleanup()
// create TX
res, body, _ := doSendWithGas(t, port, seed, name, password, addr, 0, 0, "?generate_only=true")
require.Equal(t, http.StatusOK, res.StatusCode, body)
var msg auth.StdTx
require.Nil(t, cdc.UnmarshalJSON([]byte(body), &msg))
require.Equal(t, len(msg.Msgs), 1)
require.Equal(t, msg.Msgs[0].Type(), "bank")
require.Equal(t, msg.Msgs[0].GetSigners(), []sdk.AccAddress{addr})
require.Equal(t, 0, len(msg.Signatures))
}

func TestTxs(t *testing.T) {
name, password := "test", "1234567890"
addr, seed := CreateAddr(t, "test", password, GetKeyBase(t))
Expand Down
33 changes: 29 additions & 4 deletions client/utils/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ package utils
import (
"fmt"
"net/http"
"net/url"
"strconv"

sdk "github.com/cosmos/cosmos-sdk/types"
auth "github.com/cosmos/cosmos-sdk/x/auth"
authctx "github.com/cosmos/cosmos-sdk/x/auth/client/context"
)

const (
queryArgDryRun = "simulate"
queryArgDryRun = "simulate"
queryArgGenerateOnly = "generate_only"
)

// WriteErrorResponse prepares and writes a HTTP error
Expand All @@ -26,9 +32,10 @@ func WriteSimulationResponse(w http.ResponseWriter, gas int64) {

// HasDryRunArg returns true if the request's URL query contains
// the dry run argument and its value is set to "true".
func HasDryRunArg(r *http.Request) bool {
return r.URL.Query().Get(queryArgDryRun) == "true"
}
func HasDryRunArg(r *http.Request) bool { return urlQueryHasArg(r.URL, queryArgDryRun) }

// HasGenerateOnlyArg returns whether a URL's query "generate-only" parameter is set to "true".
func HasGenerateOnlyArg(r *http.Request) bool { return urlQueryHasArg(r.URL, queryArgGenerateOnly) }

// ParseFloat64OrReturnBadRequest converts s to a float64 value. It returns a default
// value if the string is empty. Write
Expand All @@ -43,3 +50,21 @@ func ParseFloat64OrReturnBadRequest(w http.ResponseWriter, s string, defaultIfEm
}
return n, true
}

// WriteGenerateStdTxResponse writes response for the generate_only mode.
func WriteGenerateStdTxResponse(w http.ResponseWriter, txCtx authctx.TxContext, msgs []sdk.Msg) {
stdMsg, err := txCtx.Build(msgs)
if err != nil {
WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
output, err := txCtx.Codec.MarshalJSON(auth.NewStdTx(stdMsg.Msgs, stdMsg.Fee, nil, stdMsg.Memo))
if err != nil {
WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
w.Write(output)
return
}

func urlQueryHasArg(url *url.URL, arg string) bool { return url.Query().Get(arg) == "true" }
37 changes: 36 additions & 1 deletion client/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
auth "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"
Expand All @@ -28,7 +29,7 @@ func SendTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg)
if err != nil {
return err
}
fmt.Fprintf(os.Stdout, "estimated gas = %v\n", txCtx.Gas)
fmt.Fprintf(os.Stderr, "estimated gas = %v\n", txCtx.Gas)
}
if cliCtx.DryRun {
return nil
Expand Down Expand Up @@ -85,6 +86,19 @@ func CalculateGas(queryFunc func(string, common.HexBytes) ([]byte, error), cdc *
return
}

// PrintUnsignedStdTx builds an unsigned StdTx and prints it to os.Stdout.
func PrintUnsignedStdTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg) (err error) {
stdTx, err := buildUnsignedStdTx(txCtx, cliCtx, msgs)
if err != nil {
return
}
json, err := txCtx.Codec.MarshalJSON(stdTx)
if err == nil {
fmt.Printf("%s\n", json)
}
return
}

func adjustGasEstimate(estimate int64, adjustment float64) int64 {
return int64(adjustment * float64(estimate))
}
Expand Down Expand Up @@ -128,3 +142,24 @@ func prepareTxContext(txCtx authctx.TxContext, cliCtx context.CLIContext) (authc
}
return txCtx, nil
}

// buildUnsignedStdTx builds a StdTx as per the parameters passed in the
// contexts. Gas is automatically estimated if gas wanted is set to 0.
func buildUnsignedStdTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs []sdk.Msg) (stdTx auth.StdTx, err error) {
txCtx, err = prepareTxContext(txCtx, cliCtx)
if err != nil {
return
}
if txCtx.Gas == 0 {
txCtx, err = EnrichCtxWithGas(txCtx, cliCtx, cliCtx.FromAddressName, msgs)
if err != nil {
return
}
fmt.Fprintf(os.Stderr, "estimated gas = %v\n", txCtx.Gas)
}
stdSignMsg, err := txCtx.Build(msgs)
if err != nil {
return
}
return auth.NewStdTx(stdSignMsg.Msgs, stdSignMsg.Fee, nil, stdSignMsg.Memo), nil
}
97 changes: 95 additions & 2 deletions cmd/gaia/cli_test/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/tendermint/tendermint/crypto"
cmn "github.com/tendermint/tendermint/libs/common"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/keys"
"github.com/cosmos/cosmos-sdk/cmd/gaia/app"
"github.com/cosmos/cosmos-sdk/server"
Expand Down Expand Up @@ -155,8 +156,18 @@ func TestGaiaCLICreateValidator(t *testing.T) {

initialPool.BondedTokens = initialPool.BondedTokens.Add(sdk.NewDec(1))

// Test --generate-only
success, stdout, stderr := executeWriteRetStdStreams(t, cvStr+" --generate-only", app.DefaultKeyPass)
require.True(t, success)
require.True(t, success)
require.Empty(t, stderr)
msg := unmarshalStdTx(t, stdout)
require.NotZero(t, msg.Fee.Gas)
require.Equal(t, len(msg.Msgs), 1)
require.Equal(t, 0, len(msg.GetSignatures()))

// Test --dry-run
success := executeWrite(t, cvStr+" --dry-run", app.DefaultKeyPass)
success = executeWrite(t, cvStr+" --dry-run", app.DefaultKeyPass)
require.True(t, success)

executeWrite(t, cvStr, app.DefaultKeyPass)
Expand Down Expand Up @@ -222,8 +233,18 @@ func TestGaiaCLISubmitProposal(t *testing.T) {
spStr += fmt.Sprintf(" --title=%s", "Test")
spStr += fmt.Sprintf(" --description=%s", "test")

// Test generate only
success, stdout, stderr := executeWriteRetStdStreams(t, spStr+" --generate-only", app.DefaultKeyPass)
require.True(t, success)
require.True(t, success)
require.Empty(t, stderr)
msg := unmarshalStdTx(t, stdout)
require.NotZero(t, msg.Fee.Gas)
require.Equal(t, len(msg.Msgs), 1)
require.Equal(t, 0, len(msg.GetSignatures()))

// Test --dry-run
success := executeWrite(t, spStr+" --dry-run", app.DefaultKeyPass)
success = executeWrite(t, spStr+" --dry-run", app.DefaultKeyPass)
require.True(t, success)

executeWrite(t, spStr, app.DefaultKeyPass)
Expand All @@ -244,6 +265,16 @@ func TestGaiaCLISubmitProposal(t *testing.T) {
depositStr += fmt.Sprintf(" --deposit=%s", "10steak")
depositStr += fmt.Sprintf(" --proposal-id=%s", "1")

// Test generate only
success, stdout, stderr = executeWriteRetStdStreams(t, depositStr+" --generate-only", app.DefaultKeyPass)
require.True(t, success)
require.True(t, success)
require.Empty(t, stderr)
msg = unmarshalStdTx(t, stdout)
require.NotZero(t, msg.Fee.Gas)
require.Equal(t, len(msg.Msgs), 1)
require.Equal(t, 0, len(msg.GetSignatures()))

executeWrite(t, depositStr, app.DefaultKeyPass)
tests.WaitForNextNBlocksTM(2, port)

Expand All @@ -258,6 +289,16 @@ func TestGaiaCLISubmitProposal(t *testing.T) {
voteStr += fmt.Sprintf(" --proposal-id=%s", "1")
voteStr += fmt.Sprintf(" --option=%s", "Yes")

// Test generate only
success, stdout, stderr = executeWriteRetStdStreams(t, voteStr+" --generate-only", app.DefaultKeyPass)
require.True(t, success)
require.True(t, success)
require.Empty(t, stderr)
msg = unmarshalStdTx(t, stdout)
require.NotZero(t, msg.Fee.Gas)
require.Equal(t, len(msg.Msgs), 1)
require.Equal(t, 0, len(msg.GetSignatures()))

executeWrite(t, voteStr, app.DefaultKeyPass)
tests.WaitForNextNBlocksTM(2, port)

Expand Down Expand Up @@ -291,6 +332,52 @@ 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 generate sendTx with default gas
success, stdout, stderr := executeWriteRetStdStreams(t, fmt.Sprintf(
"gaiacli send %v --amount=10steak --to=%s --from=foo --generate-only",
flags, barAddr), []string{}...)
require.True(t, success)
require.Empty(t, stderr)
msg := unmarshalStdTx(t, stdout)
require.Equal(t, msg.Fee.Gas, int64(client.DefaultGasLimit))
require.Equal(t, len(msg.Msgs), 1)
require.Equal(t, 0, len(msg.GetSignatures()))

// Test generate sendTx, estimate gas
success, stdout, stderr = executeWriteRetStdStreams(t, fmt.Sprintf(
"gaiacli send %v --amount=10steak --to=%s --from=foo --gas=0 --generate-only",
flags, barAddr), []string{}...)
require.True(t, success)
require.NotEmpty(t, stderr)
msg = unmarshalStdTx(t, stdout)
require.NotZero(t, msg.Fee.Gas)
require.Equal(t, len(msg.Msgs), 1)

// Test generate sendTx with --gas=$amount
success, stdout, stderr = executeWriteRetStdStreams(t, fmt.Sprintf(
"gaiacli send %v --amount=10steak --to=%s --from=foo --gas=100 --generate-only",
flags, barAddr), []string{}...)
require.True(t, success)
require.Empty(t, stderr)
msg = unmarshalStdTx(t, stdout)
require.Equal(t, msg.Fee.Gas, int64(100))
require.Equal(t, len(msg.Msgs), 1)
require.Equal(t, 0, len(msg.GetSignatures()))
}

//___________________________________________________________________________________
// helper methods

Expand All @@ -315,6 +402,12 @@ func initializeFixtures(t *testing.T) (chainID, servAddr, port string) {
return
}

func unmarshalStdTx(t *testing.T, s string) (stdTx auth.StdTx) {
cdc := app.MakeCodec()
require.Nil(t, cdc.UnmarshalJSON([]byte(s), &stdTx))
return
}

//___________________________________________________________________________________
// executors

Expand Down
4 changes: 2 additions & 2 deletions crypto/keys/keybase.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,9 @@ func (kb dbKeybase) Sign(name, passphrase string, msg []byte) (sig []byte, pub t
}
case offlineInfo:
linfo := info.(offlineInfo)
fmt.Printf("Bytes to sign:\n%s", msg)
fmt.Fprintf(os.Stderr, "Bytes to sign:\n%s", msg)
buf := bufio.NewReader(os.Stdin)
fmt.Printf("\nEnter Amino-encoded signature:\n")
fmt.Fprintf(os.Stderr, "\nEnter Amino-encoded signature:\n")
// Will block until user inputs the signature
signed, err := buf.ReadString('\n')
if err != nil {
Expand Down
11 changes: 11 additions & 0 deletions docs/sdk/clients.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ gaiacli send \
--dry-run
```

Furthermore, you can build a transaction and print its JSON format to STDOUT by appending `--generate-only` to the list of the command line arguments:

```bash
gaiacli send \
--amount=10faucetToken \
--chain-id=<chain_id> \
--name=<key_name> \
--to=<destination_cosmosaccaddr> \
--generate-only
```

### Staking

#### Set up a Validator
Expand Down
12 changes: 6 additions & 6 deletions x/auth/stdtx.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit a612068

Please sign in to comment.