From e6a8a4df559c9474e753eab19b54a0354e60b059 Mon Sep 17 00:00:00 2001 From: Alessio Treglia Date: Fri, 7 Sep 2018 11:59:16 +0100 Subject: [PATCH] Implement command/REST endpoint for offline tx sign off #1953 * Add sign CLI command to sign transactions generated with the --generate-only flag. * Add /sign REST endpoint for Voyager support. Redirect password prompt to STDERR to avoid messing up cli commands output. As a rule of thumb, program's output should always go to STDOUT, whilst errors&diagnostics go to STDERR as per POSIX's philosophy and specs. --- Makefile | 2 +- PENDING.md | 7 ++- client/input.go | 2 +- client/lcd/lcd_test.go | 30 ++++++++++- client/utils/utils.go | 53 +++++++++++++++++++ cmd/gaia/cli_test/cli_test.go | 42 ++++++++++++++- cmd/gaia/cmd/gaiacli/main.go | 1 + docs/light/api.md | 69 ++++++++++++++++++++++++ docs/sdk/clients.md | 11 +++- x/auth/client/cli/sign.go | 91 ++++++++++++++++++++++++++++++++ x/auth/client/context/context.go | 60 +++++++++++++++------ x/auth/client/rest/query.go | 4 ++ x/auth/client/rest/sign.go | 60 +++++++++++++++++++++ 13 files changed, 409 insertions(+), 23 deletions(-) create mode 100644 x/auth/client/cli/sign.go create mode 100644 x/auth/client/rest/sign.go diff --git a/Makefile b/Makefile index 2f23fd54fe2b..78fc864c9e01 100644 --- a/Makefile +++ b/Makefile @@ -179,7 +179,7 @@ test_cover: test_lint: gometalinter.v2 --config=tools/gometalinter.json ./... - !(gometalinter.v2 --disable-all --enable='errcheck' --vendor ./... | grep -v "client/") + !(gometalinter.v2 --disable-all --enable='errcheck' --vendor ./... | grep -v -e "client/" -e "fmt\.Fprintf") find . -name '*.go' -type f -not -path "./vendor*" -not -path "*.git*" | xargs gofmt -d -s dep status >> /dev/null !(grep -n branch Gopkg.toml) diff --git a/PENDING.md b/PENDING.md index d23a372a30e9..bfcbcd978130 100644 --- a/PENDING.md +++ b/PENDING.md @@ -29,7 +29,7 @@ BREAKING CHANGES * A new bech32 prefix has been introduced for Tendermint signing keys and addresses, `cosmosconspub` and `cosmoscons` respectively. * [x/gov] \#2195 Made governance use BFT Time instead of Block Heights for deposit and voting periods. - + * SDK * [core] [\#1807](https://github.com/cosmos/cosmos-sdk/issues/1807) Switch from use of rational to decimal * [types] [\#1901](https://github.com/cosmos/cosmos-sdk/issues/1901) Validator interface's GetOwner() renamed to GetOperator() @@ -48,6 +48,7 @@ FEATURES * [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 CLI (`gaiacli`) * [cli] Cmds to query staking pool and params @@ -57,7 +58,9 @@ FEATURES * [cli] [\#2047](https://github.com/cosmos/cosmos-sdk/issues/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](https://github.com/cosmos/cosmos-sdk/issues/2047) The --gas-adjustment flag can be used to adjust the estimate obtained via the simulation triggered by --gas=0. * [cli] [\#2110](https://github.com/cosmos/cosmos-sdk/issues/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](https://github.com/cosmos/cosmos-sdk/issues/966) Add --generate-only flag to build an unsigned transaction and write it to STDOUT. + * [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. * Gaia * [cli] #2170 added ability to show the node's address via `gaiad tendermint show-address` diff --git a/client/input.go b/client/input.go index e7d13f3bfdbe..b10f65ce62d3 100644 --- a/client/input.go +++ b/client/input.go @@ -24,7 +24,7 @@ func BufferStdin() *bufio.Reader { // It enforces the password length func GetPassword(prompt string, buf *bufio.Reader) (pass string, err error) { if inputIsTty() { - pass, err = speakeasy.Ask(prompt) + pass, err = speakeasy.FAsk(os.Stderr, prompt) } else { pass, err = readLineFromBuf(buf) } diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 656362bcd63f..8b4e31868481 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -26,6 +26,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/auth" + authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/slashing" "github.com/cosmos/cosmos-sdk/x/stake" @@ -313,12 +314,13 @@ func TestIBCTransfer(t *testing.T) { // TODO: query ibc egress packet state } -func TestCoinSendGenerateOnly(t *testing.T) { +func TestCoinSendGenerateAndSign(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 + + // generate 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 @@ -327,6 +329,30 @@ func TestCoinSendGenerateOnly(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)) + + // sign tx + var signedMsg auth.StdTx + acc := getAccount(t, port, addr) + accnum := acc.GetAccountNumber() + sequence := acc.GetSequence() + + payload := authrest.SignBody{ + Tx: msg, + LocalAccountName: name, + Password: password, + ChainID: viper.GetString(client.FlagChainID), + AccountNumber: accnum, + Sequence: sequence, + } + json, err := cdc.MarshalJSON(payload) + require.Nil(t, err) + res, body = Request(t, port, "POST", "/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)) } func TestTxs(t *testing.T) { diff --git a/client/utils/utils.go b/client/utils/utils.go index fa5bcf817d6b..554406b4aa4f 100644 --- a/client/utils/utils.go +++ b/client/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "bytes" "fmt" "os" @@ -99,6 +100,49 @@ func PrintUnsignedStdTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs return } +// SignStdTx appends a signature to a StdTx and returns a copy of a it. If appendSig +// is false, it replaces the signatures already attached with the new signature. +func SignStdTx(txCtx authctx.TxContext, cliCtx context.CLIContext, name string, stdTx auth.StdTx, appendSig bool) (auth.StdTx, error) { + var signedStdTx auth.StdTx + + keybase, err := keys.GetKeyBase() + if err != nil { + return signedStdTx, err + } + info, err := keybase.Get(name) + if err != nil { + return signedStdTx, err + } + addr := info.GetPubKey().Address() + + // Check whether the address is a signer + if !isTxSigner(sdk.AccAddress(addr), stdTx.GetSigners()) { + fmt.Fprintf(os.Stderr, "WARNING: The generated transaction's intended signer does not match the given signer: '%v'", name) + } + + if txCtx.AccountNumber == 0 { + accNum, err := cliCtx.GetAccountNumber(addr) + if err != nil { + return signedStdTx, err + } + txCtx = txCtx.WithAccountNumber(accNum) + } + + if txCtx.Sequence == 0 { + accSeq, err := cliCtx.GetAccountSequence(addr) + if err != nil { + return signedStdTx, err + } + txCtx = txCtx.WithSequence(accSeq) + } + + passphrase, err := keys.GetPassphrase(name) + if err != nil { + return signedStdTx, err + } + return txCtx.SignStdTx(name, passphrase, stdTx, appendSig) +} + func adjustGasEstimate(estimate int64, adjustment float64) int64 { return int64(adjustment * float64(estimate)) } @@ -163,3 +207,12 @@ func buildUnsignedStdTx(txCtx authctx.TxContext, cliCtx context.CLIContext, msgs } return auth.NewStdTx(stdSignMsg.Msgs, stdSignMsg.Fee, nil, stdSignMsg.Memo), nil } + +func isTxSigner(user sdk.AccAddress, signers []sdk.AccAddress) bool { + for _, s := range signers { + if bytes.Equal(user.Bytes(), s.Bytes()) { + return true + } + } + return false +} diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index 8b66ab4a1db0..e67ac3082290 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -5,6 +5,7 @@ package clitest import ( "encoding/json" "fmt" + "io/ioutil" "os" "testing" @@ -332,7 +333,7 @@ func TestGaiaCLISubmitProposal(t *testing.T) { require.Equal(t, " 2 - Apples", proposalsQuery) } -func TestGaiaCLISendGenerateOnly(t *testing.T) { +func TestGaiaCLISendGenerateAndSign(t *testing.T) { chainID, servAddr, port := initializeFixtures(t) flags := fmt.Sprintf("--home=%s --node=%v --chain-id=%v", gaiacliHome, servAddr, chainID) @@ -343,6 +344,7 @@ func TestGaiaCLISendGenerateOnly(t *testing.T) { tests.WaitForTMStart(port) tests.WaitForNextNBlocksTM(2, port) + fooAddr, _ := executeGetAddrPK(t, fmt.Sprintf("gaiacli keys show foo --output=json --home=%s", gaiacliHome)) barAddr, _ := executeGetAddrPK(t, fmt.Sprintf("gaiacli keys show bar --output=json --home=%s", gaiacliHome)) // Test generate sendTx with default gas @@ -376,6 +378,35 @@ func TestGaiaCLISendGenerateOnly(t *testing.T) { require.Equal(t, msg.Fee.Gas, int64(100)) require.Equal(t, len(msg.Msgs), 1) require.Equal(t, 0, len(msg.GetSignatures())) + + // Write the output to disk + unsignedTxFile := writeToNewTempFile(t, stdout) + defer os.Remove(unsignedTxFile.Name()) + + // Test sign --print-sigs + success, stdout, _ = executeWriteRetStdStreams(t, fmt.Sprintf( + "gaiacli sign %v --print-sigs %v", flags, unsignedTxFile.Name())) + require.True(t, success) + require.Equal(t, fmt.Sprintf("Signers:\n 0: %v\n\nSignatures:\n", fooAddr.String()), stdout) + + // Test sign + success, stdout, _ = executeWriteRetStdStreams(t, fmt.Sprintf( + "gaiacli sign %v --name=foo %v", flags, unsignedTxFile.Name()), app.DefaultKeyPass) + require.True(t, success) + msg = unmarshalStdTx(t, stdout) + require.Equal(t, len(msg.Msgs), 1) + require.Equal(t, 1, len(msg.GetSignatures())) + require.Equal(t, fooAddr.String(), msg.GetSigners()[0].String()) + + // Write the output to disk + signedTxFile := writeToNewTempFile(t, stdout) + defer os.Remove(signedTxFile.Name()) + + // Test sign --print-signatures + success, stdout, _ = executeWriteRetStdStreams(t, fmt.Sprintf( + "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) } //___________________________________________________________________________________ @@ -408,6 +439,15 @@ func unmarshalStdTx(t *testing.T, s string) (stdTx auth.StdTx) { return } +func writeToNewTempFile(t *testing.T, s string) *os.File { + fp, err := ioutil.TempFile(os.TempDir(), "cosmos_cli_test_") + require.Nil(t, err) + // defer os.Remove(signedTxFile.Name()) + _, err = fp.WriteString(s) + require.Nil(t, err) + return fp +} + //___________________________________________________________________________________ // executors diff --git a/cmd/gaia/cmd/gaiacli/main.go b/cmd/gaia/cmd/gaiacli/main.go index df0fd3c1138e..7a1af622145d 100644 --- a/cmd/gaia/cmd/gaiacli/main.go +++ b/cmd/gaia/cmd/gaiacli/main.go @@ -127,6 +127,7 @@ func main() { rootCmd.AddCommand( client.GetCommands( authcmd.GetAccountCmd("acc", cdc, authcmd.GetAccountDecoder(cdc)), + authcmd.GetSignCommand(cdc, authcmd.GetAccountDecoder(cdc)), )...) rootCmd.AddCommand( client.PostCommands( diff --git a/docs/light/api.md b/docs/light/api.md index 7168cf9d743d..293b023cd063 100644 --- a/docs/light/api.md +++ b/docs/light/api.md @@ -229,6 +229,75 @@ Returns on success: } ``` +### POST /auth/accounts/sign + +- **URL**: `/auth/sign` +- **Functionality**: Sign a transaction without broadcasting it. +- Returns on success: + +```json +{ + "rest api": "1.0", + "code": 200, + "error": "", + "result": { + "type": "auth/StdTx", + "value": { + "msg": [ + { + "type": "cosmos-sdk/Send", + "value": { + "inputs": [ + { + "address": "cosmos1ql4ekxkujf3xllk8h5ldhhgh4ylpu7kwec6q3d", + "coins": [ + { + "denom": "steak", + "amount": "1" + } + ] + } + ], + "outputs": [ + { + "address": "cosmos1dhyqhg4px33ed3erqymls0hc7q2lxw9hhfwklj", + "coins": [ + { + "denom": "steak", + "amount": "1" + } + ] + } + ] + } + } + ], + "fee": { + "amount": [ + { + "denom": "", + "amount": "0" + } + ], + "gas": "2742" + }, + "signatures": [ + { + "pub_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "A2A/f2IYnrPUMTMqhwN81oas9jurtfcsvxdeLlNw3gGy" + }, + "signature": "MEQCIGVn73y9QLwBa3vmsAD1bs3ygX75Wo+lAFSAUDs431ZPAiBWAf2amyqTCDXE9J87rL9QF9sd5JvVMt7goGSuamPJwg==", + "account_number": "1", + "sequence": "0" + } + ], + "memo": "" + } + } +} +``` + ## 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 fdfbca7bd2fe..0cc87cf21500 100644 --- a/docs/sdk/clients.md +++ b/docs/sdk/clients.md @@ -147,7 +147,16 @@ gaiacli send \ --chain-id= \ --name= \ --to= \ - --generate-only + --generate-only > unsignedSendTx.json +``` + +You can now sign the transaction file generated through the `--generate-only` flag by providing your key to the following command: + +```bash +gaiacli sign \ + --chain-id= \ + --name= + unsignedSendTx.json > signedSendTx.json ``` ### Staking diff --git a/x/auth/client/cli/sign.go b/x/auth/client/cli/sign.go new file mode 100644 index 000000000000..e5cd4123564f --- /dev/null +++ b/x/auth/client/cli/sign.go @@ -0,0 +1,91 @@ +package cli + +import ( + "fmt" + "io/ioutil" + + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/utils" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + authctx "github.com/cosmos/cosmos-sdk/x/auth/client/context" + "github.com/spf13/cobra" + amino "github.com/tendermint/go-amino" +) + +const ( + flagAppend = "append" + flagPrintSigs = "print-sigs" +) + +// GetSignCommand returns the sign command +func GetSignCommand(codec *amino.Codec, decoder auth.AccountDecoder) *cobra.Command { + cmd := &cobra.Command{ + Use: "sign ", + Short: "Sign transactions", + Long: `Sign transactions created with the --generate-only flag. +Read a transaction from , sign it, and print its JSON encoding.`, + RunE: makeSignCmd(codec, decoder), + Args: cobra.ExactArgs(1), + } + cmd.Flags().String(client.FlagName, "", "Name of private key with which to sign") + cmd.Flags().Bool(flagAppend, true, "Append the signature to the existing ones. If disabled, old signatures would be overwritten") + cmd.Flags().Bool(flagPrintSigs, false, "Print the addresses that must sign the transaction and those who have already signed it, then exit") + return cmd +} + +func makeSignCmd(cdc *amino.Codec, decoder auth.AccountDecoder) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) (err error) { + stdTx, err := readAndUnmarshalStdTx(cdc, args[0]) + if err != nil { + return + } + + if viper.GetBool(flagPrintSigs) { + printSignatures(stdTx) + return nil + } + + name := viper.GetString(client.FlagName) + cliCtx := context.NewCLIContext().WithCodec(cdc).WithAccountDecoder(decoder) + txCtx := authctx.NewTxContextFromCLI() + + newTx, err := utils.SignStdTx(txCtx, cliCtx, name, stdTx, viper.GetBool(flagAppend)) + if err != nil { + return err + } + json, err := cdc.MarshalJSON(newTx) + if err != nil { + return err + } + fmt.Printf("%s\n", json) + return + } +} + +func printSignatures(stdTx auth.StdTx) { + fmt.Println("Signers:") + for i, signer := range stdTx.GetSigners() { + fmt.Printf(" %v: %v\n", i, signer.String()) + } + fmt.Println("") + fmt.Println("Signatures:") + for i, sig := range stdTx.GetSignatures() { + fmt.Printf(" %v: %v\n", i, sdk.AccAddress(sig.Address()).String()) + } + return +} + +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/auth/client/context/context.go b/x/auth/client/context/context.go index 5e55696b83e5..fe030449b291 100644 --- a/x/auth/client/context/context.go +++ b/x/auth/client/context/context.go @@ -117,24 +117,11 @@ func (ctx TxContext) Build(msgs []sdk.Msg) (auth.StdSignMsg, error) { // Sign signs a transaction given a name, passphrase, and a single message to // signed. An error is returned if signing fails. func (ctx TxContext) Sign(name, passphrase string, msg auth.StdSignMsg) ([]byte, error) { - keybase, err := keys.GetKeyBase() - if err != nil { - return nil, err - } - - sig, pubkey, err := keybase.Sign(name, passphrase, msg.Bytes()) + sig, err := MakeSignature(name, passphrase, msg) if err != nil { return nil, err } - - sigs := []auth.StdSignature{{ - AccountNumber: msg.AccountNumber, - Sequence: msg.Sequence, - PubKey: pubkey, - Signature: sig, - }} - - return ctx.Codec.MarshalBinary(auth.NewStdTx(msg.Msgs, msg.Fee, sigs, msg.Memo)) + return ctx.Codec.MarshalBinary(auth.NewStdTx(msg.Msgs, msg.Fee, []auth.StdSignature{sig}, msg.Memo)) } // BuildAndSign builds a single message to be signed, and signs a transaction @@ -177,3 +164,46 @@ func (ctx TxContext) BuildWithPubKey(name string, msgs []sdk.Msg) ([]byte, error return ctx.Codec.MarshalBinary(auth.NewStdTx(msg.Msgs, msg.Fee, sigs, msg.Memo)) } + +// SignStdTx appends a signature to a StdTx and returns a copy of a it. If append +// is false, it replaces the signatures already attached with the new signature. +func (ctx TxContext) SignStdTx(name, passphrase string, stdTx auth.StdTx, appendSig bool) (signedStdTx auth.StdTx, err error) { + stdSignature, err := MakeSignature(name, passphrase, auth.StdSignMsg{ + ChainID: ctx.ChainID, + AccountNumber: ctx.AccountNumber, + Sequence: ctx.Sequence, + Fee: stdTx.Fee, + Msgs: stdTx.GetMsgs(), + Memo: stdTx.GetMemo(), + }) + if err != nil { + return + } + + sigs := stdTx.GetSignatures() + if len(sigs) == 0 || !appendSig { + sigs = []auth.StdSignature{stdSignature} + } else { + sigs = append(sigs, stdSignature) + } + signedStdTx = auth.NewStdTx(stdTx.GetMsgs(), stdTx.Fee, sigs, stdTx.GetMemo()) + return +} + +// MakeSignature builds a StdSignature given key name, passphrase, and a StdSignMsg. +func MakeSignature(name, passphrase string, msg auth.StdSignMsg) (sig auth.StdSignature, err error) { + keybase, err := keys.GetKeyBase() + if err != nil { + return + } + sigBytes, pubkey, err := keybase.Sign(name, passphrase, msg.Bytes()) + if err != nil { + return + } + return auth.StdSignature{ + AccountNumber: msg.AccountNumber, + Sequence: msg.Sequence, + PubKey: pubkey, + Signature: sigBytes, + }, nil +} diff --git a/x/auth/client/rest/query.go b/x/auth/client/rest/query.go index 6ad50a14dda4..3a5ab756d16e 100644 --- a/x/auth/client/rest/query.go +++ b/x/auth/client/rest/query.go @@ -20,6 +20,10 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *wire.Codec, s "/accounts/{address}", QueryAccountRequestHandlerFn(storeName, cdc, authcmd.GetAccountDecoder(cdc), cliCtx), ).Methods("GET") + r.HandleFunc( + "/sign", + SignTxRequestHandlerFn(cdc, cliCtx), + ).Methods("POST") } // query accountREST Handler diff --git a/x/auth/client/rest/sign.go b/x/auth/client/rest/sign.go new file mode 100644 index 000000000000..bbbf7f26ce45 --- /dev/null +++ b/x/auth/client/rest/sign.go @@ -0,0 +1,60 @@ +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" + authctx "github.com/cosmos/cosmos-sdk/x/auth/client/context" +) + +// SignBody defines the properties of a sign request's body. +type SignBody struct { + Tx auth.StdTx `json:"tx"` + LocalAccountName string `json:"name"` + Password string `json:"password"` + ChainID string `json:"chain_id"` + AccountNumber int64 `json:"account_number"` + Sequence int64 `json:"sequence"` +} + +// sign tx REST handler +func SignTxRequestHandlerFn(cdc *wire.Codec, cliCtx context.CLIContext) http.HandlerFunc { + + return func(w http.ResponseWriter, r *http.Request) { + var m SignBody + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + err = cdc.UnmarshalJSON(body, &m) + if err != nil { + utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + txCtx := authctx.TxContext{ + ChainID: m.ChainID, + AccountNumber: m.AccountNumber, + Sequence: m.Sequence, + } + + signedTx, err := txCtx.SignStdTx(m.LocalAccountName, m.Password, m.Tx, false) + if err != nil { + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + output, err := wire.MarshalJSONIndent(cdc, signedTx) + if err != nil { + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + w.Write(output) + } +}