Merge PR #2240: New broadcast command

Implement broadcast command/REST endpoint to submit transactions
generated offline with --generated-only and the sign command.
This commit is contained in:
Alessio Treglia 2018-09-08 10:26:20 +01:00 committed by Christopher Goes
parent 88a2ddeb25
commit 4448d175ad
10 changed files with 216 additions and 22 deletions

View File

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

View File

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

View File

@ -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,16 +368,6 @@ func TestGaiaCLISendGenerateAndSign(t *testing.T) {
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",
@ -389,6 +379,16 @@ func TestGaiaCLISendGenerateAndSign(t *testing.T) {
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.True(t, msg.Fee.Gas > 0)
require.Equal(t, len(msg.Msgs), 1)
// Write the output to disk
unsignedTxFile := writeToNewTempFile(t, stdout)
defer os.Remove(unsignedTxFile.Name())
@ -417,6 +417,25 @@ 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, stdout, _ = executeWriteRetStdStreams(t, fmt.Sprintf("gaiacli broadcast %v --json %v", flags, signedTxFile.Name()))
require.True(t, success)
var result struct {
Response abci.ResponseDeliverTx
}
require.Nil(t, app.MakeCodec().UnmarshalJSON([]byte(stdout), &result))
require.Equal(t, msg.Fee.Gas, result.Response.GasUsed)
require.Equal(t, msg.Fee.Gas, result.Response.GasWanted)
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())
}
//___________________________________________________________________________________

View File

@ -132,6 +132,7 @@ func main() {
rootCmd.AddCommand(
client.PostCommands(
bankcmd.SendTxCmd(cdc),
bankcmd.GetBroadcastCommand(cdc),
)...)
// add proxy, version and key info

View File

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

View File

@ -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=<node> signedSendTx.json
```
### Staking
#### Set up a Validator

View File

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

View File

@ -0,0 +1,52 @@
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 <file>",
Short: "Broadcast transactions generated offline",
Long: `Broadcast transactions created with the --generate-only flag and signed with the sign command.
Read a transaction from <file> and broadcast it to a node. If you supply a dash (-) argument
in place of an input filename, the command reads from standard input.`,
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 filename == "-" {
bytes, err = ioutil.ReadAll(os.Stdin)
} else {
bytes, err = ioutil.ReadFile(filename)
}
if err != nil {
return
}
if err = cdc.UnmarshalJSON(bytes, &stdTx); err != nil {
return
}
return
}

View File

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

View File

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