From 1a45755027c6a523ef7b7e4e1cc891909f05e1b4 Mon Sep 17 00:00:00 2001 From: Emmanuel Odeke Date: Mon, 31 Jul 2017 16:08:19 -0600 Subject: [PATCH] client/rest, modules/coin/rest: moved code around After offline emails and a video call with @ethanfrey, a goal was decided to move things around i.e: - [X] Move /build/send and /query/account to modules/coin/rest Due to that move, there is a lot of overlap between needed code and utils so extracted common code to make https://github.com/tendermint/tmlibs/pull/33 so make sure to pull in that commit into your tmlibs tree. After code review feedback: client/rest, modules/coin/rest: FoutputProof, PrepareSendTx helper * Extract OutputProof to FoutputProof helper that can be used in modules/coin/rest/handlers.go as proofs.FoutputProof * Revert r.HandleFunc("/tx", doPostTx).Methods("POST") which was erraneously deleted * Use function signatures from "tendermint/tmblibs/common" --- .gitignore | 2 +- client/commands/proofs/get.go | 25 +++-- client/rest/cmd.go | 47 ++++++++++ client/rest/handlers.go | 170 ++++++---------------------------- client/rest/types.go | 63 ------------- cmd/baseserver/main.go | 34 +------ glide.lock | 2 +- modules/coin/rest/handlers.go | 123 ++++++++++++++++++++++++ 8 files changed, 219 insertions(+), 247 deletions(-) create mode 100644 client/rest/cmd.go create mode 100644 modules/coin/rest/handlers.go diff --git a/.gitignore b/.gitignore index 057d0464d..cdd5c2e47 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ merkleeyes.db build shunit2 docs/guide/*.sh - +keys/ diff --git a/client/commands/proofs/get.go b/client/commands/proofs/get.go index c3e510b3b..8ad394e3a 100644 --- a/client/commands/proofs/get.go +++ b/client/commands/proofs/get.go @@ -2,6 +2,8 @@ package proofs import ( "fmt" + "io" + "os" "github.com/pkg/errors" "github.com/spf13/viper" @@ -111,15 +113,22 @@ type proof struct { Data interface{} `json:"data"` } -// OutputProof prints the proof to stdout -// reuse this for printing proofs and we should enhance this for text/json, -// better presentation of height -func OutputProof(info interface{}, height uint64) error { - wrap := proof{height, info} - res, err := data.ToJSON(wrap) +// FoutputProof writes the output of wrapping height and info +// in the form {"data": , "height": } +// to the provider io.Writer +func FoutputProof(w io.Writer, v interface{}, height uint64) error { + wrap := &proof{height, v} + blob, err := data.ToJSON(wrap) if err != nil { return err } - fmt.Println(string(res)) - return nil + _, err = fmt.Fprintf(w, "%s\n", blob) + return err +} + +// OutputProof prints the proof to stdout +// reuse this for printing proofs and we should enhance this for text/json, +// better presentation of height +func OutputProof(data interface{}, height uint64) error { + return FoutputProof(os.Stdout, data, height) } diff --git a/client/rest/cmd.go b/client/rest/cmd.go new file mode 100644 index 000000000..721e34afc --- /dev/null +++ b/client/rest/cmd.go @@ -0,0 +1,47 @@ +package rest + +import ( + "fmt" + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + coinrest "github.com/tendermint/basecoin/modules/coin/rest" +) + +var ServeCmd = &cobra.Command{ + Use: "serve", + Short: "Serve the light REST client for tendermint", + Long: "Access basecoin via REST", + RunE: serve, +} + +const envPortFlag = "port" + +func init() { + _ = ServeCmd.PersistentFlags().Int(envPortFlag, 8998, "the port to run the server on") +} + +const defaultAlgo = "ed25519" + +func serve(cmd *cobra.Command, args []string) error { + port := viper.GetInt(envPortFlag) + keysManager := DefaultKeysManager() + router := mux.NewRouter() + ctx := Context{ + Keys: New(keysManager, defaultAlgo), + } + if err := ctx.RegisterHandlers(router); err != nil { + return err + } + if err := coinrest.RegisterHandlers(router); err != nil { + return err + } + + addr := fmt.Sprintf(":%d", port) + log.Printf("Serving on %q", addr) + return http.ListenAndServe(addr, router) +} diff --git a/client/rest/handlers.go b/client/rest/handlers.go index e9c427b0b..22a93cb1a 100644 --- a/client/rest/handlers.go +++ b/client/rest/handlers.go @@ -1,25 +1,15 @@ package rest import ( - "fmt" "net/http" - "strings" "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/tendermint/basecoin" - "github.com/tendermint/basecoin/client/commands" - "github.com/tendermint/basecoin/client/commands/proofs" - "github.com/tendermint/basecoin/modules/auth" - "github.com/tendermint/basecoin/modules/base" - "github.com/tendermint/basecoin/modules/coin" - "github.com/tendermint/basecoin/modules/fee" - "github.com/tendermint/basecoin/modules/nonce" - "github.com/tendermint/basecoin/stack" keysutils "github.com/tendermint/go-crypto/cmd" keys "github.com/tendermint/go-crypto/keys" - lightclient "github.com/tendermint/light-client" + "github.com/tendermint/tmlibs/common" ) type Keys struct { @@ -42,19 +32,19 @@ func (k *Keys) GenerateKey(w http.ResponseWriter, r *http.Request) { ckReq := &CreateKeyRequest{ Algo: k.algo, } - if err := parseRequestJSON(r, ckReq); err != nil { - writeError(w, err) + if err := common.ParseRequestAndValidateJSON(r, ckReq); err != nil { + common.WriteError(w, err) return } key, seed, err := k.manager.Create(ckReq.Name, ckReq.Passphrase, ckReq.Algo) if err != nil { - writeError(w, err) + common.WriteError(w, err) return } res := &CreateKeyResponse{Key: key, Seed: seed} - writeSuccess(w, res) + common.WriteSuccess(w, res) } func (k *Keys) GetKey(w http.ResponseWriter, r *http.Request) { @@ -62,19 +52,19 @@ func (k *Keys) GetKey(w http.ResponseWriter, r *http.Request) { name := query["name"] key, err := k.manager.Get(name) if err != nil { - writeError(w, err) + common.WriteError(w, err) return } - writeSuccess(w, &key) + common.WriteSuccess(w, &key) } func (k *Keys) ListKeys(w http.ResponseWriter, r *http.Request) { keys, err := k.manager.List() if err != nil { - writeError(w, err) + common.WriteError(w, err) return } - writeSuccess(w, keys) + common.WriteSuccess(w, keys) } var ( @@ -83,52 +73,52 @@ var ( func (k *Keys) UpdateKey(w http.ResponseWriter, r *http.Request) { uReq := new(UpdateKeyRequest) - if err := parseRequestJSON(r, uReq); err != nil { - writeError(w, err) + if err := common.ParseRequestAndValidateJSON(r, uReq); err != nil { + common.WriteError(w, err) return } query := mux.Vars(r) name := query["name"] if name != uReq.Name { - writeError(w, errNonMatchingPathAndJSONKeyNames) + common.WriteError(w, errNonMatchingPathAndJSONKeyNames) return } if err := k.manager.Update(uReq.Name, uReq.OldPass, uReq.NewPass); err != nil { - writeError(w, err) + common.WriteError(w, err) return } key, err := k.manager.Get(uReq.Name) if err != nil { - writeError(w, err) + common.WriteError(w, err) return } - writeSuccess(w, &key) + common.WriteSuccess(w, &key) } func (k *Keys) DeleteKey(w http.ResponseWriter, r *http.Request) { dReq := new(DeleteKeyRequest) - if err := parseRequestJSON(r, dReq); err != nil { - writeError(w, err) + if err := common.ParseRequestAndValidateJSON(r, dReq); err != nil { + common.WriteError(w, err) return } query := mux.Vars(r) name := query["name"] if name != dReq.Name { - writeError(w, errNonMatchingPathAndJSONKeyNames) + common.WriteError(w, errNonMatchingPathAndJSONKeyNames) return } if err := k.manager.Delete(dReq.Name, dReq.Passphrase); err != nil { - writeError(w, err) + common.WriteError(w, err) return } - resp := &ErrorResponse{Success: true} - writeSuccess(w, resp) + resp := &common.ErrorResponse{Success: true} + common.WriteSuccess(w, resp) } func (k *Keys) Register(r *mux.Router) { @@ -145,140 +135,38 @@ type Context struct { func (ctx *Context) RegisterHandlers(r *mux.Router) error { ctx.Keys.Register(r) - r.HandleFunc("/build/send", doSend).Methods("POST") r.HandleFunc("/sign", doSign).Methods("POST") r.HandleFunc("/tx", doPostTx).Methods("POST") - r.HandleFunc("/query/account/{signature}", doAccountQuery).Methods("GET") return nil } -func extractAddress(signature string) (address string, err *ErrorResponse) { - // Expecting the signature of the form: - // sig:
- splits := strings.Split(signature, ":") - if len(splits) < 2 { - return "", &ErrorResponse{ - Error: `expecting the signature of the form "sig:
"`, - Code: 406, - } - } - if splits[0] != "sigs" { - return "", &ErrorResponse{ - Error: `expecting the signature of the form "sig:
"`, - Code: 406, - } - } - return splits[1], nil -} - -func doAccountQuery(w http.ResponseWriter, r *http.Request) { - query := mux.Vars(r) - signature := query["signature"] - address, errResp := extractAddress(signature) - if errResp != nil { - writeCode(w, errResp, errResp.Code) - return - } - actor, err := commands.ParseActor(address) - if err != nil { - writeError(w, err) - return - } - actor = coin.ChainAddr(actor) - key := stack.PrefixedKey(coin.NameCoin, actor.Bytes()) - account := new(coin.Account) - proof, err := proofs.GetAndParseAppProof(key, account) - if lightclient.IsNoDataErr(err) { - err := fmt.Errorf("account bytes are empty for address: %q", address) - writeError(w, err) - return - } else if err != nil { - writeError(w, err) - return - } - - if err := proofs.OutputProof(account, proof.BlockHeight()); err != nil { - writeError(w, err) - return - } - writeSuccess(w, account) -} - func doPostTx(w http.ResponseWriter, r *http.Request) { tx := new(basecoin.Tx) - if err := parseRequestJSON(r, tx); err != nil { - writeError(w, err) + if err := common.ParseRequestAndValidateJSON(r, tx); err != nil { + common.WriteError(w, err) return } commit, err := PostTx(*tx) if err != nil { - writeError(w, err) + common.WriteError(w, err) return } - writeSuccess(w, commit) + common.WriteSuccess(w, commit) } func doSign(w http.ResponseWriter, r *http.Request) { sr := new(SignRequest) - if err := parseRequestJSON(r, sr); err != nil { - writeError(w, err) + if err := common.ParseRequestAndValidateJSON(r, sr); err != nil { + common.WriteError(w, err) return } tx := sr.Tx if err := SignTx(sr.Name, sr.Password, tx); err != nil { - writeError(w, err) + common.WriteError(w, err) return } - writeSuccess(w, tx) -} - -func doSend(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - si := new(SendInput) - if err := parseRequestJSON(r, si); err != nil { - writeError(w, err) - return - } - - var errsList []string - if si.From == nil { - errsList = append(errsList, `"from" cannot be nil`) - } - if si.Sequence <= 0 { - errsList = append(errsList, `"sequence" must be > 0`) - } - if si.To == nil { - errsList = append(errsList, `"to" cannot be nil`) - } - if len(si.Amount) == 0 { - errsList = append(errsList, `"amount" cannot be empty`) - } - if len(errsList) > 0 { - err := &ErrorResponse{ - Error: strings.Join(errsList, ", "), - Code: 406, - } - writeCode(w, err, 406) - return - } - - tx := coin.NewSendOneTx(*si.From, *si.To, si.Amount) - // fees are optional - if si.Fees != nil && !si.Fees.IsZero() { - tx = fee.NewFee(tx, *si.Fees, *si.From) - } - // only add the actual signer to the nonce - signers := []basecoin.Actor{*si.From} - tx = nonce.NewTx(si.Sequence, signers, tx) - tx = base.NewChainTx(commands.GetChainID(), 0, tx) - - if si.Multi { - tx = auth.NewMulti(tx).Wrap() - } else { - tx = auth.NewSig(tx).Wrap() - } - writeSuccess(w, tx) + common.WriteSuccess(w, tx) } diff --git a/client/rest/types.go b/client/rest/types.go index fdc96ae31..a04141ae9 100644 --- a/client/rest/types.go +++ b/client/rest/types.go @@ -1,17 +1,9 @@ package rest import ( - "encoding/json" - "io/ioutil" - "net/http" - - "github.com/pkg/errors" - "gopkg.in/go-playground/validator.v9" - "github.com/tendermint/basecoin" "github.com/tendermint/basecoin/modules/coin" "github.com/tendermint/go-crypto/keys" - data "github.com/tendermint/go-wire/data" ) type CreateKeyRequest struct { @@ -40,16 +32,6 @@ type SignRequest struct { Tx basecoin.Tx `json:"tx" validate:"required"` } -type ErrorResponse struct { - Success bool `json:"success,omitempty"` - - // Error is the error message if Success is false - Error string `json:"error,omitempty"` - - // Code is set if Success is false - Code int `json:"code,omitempty"` -} - type CreateKeyResponse struct { Key keys.Info `json:"key,omitempty"` Seed string `json:"seed_phrase,omitempty"` @@ -68,48 +50,3 @@ type SendInput struct { From *basecoin.Actor `json:"from"` Amount coin.Coins `json:"amount"` } - -// Validators - -var theValidator = validator.New() - -func validate(req interface{}) error { - return errors.Wrap(theValidator.Struct(req), "Validate") -} - -// Helpers -func parseRequestJSON(r *http.Request, save interface{}) error { - defer r.Body.Close() - - slurp, err := ioutil.ReadAll(r.Body) - if err != nil { - return errors.Wrap(err, "Read Request") - } - if err := json.Unmarshal(slurp, save); err != nil { - return errors.Wrap(err, "Parse") - } - return validate(save) -} - -func writeSuccess(w http.ResponseWriter, data interface{}) { - writeCode(w, data, 200) -} - -func writeCode(w http.ResponseWriter, out interface{}, code int) { - blob, err := data.ToJSON(out) - if err != nil { - writeError(w, err) - } else { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(code) - w.Write(blob) - } -} - -func writeError(w http.ResponseWriter, err error) { - resp := &ErrorResponse{ - Code: 406, - Error: err.Error(), - } - writeCode(w, resp, 406) -} diff --git a/cmd/baseserver/main.go b/cmd/baseserver/main.go index 7e88bdfde..b15603cde 100644 --- a/cmd/baseserver/main.go +++ b/cmd/baseserver/main.go @@ -1,12 +1,9 @@ package main import ( - "fmt" "log" - "net/http" "os" - "github.com/gorilla/mux" "github.com/spf13/cobra" "github.com/tendermint/basecoin/client/commands" @@ -20,24 +17,12 @@ var srvCli = &cobra.Command{ Long: `Baseserver presents a nice (not raw hex) interface to the basecoin blockchain structure.`, } -var serveCmd = &cobra.Command{ - Use: "serve", - Short: "Serve the light REST client for tendermint", - Long: "Access basecoin via REST", - RunE: serve, -} - -var port int - func main() { commands.AddBasicFlags(srvCli) - flagset := serveCmd.Flags() - flagset.IntVar(&port, "port", 8998, "the port to run the server on") - srvCli.AddCommand( commands.InitCmd, - serveCmd, + rest.ServeCmd, ) // TODO: Decide whether to use $HOME/.basecli for compatibility @@ -47,20 +32,3 @@ func main() { log.Fatal(err) } } - -const defaultAlgo = "ed25519" - -func serve(cmd *cobra.Command, args []string) error { - keysManager := rest.DefaultKeysManager() - router := mux.NewRouter() - ctx := rest.Context{ - Keys: rest.New(keysManager, defaultAlgo), - } - if err := ctx.RegisterHandlers(router); err != nil { - return err - } - - addr := fmt.Sprintf(":%d", port) - log.Printf("Serving on %q", addr) - return http.ListenAndServe(addr, router) -} diff --git a/glide.lock b/glide.lock index 5071a9ab7..4a6213884 100644 --- a/glide.lock +++ b/glide.lock @@ -173,7 +173,7 @@ imports: - types - version - name: github.com/tendermint/tmlibs - version: 2f6f3e6aa70bb19b70a6e73210273fa127041070 + version: 75372988e737a9f672c0e7f6308042620bd3e151 subpackages: - autofile - cli diff --git a/modules/coin/rest/handlers.go b/modules/coin/rest/handlers.go new file mode 100644 index 000000000..fbfed54c4 --- /dev/null +++ b/modules/coin/rest/handlers.go @@ -0,0 +1,123 @@ +package rest + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gorilla/mux" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/client/commands" + "github.com/tendermint/basecoin/client/commands/proofs" + "github.com/tendermint/basecoin/modules/auth" + "github.com/tendermint/basecoin/modules/base" + "github.com/tendermint/basecoin/modules/coin" + "github.com/tendermint/basecoin/modules/fee" + "github.com/tendermint/basecoin/modules/nonce" + "github.com/tendermint/basecoin/stack" + lightclient "github.com/tendermint/light-client" + "github.com/tendermint/tmlibs/common" +) + +// SendInput is the request to send an amount from one actor to another. +// Note: Not using the `validator:""` tags here because SendInput has +// many fields so it would be nice to figure out all the invalid +// inputs and report them back to the caller, in one shot. +type SendInput struct { + Fees *coin.Coin `json:"fees"` + Multi bool `json:"multi,omitempty"` + Sequence uint32 `json:"sequence"` + + To *basecoin.Actor `json:"to"` + From *basecoin.Actor `json:"from"` + Amount coin.Coins `json:"amount"` +} + +func RegisterHandlers(r *mux.Router) error { + r.HandleFunc("/build/send", doSend).Methods("POST") + r.HandleFunc("/query/account/{signature}", doQueryAccount).Methods("GET") + return nil +} + +// doQueryAccount is the HTTP handlerfunc to query an account +// It expects a query string with +func doQueryAccount(w http.ResponseWriter, r *http.Request) { + query := mux.Vars(r) + signature := query["signature"] + actor, err := commands.ParseActor(signature) + if err != nil { + common.WriteError(w, err) + return + } + actor = coin.ChainAddr(actor) + key := stack.PrefixedKey(coin.NameCoin, actor.Bytes()) + account := new(coin.Account) + proof, err := proofs.GetAndParseAppProof(key, account) + if lightclient.IsNoDataErr(err) { + err := fmt.Errorf("account bytes are empty for address: %q", signature) + common.WriteError(w, err) + return + } else if err != nil { + common.WriteError(w, err) + return + } + + if err := proofs.FoutputProof(w, account, proof.BlockHeight()); err != nil { + common.WriteError(w, err) + } +} + +func PrepareSendTx(si *SendInput) basecoin.Tx { + tx := coin.NewSendOneTx(*si.From, *si.To, si.Amount) + // fees are optional + if si.Fees != nil && !si.Fees.IsZero() { + tx = fee.NewFee(tx, *si.Fees, *si.From) + } + // only add the actual signer to the nonce + signers := []basecoin.Actor{*si.From} + tx = nonce.NewTx(si.Sequence, signers, tx) + tx = base.NewChainTx(commands.GetChainID(), 0, tx) + + if si.Multi { + tx = auth.NewMulti(tx).Wrap() + } else { + tx = auth.NewSig(tx).Wrap() + } + return tx +} + +func doSend(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + si := new(SendInput) + if err := common.ParseRequestAndValidateJSON(r, si); err != nil { + common.WriteError(w, err) + return + } + + var errsList []string + if si.From == nil { + errsList = append(errsList, `"from" cannot be nil`) + } + if si.Sequence <= 0 { + errsList = append(errsList, `"sequence" must be > 0`) + } + if si.To == nil { + errsList = append(errsList, `"to" cannot be nil`) + } + if len(si.Amount) == 0 { + errsList = append(errsList, `"amount" cannot be empty`) + } + if len(errsList) > 0 { + code := http.StatusBadRequest + err := &common.ErrorResponse{ + Err: strings.Join(errsList, ", "), + Code: code, + } + common.WriteCode(w, err, code) + return + } + + tx := PrepareSendTx(si) + common.WriteSuccess(w, tx) +}