From 6791329bdfd761a5741f8e7feabd6550a9e2b67e Mon Sep 17 00:00:00 2001 From: Ducem Barr Date: Wed, 6 Feb 2019 15:17:24 -0500 Subject: [PATCH] Add tx/encode endpoint and CLI command --- PENDING.md | 4 +- client/lcd/lcd_test.go | 41 +++++++++++++ client/lcd/swagger-ui/swagger.yaml | 33 +++++++++++ cmd/gaia/cli_test/cli_test.go | 71 ++++++++++++++++++----- cmd/gaia/cli_test/test_helpers.go | 11 +++- cmd/gaia/cmd/gaiacli/main.go | 3 +- x/{bank => auth}/client/cli/broadcast.go | 22 +------ x/auth/client/cli/encode.go | 55 ++++++++++++++++++ x/auth/client/cli/multisign.go | 3 +- x/auth/client/cli/sign.go | 15 +---- x/{bank => auth}/client/rest/broadcast.go | 2 +- x/auth/client/rest/encode.go | 46 +++++++++++++++ x/auth/client/rest/query.go | 8 +++ x/auth/client/util.go | 27 +++++++++ x/auth/client/util_test.go | 32 ++++++++++ x/bank/client/rest/sendtx.go | 1 - 16 files changed, 318 insertions(+), 56 deletions(-) rename x/{bank => auth}/client/cli/broadcast.go (71%) create mode 100644 x/auth/client/cli/encode.go rename x/{bank => auth}/client/rest/broadcast.go (95%) create mode 100644 x/auth/client/rest/encode.go create mode 100644 x/auth/client/util.go create mode 100644 x/auth/client/util_test.go diff --git a/PENDING.md b/PENDING.md index 687030169124..381fa0e987d6 100644 --- a/PENDING.md +++ b/PENDING.md @@ -25,7 +25,7 @@ BREAKING CHANGES * Reintroduce OR semantics for tx fees * SDK - * \#2513 Tendermint updates are adjusted by 10^-6 relative to staking tokens, + * \#2513 Tendermint updates are adjusted by 10^-6 relative to staking tokens, * [\#3487](https://github.com/cosmos/cosmos-sdk/pull/3487) Move HTTP/REST utilities out of client/utils into a new dedicated client/rest package. * [\#3490](https://github.com/cosmos/cosmos-sdk/issues/3490) ReadRESTReq() returns bool to avoid callers to write error responses twice. * [\#3502](https://github.com/cosmos/cosmos-sdk/pull/3502) Fixes issue when comparing genesis states @@ -71,6 +71,7 @@ IMPROVEMENTS * [\#3423](https://github.com/cosmos/cosmos-sdk/issues/3423) Allow simulation (auto gas) to work with generate only. * [\#3514](https://github.com/cosmos/cosmos-sdk/pull/3514) REST server calls to keybase does not lock the underlying storage anymore. + * [\#3523](https://github.com/cosmos/cosmos-sdk/pull/3523) Added `/tx/encode` endpoint to serialize a JSON tx to base64-encoded Amino. * Gaia CLI (`gaiacli`) * [\#3476](https://github.com/cosmos/cosmos-sdk/issues/3476) New `withdraw-all-rewards` command to withdraw all delegations rewards for delegators. @@ -78,6 +79,7 @@ IMPROVEMENTS * [\#3518](https://github.com/cosmos/cosmos-sdk/issues/3518) Fix flow in `keys add` to show the mnemonic by default. * [\#3517](https://github.com/cosmos/cosmos-sdk/pull/3517) Increased test coverage + * [\#3523](https://github.com/cosmos/cosmos-sdk/pull/3523) Added `tx encode` command to serialize a JSON tx to base64-encoded Amino. * Gaia * [\#3418](https://github.com/cosmos/cosmos-sdk/issues/3418) Add vesting account diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 7648d86a3012..fff8808dd741 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -1,6 +1,7 @@ package lcd import ( + "encoding/base64" "encoding/hex" "fmt" "net/http" @@ -423,6 +424,46 @@ func TestCoinSendGenerateSignAndBroadcast(t *testing.T) { require.Equal(t, gasEstimate, resultTx.GasWanted) } +func TestEncodeTx(t *testing.T) { + // Setup + 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() + + // Make a transaction to test with + res, body, _ := doTransferWithGas(t, port, seed, name1, memo, "", addr, "2", 1, false, true, fees) + var tx auth.StdTx + cdc.UnmarshalJSON([]byte(body), &tx) + + // Build the request + encodeReq := struct { + Tx auth.StdTx `json:"tx"` + }{Tx: tx} + encodedJSON, _ := cdc.MarshalJSON(encodeReq) + res, body = Request(t, port, "POST", "/tx/encode", encodedJSON) + + // 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) + encodeResp := struct { + Tx string `json:"tx"` + }{} + + // No error decoding the JSON + require.Nil(t, cdc.UnmarshalJSON([]byte(body), &encodeResp)) + + // Check that the base64 decodes + decodedBytes, err := base64.StdEncoding.DecodeString(encodeResp.Tx) + require.Nil(t, err) + + // Check that the transaction decodes as expected + var decodedTx auth.StdTx + require.Nil(t, cdc.UnmarshalBinaryLengthPrefixed(decodedBytes, &decodedTx)) + require.Equal(t, memo, decodedTx.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 38e9c1864399..82a6af083583 100644 --- a/client/lcd/swagger-ui/swagger.yaml +++ b/client/lcd/swagger-ui/swagger.yaml @@ -334,6 +334,39 @@ paths: description: The Tx was malformated 500: description: Server internal error + /tx/encode: + post: + tags: + - ICS20 + summary: Encode a transaction to wire format + description: Encode a transaction (signed or not) from JSON to base64-encoded Amino serialized bytes + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: tx + description: The transaction to encode + required: true + schema: + type: object + properties: + tx: + $ref: "#/definitions/StdTx" + responses: + 200: + description: Transaction was successfully decoded and re-encoded + schema: + type: object + properties: + tx: + type: string + example: The base64-encoded Amino-serialized bytes for the transaction + 400: + description: The Tx was malformated + 500: + description: Server internal error /bank/balances/{address}: get: summary: Get the account balances diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index fcea83893751..5df719f818ab 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -1,12 +1,14 @@ package clitest import ( + "encoding/base64" "errors" "fmt" "io/ioutil" "os" "path" "path/filepath" + "strings" "testing" "time" @@ -18,6 +20,7 @@ import ( "github.com/cosmos/cosmos-sdk/cmd/gaia/app" "github.com/cosmos/cosmos-sdk/tests" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/staking" ) @@ -587,7 +590,7 @@ func TestGaiaCLIValidateSignatures(t *testing.T) { require.Empty(t, stderr) // write unsigned tx to file - unsignedTxFile := writeToNewTempFile(t, stdout) + unsignedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(unsignedTxFile.Name()) // validate we can successfully sign @@ -600,7 +603,7 @@ func TestGaiaCLIValidateSignatures(t *testing.T) { require.Equal(t, fooAddr.String(), stdTx.GetSigners()[0].String()) // write signed tx to file - signedTxFile := writeToNewTempFile(t, stdout) + signedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(signedTxFile.Name()) // validate signatures @@ -610,7 +613,7 @@ func TestGaiaCLIValidateSignatures(t *testing.T) { // modify the transaction stdTx.Memo = "MODIFIED-ORIGINAL-TX-BAD" bz := marshalStdTx(t, stdTx) - modSignedTxFile := writeToNewTempFile(t, string(bz)) + modSignedTxFile := WriteToNewTempFile(t, string(bz)) defer os.Remove(modSignedTxFile.Name()) // validate signature validation failure due to different transaction sig bytes @@ -659,7 +662,7 @@ func TestGaiaCLISendGenerateSignAndBroadcast(t *testing.T) { require.Equal(t, len(msg.Msgs), 1) // Write the output to disk - unsignedTxFile := writeToNewTempFile(t, stdout) + unsignedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(unsignedTxFile.Name()) // Test sign --validate-signatures @@ -676,7 +679,7 @@ func TestGaiaCLISendGenerateSignAndBroadcast(t *testing.T) { require.Equal(t, fooAddr.String(), msg.GetSigners()[0].String()) // Write the output to disk - signedTxFile := writeToNewTempFile(t, stdout) + signedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(signedTxFile.Name()) // Test sign --validate-signatures @@ -732,7 +735,7 @@ func TestGaiaCLIMultisignInsufficientCosigners(t *testing.T) { require.True(t, success) // Write the output to disk - unsignedTxFile := writeToNewTempFile(t, stdout) + unsignedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(unsignedTxFile.Name()) // Sign with foo's key @@ -740,7 +743,7 @@ func TestGaiaCLIMultisignInsufficientCosigners(t *testing.T) { require.True(t, success) // Write the output to disk - fooSignatureFile := writeToNewTempFile(t, stdout) + fooSignatureFile := WriteToNewTempFile(t, stdout) defer os.Remove(fooSignatureFile.Name()) // Multisign, not enough signatures @@ -748,7 +751,7 @@ func TestGaiaCLIMultisignInsufficientCosigners(t *testing.T) { require.True(t, success) // Write the output to disk - signedTxFile := writeToNewTempFile(t, stdout) + signedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(signedTxFile.Name()) // Validate the multisignature @@ -760,6 +763,42 @@ func TestGaiaCLIMultisignInsufficientCosigners(t *testing.T) { require.False(t, success) } +func TestGaiaCLIEncode(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + + // start gaiad server + proc := f.GDStart() + defer proc.Stop(false) + + cdc := app.MakeCodec() + + // Build a testing transaction and write it to disk + barAddr := f.KeyAddress(keyBar) + sendTokens := staking.TokensFromTendermintPower(10) + success, stdout, stderr := f.TxSend(keyFoo, 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") + + // Decode the base64 + decodedBytes, err := base64.StdEncoding.DecodeString(trimmedBase64) + require.Nil(t, err) + + // Check that the transaction decodes as epxceted + var decodedTx auth.StdTx + require.Nil(t, cdc.UnmarshalBinaryLengthPrefixed(decodedBytes, &decodedTx)) + require.Equal(t, "deadbeef", decodedTx.Memo) +} + func TestGaiaCLIMultisignSortSignatures(t *testing.T) { t.Parallel() f := InitFixtures(t) @@ -785,7 +824,7 @@ func TestGaiaCLIMultisignSortSignatures(t *testing.T) { require.True(t, success) // Write the output to disk - unsignedTxFile := writeToNewTempFile(t, stdout) + unsignedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(unsignedTxFile.Name()) // Sign with foo's key @@ -793,7 +832,7 @@ func TestGaiaCLIMultisignSortSignatures(t *testing.T) { require.True(t, success) // Write the output to disk - fooSignatureFile := writeToNewTempFile(t, stdout) + fooSignatureFile := WriteToNewTempFile(t, stdout) defer os.Remove(fooSignatureFile.Name()) // Sign with baz's key @@ -801,7 +840,7 @@ func TestGaiaCLIMultisignSortSignatures(t *testing.T) { require.True(t, success) // Write the output to disk - bazSignatureFile := writeToNewTempFile(t, stdout) + bazSignatureFile := WriteToNewTempFile(t, stdout) defer os.Remove(bazSignatureFile.Name()) // Multisign, keys in different order @@ -810,7 +849,7 @@ func TestGaiaCLIMultisignSortSignatures(t *testing.T) { require.True(t, success) // Write the output to disk - signedTxFile := writeToNewTempFile(t, stdout) + signedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(signedTxFile.Name()) // Validate the multisignature @@ -848,7 +887,7 @@ func TestGaiaCLIMultisign(t *testing.T) { require.Empty(t, stderr) // Write the output to disk - unsignedTxFile := writeToNewTempFile(t, stdout) + unsignedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(unsignedTxFile.Name()) // Sign with foo's key @@ -856,7 +895,7 @@ func TestGaiaCLIMultisign(t *testing.T) { require.True(t, success) // Write the output to disk - fooSignatureFile := writeToNewTempFile(t, stdout) + fooSignatureFile := WriteToNewTempFile(t, stdout) defer os.Remove(fooSignatureFile.Name()) // Sign with bar's key @@ -864,7 +903,7 @@ func TestGaiaCLIMultisign(t *testing.T) { require.True(t, success) // Write the output to disk - barSignatureFile := writeToNewTempFile(t, stdout) + barSignatureFile := WriteToNewTempFile(t, stdout) defer os.Remove(barSignatureFile.Name()) // Multisign @@ -873,7 +912,7 @@ func TestGaiaCLIMultisign(t *testing.T) { require.True(t, success) // Write the output to disk - signedTxFile := writeToNewTempFile(t, stdout) + signedTxFile := WriteToNewTempFile(t, stdout) defer os.Remove(signedTxFile.Name()) // Validate the multisignature diff --git a/cmd/gaia/cli_test/test_helpers.go b/cmd/gaia/cli_test/test_helpers.go index aba604ae0872..a123adae13db 100644 --- a/cmd/gaia/cli_test/test_helpers.go +++ b/cmd/gaia/cli_test/test_helpers.go @@ -279,12 +279,18 @@ func (f *Fixtures) TxSign(signer, fileName string, flags ...string) (bool, strin return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), app.DefaultKeyPass) } -// TxBroadcast is gaiacli tx sign +// TxBroadcast is gaiacli tx broadcast func (f *Fixtures) TxBroadcast(fileName string, flags ...string) (bool, string, string) { cmd := fmt.Sprintf("gaiacli tx broadcast %v %v", f.Flags(), fileName) return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), app.DefaultKeyPass) } +// TxEncode is gaiacli tx encode +func (f *Fixtures) TxEncode(fileName string, flags ...string) (bool, string, string) { + cmd := fmt.Sprintf("gaiacli tx encode %v %v", f.Flags(), fileName) + return executeWriteRetStdStreams(f.T, addFlags(cmd, flags), app.DefaultKeyPass) +} + // TxMultisign is gaiacli tx multisign func (f *Fixtures) TxMultisign(fileName, name string, signaturesFiles []string, flags ...string) (bool, string, string) { @@ -625,7 +631,8 @@ func queryTags(tags []string) (out string) { return strings.TrimSuffix(out, "&") } -func writeToNewTempFile(t *testing.T, s string) *os.File { +// Write the given string to a new temporary file +func WriteToNewTempFile(t *testing.T, s string) *os.File { fp, err := ioutil.TempFile(os.TempDir(), "cosmos_cli_test_") require.Nil(t, err) _, err = fp.WriteString(s) diff --git a/cmd/gaia/cmd/gaiacli/main.go b/cmd/gaia/cmd/gaiacli/main.go index f9cb8517342d..a5939116ffd6 100644 --- a/cmd/gaia/cmd/gaiacli/main.go +++ b/cmd/gaia/cmd/gaiacli/main.go @@ -141,7 +141,8 @@ func txCmd(cdc *amino.Codec, mc []sdk.ModuleClients) *cobra.Command { client.LineBreak, authcmd.GetSignCommand(cdc), authcmd.GetMultiSignCommand(cdc), - bankcmd.GetBroadcastCommand(cdc), + authcmd.GetBroadcastCommand(cdc), + authcmd.GetEncodeCommand(cdc), client.LineBreak, ) diff --git a/x/bank/client/cli/broadcast.go b/x/auth/client/cli/broadcast.go similarity index 71% rename from x/bank/client/cli/broadcast.go rename to x/auth/client/cli/broadcast.go index 09ba88d14306..8dc1992b597a 100644 --- a/x/bank/client/cli/broadcast.go +++ b/x/auth/client/cli/broadcast.go @@ -1,8 +1,6 @@ package cli import ( - "io/ioutil" - "os" "strings" "github.com/spf13/cobra" @@ -10,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" - "github.com/cosmos/cosmos-sdk/x/auth" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client" ) // GetSignCommand returns the sign command @@ -27,7 +25,7 @@ $ gaiacli tx broadcast ./mytxn.json Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { cliCtx := context.NewCLIContext().WithCodec(codec) - stdTx, err := readAndUnmarshalStdTx(cliCtx.Codec, args[0]) + stdTx, err := authclient.ReadStdTxFromFile(cliCtx.Codec, args[0]) if err != nil { return } @@ -45,19 +43,3 @@ $ gaiacli tx broadcast ./mytxn.json return client.PostCommands(cmd)[0] } - -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 -} diff --git a/x/auth/client/cli/encode.go b/x/auth/client/cli/encode.go new file mode 100644 index 000000000000..07179155152f --- /dev/null +++ b/x/auth/client/cli/encode.go @@ -0,0 +1,55 @@ +package cli + +import ( + "encoding/base64" + + "github.com/spf13/cobra" + amino "github.com/tendermint/go-amino" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client" +) + +// PrintOutput requires a Stringer, so we wrap string +type encodeResp string + +func (e encodeResp) String() string { + return string(e) +} + +// GetEncodeCommand returns the encode command to take a JSONified transaction and turn it into +// Amino-serialized bytes +func GetEncodeCommand(codec *amino.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "encode [file]", + Short: "encode transactions generated offline", + Long: `Encode transactions created with the --generate-only flag and signed with the sign command. +Read a transaction from , serialize it to the Amino wire protocol, and output it as base64. +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) + + stdTx, err := authclient.ReadStdTxFromFile(cliCtx.Codec, args[0]) + if err != nil { + return + } + + txBytes, err := cliCtx.Codec.MarshalBinaryLengthPrefixed(stdTx) + if err != nil { + return err + } + + // Encode the bytes to base64 + txBytesBase64 := base64.StdEncoding.EncodeToString(txBytes) + + // Write it back + response := encodeResp(txBytesBase64) + cliCtx.PrintOutput(response) + return nil + }, + } + + return client.PostCommands(cmd)[0] +} diff --git a/x/auth/client/cli/multisign.go b/x/auth/client/cli/multisign.go index 5101dda1144b..fdcd1d14247a 100644 --- a/x/auth/client/cli/multisign.go +++ b/x/auth/client/cli/multisign.go @@ -16,6 +16,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/keys" crkeys "github.com/cosmos/cosmos-sdk/crypto/keys" "github.com/cosmos/cosmos-sdk/x/auth" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client" authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" ) @@ -51,7 +52,7 @@ recommended to set such parameters manually. func makeMultiSignCmd(cdc *amino.Codec) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) (err error) { - stdTx, err := readAndUnmarshalStdTx(cdc, args[0]) + stdTx, err := authclient.ReadStdTxFromFile(cdc, args[0]) if err != nil { return } diff --git a/x/auth/client/cli/sign.go b/x/auth/client/cli/sign.go index 171692ba565d..371abd84bfb4 100644 --- a/x/auth/client/cli/sign.go +++ b/x/auth/client/cli/sign.go @@ -3,7 +3,6 @@ package cli import ( "errors" "fmt" - "io/ioutil" "os" "github.com/spf13/cobra" @@ -15,6 +14,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/utils" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client" authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" ) @@ -75,7 +75,7 @@ be generated via the 'multisign' command. func makeSignCmd(cdc *amino.Codec) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) (err error) { - stdTx, err := readAndUnmarshalStdTx(cdc, args[0]) + stdTx, err := authclient.ReadStdTxFromFile(cdc, args[0]) if err != nil { return } @@ -222,14 +222,3 @@ func printAndValidateSigs( fmt.Println("") return success } - -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/auth/client/rest/broadcast.go similarity index 95% rename from x/bank/client/rest/broadcast.go rename to x/auth/client/rest/broadcast.go index 1d40c2820e23..3d923589a212 100644 --- a/x/bank/client/rest/broadcast.go +++ b/x/auth/client/rest/broadcast.go @@ -38,7 +38,7 @@ func BroadcastTxRequestHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) ht } } -func unmarshalBodyOrReturnBadRequest(cliCtx context.CLIContext, w http.ResponseWriter, r *http.Request, m *broadcastBody) bool { +func unmarshalBodyOrReturnBadRequest(cliCtx context.CLIContext, w http.ResponseWriter, r *http.Request, m interface{}) bool { body, err := ioutil.ReadAll(r.Body) if err != nil { rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) diff --git a/x/auth/client/rest/encode.go b/x/auth/client/rest/encode.go new file mode 100644 index 000000000000..bac646038bcc --- /dev/null +++ b/x/auth/client/rest/encode.go @@ -0,0 +1,46 @@ +package rest + +import ( + "encoding/base64" + "net/http" + + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/rest" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/auth" +) + +type encodeReq struct { + Tx auth.StdTx `json:"tx"` +} + +type encodeResp struct { + Tx string `json:"tx"` +} + +// EncodeTxRequestHandlerFn returns the encode tx REST handler. In particular, it takes a +// json-formatted transaction, encodes it to the Amino wire protocol, and responds with +// base64-encoded bytes +func EncodeTxRequestHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var m encodeReq + // Decode the transaction from JSON + if ok := unmarshalBodyOrReturnBadRequest(cliCtx, w, r, &m); !ok { + return + } + + // Re-encode it to the wire protocol + txBytes, err := cliCtx.Codec.MarshalBinaryLengthPrefixed(m.Tx) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + // Encode the bytes to base64 + txBytesBase64 := base64.StdEncoding.EncodeToString(txBytes) + + // Write it back + response := encodeResp{Tx: txBytesBase64} + rest.PostProcessResponse(w, cdc, response, cliCtx.Indent) + } +} diff --git a/x/auth/client/rest/query.go b/x/auth/client/rest/query.go index 50ae9e23418d..97810cecf1eb 100644 --- a/x/auth/client/rest/query.go +++ b/x/auth/client/rest/query.go @@ -22,6 +22,14 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, "/bank/balances/{address}", QueryBalancesRequestHandlerFn(storeName, cdc, context.GetAccountDecoder(cdc), cliCtx), ).Methods("GET") + r.HandleFunc( + "/tx/broadcast", + BroadcastTxRequestHandlerFn(cdc, cliCtx), + ).Methods("POST") + r.HandleFunc( + "/tx/encode", + EncodeTxRequestHandlerFn(cdc, cliCtx), + ).Methods("POST") r.HandleFunc( "/tx/sign", SignTxRequestHandlerFn(cdc, cliCtx), diff --git a/x/auth/client/util.go b/x/auth/client/util.go new file mode 100644 index 000000000000..94eb480d6ab5 --- /dev/null +++ b/x/auth/client/util.go @@ -0,0 +1,27 @@ +package client + +import ( + "io/ioutil" + "os" + + "github.com/tendermint/go-amino" + + "github.com/cosmos/cosmos-sdk/x/auth" +) + +// Read and decode a StdTx from the given filename. Can pass "-" to read from stdin. +func ReadStdTxFromFile(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 +} diff --git a/x/auth/client/util_test.go b/x/auth/client/util_test.go new file mode 100644 index 000000000000..409219cd7269 --- /dev/null +++ b/x/auth/client/util_test.go @@ -0,0 +1,32 @@ +package client + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/cmd/gaia/cli_test" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" +) + +func TestReadStdTxFromFile(t *testing.T) { + cdc := codec.New() + sdk.RegisterCodec(cdc) + + // Build a test transaction + fee := auth.NewStdFee(50000, sdk.Coins{sdk.NewInt64Coin("atom", 150)}) + stdTx := auth.NewStdTx([]sdk.Msg{}, fee, []auth.StdSignature{}, "foomemo") + + // Write it to the file + encodedTx, _ := cdc.MarshalJSON(stdTx) + jsonTxFile := clitest.WriteToNewTempFile(t, string(encodedTx)) + defer os.Remove(jsonTxFile.Name()) + + // Read it back + decodedTx, err := ReadStdTxFromFile(cdc, jsonTxFile.Name()) + require.Nil(t, err) + require.Equal(t, decodedTx.Memo, "foomemo") +} diff --git a/x/bank/client/rest/sendtx.go b/x/bank/client/rest/sendtx.go index 2e7cbbdd972d..e602539dccdc 100644 --- a/x/bank/client/rest/sendtx.go +++ b/x/bank/client/rest/sendtx.go @@ -17,7 +17,6 @@ import ( // RegisterRoutes - Central function to define routes that get registered by the main application func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec, kb keys.Keybase) { r.HandleFunc("/bank/accounts/{address}/transfers", SendRequestHandlerFn(cdc, kb, cliCtx)).Methods("POST") - r.HandleFunc("/tx/broadcast", BroadcastTxRequestHandlerFn(cdc, cliCtx)).Methods("POST") } type sendReq struct {