diff --git a/PENDING.md b/PENDING.md index e9182f22afae..9254f69b9a0b 100644 --- a/PENDING.md +++ b/PENDING.md @@ -49,10 +49,11 @@ BREAKING CHANGES FEATURES * Gaia REST API (`gaiacli advanced rest-server`) - * [lcd] Endpoints to query staking pool and params - * [lcd] [\#2110](https://github.com/cosmos/cosmos-sdk/issues/2110) Add support for `simulate=true` requests query argument to endpoints that send txs to run simulations of transactions - * [lcd] [\#966](https://github.com/cosmos/cosmos-sdk/issues/966) Add support for `generate_only=true` query argument to generate offline unsigned transactions - * [lcd] [\#1953](https://github.com/cosmos/cosmos-sdk/issues/1953) Add /sign endpoint to sign transactions generated with `generate_only=true`. + * [gaia-lite] Endpoints to query staking pool and params + * [gaia-lite] [\#2110](https://github.com/cosmos/cosmos-sdk/issues/2110) Add support for `simulate=true` requests query argument to endpoints that send txs to run simulations of transactions + * [gaia-lite] [\#966](https://github.com/cosmos/cosmos-sdk/issues/966) Add support for `generate_only=true` query argument to generate offline unsigned transactions + * [gaia-lite] [\#1953](https://github.com/cosmos/cosmos-sdk/issues/1953) Add /sign endpoint to sign transactions generated with `generate_only=true`. + * [gaia-lite] [\#1954](https://github.com/cosmos/cosmos-sdk/issues/1954) Add /broadcast endpoint to broadcast transactions signed by the /sign endpoint. * Gaia CLI (`gaiacli`) * [cli] Cmds to query staking pool and params @@ -65,6 +66,7 @@ FEATURES * [cli] [\#2204](https://github.com/cosmos/cosmos-sdk/issues/2204) Support generating and broadcasting messages with multiple signatures via command line: * [\#966](https://github.com/cosmos/cosmos-sdk/issues/966) Add --generate-only flag to build an unsigned transaction and write it to STDOUT. * [\#1953](https://github.com/cosmos/cosmos-sdk/issues/1953) New `sign` command to sign transactions generated with the --generate-only flag. + * [\#1954](https://github.com/cosmos/cosmos-sdk/issues/1954) New `broadcast` command to broadcast transactions generated offline and signed with the `sign` command. * Gaia * [cli] #2170 added ability to show the node's address via `gaiad tendermint show-address` diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index e6cfd24a7b8e..7fc98aef6fee 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -314,11 +314,12 @@ func TestIBCTransfer(t *testing.T) { // TODO: query ibc egress packet state } -func TestCoinSendGenerateAndSign(t *testing.T) { +func TestCoinSendGenerateSignAndBroadcast(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() + acc := getAccount(t, port, addr) // generate TX res, body, _ := doSendWithGas(t, port, seed, name, password, addr, 0, 0, "?generate_only=true") @@ -329,10 +330,10 @@ func TestCoinSendGenerateAndSign(t *testing.T) { 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)) + gasEstimate := msg.Fee.Gas // sign tx var signedMsg auth.StdTx - acc := getAccount(t, port, addr) accnum := acc.GetAccountNumber() sequence := acc.GetSequence() @@ -346,13 +347,30 @@ func TestCoinSendGenerateAndSign(t *testing.T) { } json, err := cdc.MarshalJSON(payload) require.Nil(t, err) - res, body = Request(t, port, "POST", "/sign", json) + res, body = Request(t, port, "POST", "/tx/sign", json) require.Equal(t, http.StatusOK, res.StatusCode, body) require.Nil(t, cdc.UnmarshalJSON([]byte(body), &signedMsg)) require.Equal(t, len(msg.Msgs), len(signedMsg.Msgs)) require.Equal(t, msg.Msgs[0].Type(), signedMsg.Msgs[0].Type()) require.Equal(t, msg.Msgs[0].GetSigners(), signedMsg.Msgs[0].GetSigners()) require.Equal(t, 1, len(signedMsg.Signatures)) + + // broadcast tx + broadcastPayload := struct { + Tx auth.StdTx `json:"tx"` + }{Tx: signedMsg} + json, err = cdc.MarshalJSON(broadcastPayload) + require.Nil(t, err) + res, body = Request(t, port, "POST", "/tx/broadcast", json) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + // check if tx was committed + var resultTx ctypes.ResultBroadcastTxCommit + require.Nil(t, cdc.UnmarshalJSON([]byte(body), &resultTx)) + require.Equal(t, uint32(0), resultTx.CheckTx.Code) + require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + require.Equal(t, gasEstimate, resultTx.DeliverTx.GasWanted) + require.Equal(t, gasEstimate, resultTx.DeliverTx.GasUsed) } func TestTxs(t *testing.T) { diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index ab1a39ac8d03..40f3872fe735 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -343,7 +343,7 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.Equal(t, " 2 - Apples", proposalsQuery) } -func TestGaiaCLISendGenerateAndSign(t *testing.T) { +func TestGaiaCLISendGenerateSignAndBroadcast(t *testing.T) { chainID, servAddr, port := initializeFixtures(t) flags := fmt.Sprintf("--home=%s --node=%v --chain-id=%v", gaiacliHome, servAddr, chainID) @@ -368,26 +368,26 @@ func TestGaiaCLISendGenerateAndSign(t *testing.T) { require.Equal(t, len(msg.Msgs), 1) require.Equal(t, 0, len(msg.GetSignatures())) - // Test generate sendTx, estimate gas + // Test generate sendTx with --gas=$amount success, stdout, stderr = executeWriteRetStdStreams(t, fmt.Sprintf( - "gaiacli send %v --amount=10steak --to=%s --from=foo --gas=0 --generate-only", + "gaiacli send %v --amount=10steak --to=%s --from=foo --gas=100 --generate-only", flags, barAddr), []string{}...) require.True(t, success) - require.NotEmpty(t, stderr) + require.Empty(t, stderr) msg = unmarshalStdTx(t, stdout) - require.NotZero(t, msg.Fee.Gas) + require.Equal(t, msg.Fee.Gas, int64(100)) require.Equal(t, len(msg.Msgs), 1) + require.Equal(t, 0, len(msg.GetSignatures())) - // Test generate sendTx with --gas=$amount + // Test generate sendTx, estimate gas success, stdout, stderr = executeWriteRetStdStreams(t, fmt.Sprintf( - "gaiacli send %v --amount=10steak --to=%s --from=foo --gas=100 --generate-only", + "gaiacli send %v --amount=10steak --to=%s --from=foo --gas=0 --generate-only", flags, barAddr), []string{}...) require.True(t, success) - require.Empty(t, stderr) + require.NotEmpty(t, stderr) msg = unmarshalStdTx(t, stdout) - require.Equal(t, msg.Fee.Gas, int64(100)) + require.True(t, msg.Fee.Gas > 0) require.Equal(t, len(msg.Msgs), 1) - require.Equal(t, 0, len(msg.GetSignatures())) // Write the output to disk unsignedTxFile := writeToNewTempFile(t, stdout) @@ -417,6 +417,19 @@ func TestGaiaCLISendGenerateAndSign(t *testing.T) { "gaiacli sign %v --print-sigs %v", flags, signedTxFile.Name())) require.True(t, success) require.Equal(t, fmt.Sprintf("Signers:\n 0: %v\n\nSignatures:\n 0: %v\n", fooAddr.String(), fooAddr.String()), stdout) + + // Test broadcast + fooAcc := executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags)) + require.Equal(t, int64(50), fooAcc.GetCoins().AmountOf("steak").Int64()) + + success = executeWrite(t, fmt.Sprintf("gaiacli broadcast %v %v", flags, signedTxFile.Name())) + require.True(t, success) + tests.WaitForNextNBlocksTM(2, port) + + barAcc := executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", barAddr, flags)) + require.Equal(t, int64(10), barAcc.GetCoins().AmountOf("steak").Int64()) + fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli account %s %v", fooAddr, flags)) + require.Equal(t, int64(40), fooAcc.GetCoins().AmountOf("steak").Int64()) } //___________________________________________________________________________________ diff --git a/cmd/gaia/cmd/gaiacli/main.go b/cmd/gaia/cmd/gaiacli/main.go index 7a1af622145d..c216bb2eb065 100644 --- a/cmd/gaia/cmd/gaiacli/main.go +++ b/cmd/gaia/cmd/gaiacli/main.go @@ -132,6 +132,7 @@ func main() { rootCmd.AddCommand( client.PostCommands( bankcmd.SendTxCmd(cdc), + bankcmd.GetBroadcastCommand(cdc), )...) // add proxy, version and key info diff --git a/docs/light/api.md b/docs/light/api.md index 293b023cd063..6c7f9de177de 100644 --- a/docs/light/api.md +++ b/docs/light/api.md @@ -226,12 +226,11 @@ Returns on success: "sequence": 7 } } -} ``` -### POST /auth/accounts/sign +### POST /auth/tx/sign -- **URL**: `/auth/sign` +- **URL**: `/auth/tx/sign` - **Functionality**: Sign a transaction without broadcasting it. - Returns on success: @@ -298,6 +297,45 @@ Returns on success: } ``` +### POST /auth/tx/broadcast + +- **URL**: `/auth/broadcast` +- **Functionality**: Broadcast a transaction. +- Returns on success: + +```json +{ + "rest api": "1.0", + "code": 200, + "error": "", + "result": + { + "check_tx": { + "log": "Msg 0: ", + "gasWanted": "2742", + "gasUsed": "1002" + }, + "deliver_tx": { + "log": "Msg 0: ", + "gasWanted": "2742", + "gasUsed": "2742", + "tags": [ + { + "key": "c2VuZGVy", + "value": "Y29zbW9zMXdjNTl6ZXU3MmNjdnp5ZWR6ZGE1N3pzcXh2eXZ2Y3poaHBhdDI4" + }, + { + "key": "cmVjaXBpZW50", + "value": "Y29zbW9zMTJ4OTNmY3V2azg3M3o1ejZnejRlNTl2dnlxcXp1eDdzdDcwNWd5" + } + ] + }, + "hash": "784314784503582AC885BD6FB0D2A5B79FF703A7", + "height": "5" + } +} +``` + ## ICS20 - TokenAPI The TokenAPI exposes all functionality needed to query account balances and send transactions. diff --git a/docs/sdk/clients.md b/docs/sdk/clients.md index 2b2e8e52a4ec..4d02d3c90850 100644 --- a/docs/sdk/clients.md +++ b/docs/sdk/clients.md @@ -159,6 +159,12 @@ gaiacli sign \ unsignedSendTx.json > signedSendTx.json ``` +You can broadcast the signed transaction to a node by providing the JSON file to the following command: + +``` +gaiacli broadcast --node= signedSendTx.json +``` + ### Staking #### Set up a Validator diff --git a/x/auth/client/rest/query.go b/x/auth/client/rest/query.go index 3a5ab756d16e..9e5fc88b5cb9 100644 --- a/x/auth/client/rest/query.go +++ b/x/auth/client/rest/query.go @@ -21,7 +21,7 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *wire.Codec, s QueryAccountRequestHandlerFn(storeName, cdc, authcmd.GetAccountDecoder(cdc), cliCtx), ).Methods("GET") r.HandleFunc( - "/sign", + "/tx/sign", SignTxRequestHandlerFn(cdc, cliCtx), ).Methods("POST") } diff --git a/x/bank/client/cli/broadcast.go b/x/bank/client/cli/broadcast.go new file mode 100644 index 000000000000..c96b924d5663 --- /dev/null +++ b/x/bank/client/cli/broadcast.go @@ -0,0 +1,46 @@ +package cli + +import ( + "io/ioutil" + "os" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/spf13/cobra" + amino "github.com/tendermint/go-amino" +) + +// GetSignCommand returns the sign command +func GetBroadcastCommand(codec *amino.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "broadcast ", + Short: "Broadcast transactions generated offline", + Long: `Broadcast transactions created with the --generate-only flag and signed with the sign command. +Read a transaction from and broadcast it to a node.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + cliCtx := context.NewCLIContext().WithCodec(codec).WithLogger(os.Stdout) + stdTx, err := readAndUnmarshalStdTx(cliCtx.Codec, args[0]) + if err != nil { + return + } + txBytes, err := cliCtx.Codec.MarshalBinary(stdTx) + if err != nil { + return + } + return cliCtx.EnsureBroadcastTx(txBytes) + }, + } + return cmd +} + +func readAndUnmarshalStdTx(cdc *amino.Codec, filename string) (stdTx auth.StdTx, err error) { + var bytes []byte + if bytes, err = ioutil.ReadFile(filename); err != nil { + return + } + if err = cdc.UnmarshalJSON(bytes, &stdTx); err != nil { + return + } + return +} diff --git a/x/bank/client/rest/broadcast.go b/x/bank/client/rest/broadcast.go new file mode 100644 index 000000000000..5124b791bad2 --- /dev/null +++ b/x/bank/client/rest/broadcast.go @@ -0,0 +1,57 @@ +package rest + +import ( + "io/ioutil" + "net/http" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/utils" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/auth" +) + +type broadcastBody struct { + Tx auth.StdTx `json:"tx"` +} + +// BroadcastTxRequestHandlerFn returns the broadcast tx REST handler +func BroadcastTxRequestHandlerFn(cdc *wire.Codec, cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var m broadcastBody + if ok := unmarshalBodyOrReturnBadRequest(cliCtx, w, r, &m); !ok { + return + } + + txBytes, err := cliCtx.Codec.MarshalBinary(m.Tx) + if err != nil { + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + res, err := cliCtx.BroadcastTx(txBytes) + if err != nil { + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + output, err := wire.MarshalJSONIndent(cdc, res) + if err != nil { + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + w.Write(output) + } +} + +func unmarshalBodyOrReturnBadRequest(cliCtx context.CLIContext, w http.ResponseWriter, r *http.Request, m *broadcastBody) bool { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return false + } + err = cliCtx.Codec.UnmarshalJSON(body, m) + if err != nil { + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return false + } + return true +} diff --git a/x/bank/client/rest/sendtx.go b/x/bank/client/rest/sendtx.go index b3694959d248..82df24642f36 100644 --- a/x/bank/client/rest/sendtx.go +++ b/x/bank/client/rest/sendtx.go @@ -20,6 +20,7 @@ import ( // RegisterRoutes - Central function to define routes that get registered by the main application func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *wire.Codec, kb keys.Keybase) { r.HandleFunc("/accounts/{address}/send", SendRequestHandlerFn(cdc, kb, cliCtx)).Methods("POST") + r.HandleFunc("/tx/broadcast", BroadcastTxRequestHandlerFn(cdc, cliCtx)).Methods("POST") } type sendBody struct {