diff --git a/.pending/features/gaiacli/3872-Add-gaiacli-tx- b/.pending/features/gaiacli/3872-Add-gaiacli-tx- new file mode 100644 index 000000000000..5e521f3c93e6 --- /dev/null +++ b/.pending/features/gaiacli/3872-Add-gaiacli-tx- @@ -0,0 +1 @@ +#3872 Implement a command to decode txs via `gaiacli tx decode` diff --git a/.pending/features/gaiarest/3872-Add-POST-txs-de b/.pending/features/gaiarest/3872-Add-POST-txs-de new file mode 100644 index 000000000000..55f976eec6a5 --- /dev/null +++ b/.pending/features/gaiarest/3872-Add-POST-txs-de @@ -0,0 +1 @@ +#3872 Implement a RESTful endpoint to decode txs via `POST /txs/decode` diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 69c5281b38f3..7525515678e7 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -319,6 +319,46 @@ func TestEncodeTx(t *testing.T) { require.Equal(t, memo, decodedTx.Memo) } +func TestDecodeTx(t *testing.T) { + kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) + require.NoError(t, err) + addr, seed := CreateAddr(t, name1, pw, kb) + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}, true) + defer cleanup() + + res, body, _ := doTransferWithGas(t, port, seed, name1, memo, "", addr, "2", 1, false, false, fees) + var tx auth.StdTx + _ = cdc.UnmarshalJSON([]byte(body), &tx) + + encodeReq := clienttx.EncodeReq{Tx: tx} + encodedJSON, _ := cdc.MarshalJSON(encodeReq) + res, body = Request(t, port, "POST", "/txs/encode", encodedJSON) + + // Make sure it came back ok, and that we can encode it back to the transaction + // 200 response. + require.Equal(t, http.StatusOK, res.StatusCode, body) + encodeResp := struct { + Tx string `json:"tx"` + }{} + + require.Nil(t, cdc.UnmarshalJSON([]byte(body), &encodeResp)) + + decodeReq := clienttx.DecodeReq{Tx: encodeResp.Tx} + decodedJSON, _ := cdc.MarshalJSON(decodeReq) + res, body = Request(t, port, "POST", "/txs/decode", decodedJSON) + + // Make sure it came back ok, and that we can decode it back to the transaction + // 200 response. + require.Equal(t, http.StatusOK, res.StatusCode, body) + decodeResp := auth.StdTx{} + + aminoJson := "{\"type\": \"auth/StdTx\",\"value\": " + body + "}" + require.Nil(t, cdc.UnmarshalJSON([]byte(aminoJson), &decodeResp)) + + // check that the transaction decodes as expected + require.Equal(t, memo, decodeResp.Memo) +} + func TestTxs(t *testing.T) { kb, err := keys.NewKeyBaseFromDir(InitClientHome(t, "")) require.NoError(t, err) diff --git a/client/lcd/swagger-ui/swagger.yaml b/client/lcd/swagger-ui/swagger.yaml index 8a0df1e318f8..df3414cfcceb 100644 --- a/client/lcd/swagger-ui/swagger.yaml +++ b/client/lcd/swagger-ui/swagger.yaml @@ -322,6 +322,36 @@ paths: description: The tx was malformated 500: description: Server internal error + /txs/decode: + post: + tags: + - ICS0 + summary: Decode a transaction from the Amino wire format + description: Decode a transaction (signed or not) from base64-encoded Amino serialized bytes to JSON + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: tx + description: The tx to decode + required: true + schema: + type: object + properties: + tx: + type: string + example: SvBiXe4KPqijYZoKFFHEzJ8c2HPAfv2EFUcIhx0yPagwEhTy0vPA+GGhCEslKXa4Af0uB+mfShoMCgVzdGFrZRIDMTAwEgQQwJoM + responses: + 200: + description: The tx was successfully decoded + schema: + $ref: "#/definitions/StdTx" + 400: + description: The tx was malformated + 500: + description: Server internal error /bank/balances/{address}: get: summary: Get the account balances diff --git a/client/tx/decode.go b/client/tx/decode.go new file mode 100644 index 000000000000..dbfc41ee5946 --- /dev/null +++ b/client/tx/decode.go @@ -0,0 +1,102 @@ +package tx + +import ( + "encoding/base64" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/spf13/cobra" + "github.com/tendermint/go-amino" + "io/ioutil" + "net/http" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/x/auth" +) + +type ( + // DecodeReq defines a tx decoding request. + DecodeReq struct { + Tx string `json:"tx"` + } + + // DecodeResp defines a tx decoding response. + DecodeResp auth.StdTx +) + +// DecodeTxRequestHandlerFn returns the decode tx REST handler. In particular, +// it takes base64-decoded bytes, decodes it from the Amino wire protocol, +// and responds with a json-formatted transaction. +func DecodeTxRequestHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req DecodeReq + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + err = cdc.UnmarshalJSON(body, &req) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + txBytes, err := base64.StdEncoding.DecodeString(req.Tx) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + var stdTx auth.StdTx + err = cliCtx.Codec.UnmarshalBinaryLengthPrefixed(txBytes, &stdTx) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + response := DecodeResp(stdTx) + rest.PostProcessResponse(w, cdc, response, cliCtx.Indent) + } +} + +// txDecodeRespStr implements a simple Stringer wrapper for a decoded tx. +type txDecodeRespTx auth.StdTx + +func (tx txDecodeRespTx) String() string { + return tx.String() +} + +// GetDecodeCommand returns the decode command to take Amino-serialized bytes and turn it into +// a JSONified transaction +func GetDecodeCommand(codec *amino.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "decode [amino-byte-string]", + Short: "Decode an amino-encoded transaction string", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + cliCtx := context.NewCLIContext().WithCodec(codec) + + txBytesBase64 := args[0] + + txBytes, err := base64.StdEncoding.DecodeString(txBytesBase64) + if err != nil { + return err + } + + var stdTx auth.StdTx + err = cliCtx.Codec.UnmarshalBinaryLengthPrefixed(txBytes, &stdTx) + if err != nil { + return err + } + + response := txDecodeRespTx(stdTx) + _ = cliCtx.PrintOutput(response) + + return nil + }, + } + + return client.PostCommands(cmd)[0] +} diff --git a/client/tx/root.go b/client/tx/root.go index cb2c6460917e..84d10c0a945f 100644 --- a/client/tx/root.go +++ b/client/tx/root.go @@ -13,4 +13,5 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec) r.HandleFunc("/txs", QueryTxsByTagsRequestHandlerFn(cliCtx, cdc)).Methods("GET") r.HandleFunc("/txs", BroadcastTxRequest(cliCtx, cdc)).Methods("POST") r.HandleFunc("/txs/encode", EncodeTxRequestHandlerFn(cdc, cliCtx)).Methods("POST") + r.HandleFunc("/txs/decode", DecodeTxRequestHandlerFn(cdc, cliCtx)).Methods("POST") } diff --git a/client/utils/utils.go b/client/utils/utils.go index 25d81c4518e4..16c00113a15c 100644 --- a/client/utils/utils.go +++ b/client/utils/utils.go @@ -158,9 +158,14 @@ func PrintUnsignedStdTx( return } - json, err := cliCtx.Codec.MarshalJSON(stdTx) + var json []byte + if viper.GetBool(client.FlagIndentResponse) { + json, err = cliCtx.Codec.MarshalJSONIndent(stdTx, "", " ") + } else { + json, err = cliCtx.Codec.MarshalJSON(stdTx) + } if err == nil { - fmt.Fprintf(cliCtx.Output, "%s\n", json) + _, _ = fmt.Fprintf(cliCtx.Output, "%s\n", json) } return diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index 2cb0d2223918..3d12c115b51e 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -875,6 +875,45 @@ func TestGaiaCLIEncode(t *testing.T) { require.Equal(t, "deadbeef", decodedTx.Memo) } +func TestGaiaCLIDecode(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + + // start gaiad server + proc := f.GDStart() + defer proc.Stop(false) + + // Build a testing transaction and write it to disk + barAddr := f.KeyAddress(keyBar) + keyAddr := f.KeyAddress(keyFoo) + + sendTokens := sdk.TokensFromTendermintPower(10) + success, stdout, stderr := f.TxSend(keyAddr.String(), barAddr, sdk.NewCoin(denom, sendTokens), "--generate-only", "--memo", "deadbeef") + require.True(t, success) + require.Empty(t, stderr) + + // Write it to disk + jsonTxFile := WriteToNewTempFile(t, stdout) + defer os.Remove(jsonTxFile.Name()) + + // Run the encode command, and trim the extras from the stdout capture + success, base64Encoded, _ := f.TxEncode(jsonTxFile.Name()) + require.True(t, success) + trimmedBase64 := strings.Trim(base64Encoded, "\"\n") + + // Run the decode command + success, stdout, stderr = f.TxDecode(trimmedBase64) + require.True(t, success) + require.Empty(t, stderr) + + // Check that the transaction decodes as epxceted + var stdTx auth.StdTx + cdc := app.MakeCodec() + aminoJson := "{\"type\": \"auth/StdTx\",\"value\": " + stdout + "}" + require.Nil(t, cdc.UnmarshalJSON([]byte(aminoJson), &stdTx)) + require.Equal(t, "deadbeef", stdTx.Memo) +} + func TestGaiaCLIMultisignSortSignatures(t *testing.T) { t.Parallel() f := InitFixtures(t) diff --git a/cmd/gaia/cli_test/test_helpers.go b/cmd/gaia/cli_test/test_helpers.go index 1df5b93c9eb8..fa6f45f370d1 100644 --- a/cmd/gaia/cli_test/test_helpers.go +++ b/cmd/gaia/cli_test/test_helpers.go @@ -337,6 +337,12 @@ func (f *Fixtures) TxEncode(fileName string, flags ...string) (bool, string, str return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), client.DefaultKeyPass) } +// TxDecode is gaiacli tx decode +func (f *Fixtures) TxDecode(tx string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("%s tx decode %s %v", f.GaiacliBinary, tx, f.Flags()) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), client.DefaultKeyPass) +} + // TxMultisign is gaiacli tx multisign func (f *Fixtures) TxMultisign(fileName, name string, signaturesFiles []string, flags ...string) (bool, string, string) { diff --git a/cmd/gaia/cmd/gaiacli/main.go b/cmd/gaia/cmd/gaiacli/main.go index 4fd6d1c3ad75..718ed1a242f7 100644 --- a/cmd/gaia/cmd/gaiacli/main.go +++ b/cmd/gaia/cmd/gaiacli/main.go @@ -153,6 +153,7 @@ func txCmd(cdc *amino.Codec, mc []sdk.ModuleClients) *cobra.Command { authcmd.GetMultiSignCommand(cdc), tx.GetBroadcastCommand(cdc), tx.GetEncodeCommand(cdc), + tx.GetDecodeCommand(cdc), client.LineBreak, )