client/rest, cmd/baseserver: started a basecoin REST client

```shell
$ go get -u -v github.com/tendermint/basecoin/cmd/baseserver
$ baseserver init
$ baseserver serve
```
A server that can be ran by default on port 8998
otherwise one can specify the port using flag `--port` like this:
```shell
$ baseserver serve --port 9999
```
to serve it on port 9999, accessible at http://localhost:9999

Implemented:
- [X] /keys POST -- generate a new key
- [X] /keys GET  -- list all keys
- [X] /keys/{name}  DELETE-- delete a named key
- [X] /keys/{name}  GET -- get a named key
- [X] /keys/{name}  POST, PUT -- update a named key
- [X] /sign POST -- sign a transaction
- [X] /build/send POST -- send money from one actor to another. However,
  still needs testing and verification of output
- [X] /tx POST -- post a transaction to the blockchain. However, still
  needs testing and verification of output

This base code to get the handlers starters was adapted from:
* https://github.com/tendermint/go-crypto/blob/master/keys/server
* https://github.com/tendermint/basecoin/blob/unstable/client/commands/proxy/root.go

Updates #186
This commit is contained in:
Emmanuel Odeke
2017-07-29 04:12:24 -06:00
parent eae1883f3d
commit d4ab79ece0
6 changed files with 590 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
## basecoin-server
### Proxy server
This package exposes access to key management i.e
- creating
- listing
- updating
- deleting
The HTTP handlers can be embedded in a larger server that
does things like signing transactions and posting them to a
Tendermint chain (which requires domain-knowledge of the transaction
types and is out of scope of this generic app).
### Key Management
We expose a couple of methods for safely managing your keychain.
If you are embedding this in a larger server, you will typically
want to mount all these paths /keys.
HTTP Method | Route | Description
---|---|---
POST|/|Requires a name and passphrase to create a brand new key
GET|/|Retrieves the list of all available key names, along with their public key and address
GET|/{name} | Updates the passphrase for the given key. It requires you to correctly provide the current passphrase, as well as a new one.
DELETE|/{name} | Permanently delete this private key. It requires you to correctly provide the current passphrase.
+234
View File
@@ -0,0 +1,234 @@
package rest
import (
"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/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"
keysutils "github.com/tendermint/go-crypto/cmd"
keys "github.com/tendermint/go-crypto/keys"
)
type Keys struct {
algo string
manager keys.Manager
}
func DefaultKeysManager() keys.Manager {
return keysutils.GetKeyManager()
}
func New(manager keys.Manager, algo string) *Keys {
return &Keys{
algo: algo,
manager: manager,
}
}
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)
return
}
key, seed, err := k.manager.Create(ckReq.Name, ckReq.Passphrase, ckReq.Algo)
if err != nil {
writeError(w, err)
return
}
res := &CreateKeyResponse{Key: key, Seed: seed}
writeSuccess(w, res)
}
func (k *Keys) GetKey(w http.ResponseWriter, r *http.Request) {
query := mux.Vars(r)
name := query["name"]
key, err := k.manager.Get(name)
if err != nil {
writeError(w, err)
return
}
writeSuccess(w, &key)
}
func (k *Keys) ListKeys(w http.ResponseWriter, r *http.Request) {
keys, err := k.manager.List()
if err != nil {
writeError(w, err)
return
}
writeSuccess(w, keys)
}
var (
errNonMatchingPathAndJSONKeyNames = errors.New("path and json key names don't match")
)
func (k *Keys) UpdateKey(w http.ResponseWriter, r *http.Request) {
uReq := new(UpdateKeyRequest)
if err := parseRequestJSON(r, uReq); err != nil {
writeError(w, err)
return
}
query := mux.Vars(r)
name := query["name"]
if name != uReq.Name {
writeError(w, errNonMatchingPathAndJSONKeyNames)
return
}
if err := k.manager.Update(uReq.Name, uReq.OldPass, uReq.NewPass); err != nil {
writeError(w, err)
return
}
key, err := k.manager.Get(uReq.Name)
if err != nil {
writeError(w, err)
return
}
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)
return
}
query := mux.Vars(r)
name := query["name"]
if name != dReq.Name {
writeError(w, errNonMatchingPathAndJSONKeyNames)
return
}
if err := k.manager.Delete(dReq.Name, dReq.Passphrase); err != nil {
writeError(w, err)
return
}
resp := &ErrorResponse{Success: true}
writeSuccess(w, resp)
}
func (k *Keys) Register(r *mux.Router) {
r.HandleFunc("/keys", k.GenerateKey).Methods("POST")
r.HandleFunc("/keys", k.ListKeys).Methods("GET")
r.HandleFunc("/keys/{name}", k.GetKey).Methods("GET")
r.HandleFunc("/keys/{name}", k.UpdateKey).Methods("POST", "PUT")
r.HandleFunc("/keys/{name}", k.DeleteKey).Methods("DELETE")
}
type Context struct {
Keys *Keys
}
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")
return nil
}
func doPostTx(w http.ResponseWriter, r *http.Request) {
tx := new(basecoin.Tx)
if err := parseRequestJSON(r, tx); err != nil {
writeError(w, err)
return
}
commit, err := PostTx(*tx)
if err != nil {
writeError(w, err)
return
}
writeSuccess(w, commit)
}
func doSign(w http.ResponseWriter, r *http.Request) {
sr := new(SignRequest)
if err := parseRequestJSON(r, sr); err != nil {
writeError(w, err)
return
}
tx := sr.Tx
if err := SignTx(sr.Name, sr.Password, tx); err != nil {
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 si.Fees == nil {
errsList = append(errsList, `"fees" cannot be nil`)
}
if len(errsList) > 0 {
err := &ErrorResponse{
Error: strings.Join(errsList, ", "),
Code: 406,
}
writeCode(w, err, 406)
return
}
coins := []coin.Coin{*si.Fees}
in := []coin.TxInput{
coin.NewTxInput(*si.From, coins),
}
out := []coin.TxOutput{
coin.NewTxOutput(*si.To, coins),
}
tx := coin.NewSendTx(in, out)
tx = fee.NewFee(tx, *si.Fees, *si.From)
signers := []basecoin.Actor{
*si.From,
*si.To,
}
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()
}
}
+35
View File
@@ -0,0 +1,35 @@
package rest
import (
"github.com/tendermint/tendermint/rpc/client"
"github.com/tendermint/tendermint/rpc/core"
rpc "github.com/tendermint/tendermint/rpc/lib/server"
)
func Routes(c client.Client) map[string]*rpc.RPCFunc {
return map[string]*rpc.RPCFunc{
// subscribe/unsubscribe are reserved for websocket events.
// We can just the core Tendermint implementation, which uses
// the EventSwitch that we registered in NewWebsocketManager above.
"subscribe": rpc.NewWSRPCFunc(core.Subscribe, "event"),
"unsubscribe": rpc.NewWSRPCFunc(core.Unsubscribe, "event"),
// info API
"status": rpc.NewRPCFunc(c.Status, ""),
"blockchain": rpc.NewRPCFunc(c.BlockchainInfo, "minHeight,maxHeight"),
"genesis": rpc.NewRPCFunc(c.Genesis, ""),
"block": rpc.NewRPCFunc(c.Block, "height"),
"commit": rpc.NewRPCFunc(c.Commit, "height"),
"tx": rpc.NewRPCFunc(c.Tx, "hash.prove"),
"validators": rpc.NewRPCFunc(c.Validators, ""),
// broadcast API
"broadcast_tx_commit": rpc.NewRPCFunc(c.BroadcastTxCommit, "tx"),
"broadcast_tx_sync": rpc.NewRPCFunc(c.BroadcastTxSync, "tx"),
"broadcast_tx_async": rpc.NewRPCFunc(c.BroadcastTxAsync, "tx"),
// abci API
"abci_query": rpc.NewRPCFunc(c.ABCIQuery, "path,data,prove"),
"abci_info": rpc.NewRPCFunc(c.ABCIInfo, ""),
}
}
+114
View File
@@ -0,0 +1,114 @@
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 {
Name string `json:"name,omitempty" validate:"required,min=4,printascii"`
Passphrase string `json:"passphrase,omitempty" validate:"required,min=10"`
// Algo is the requested algorithm to create the key
Algo string `json:"algo,omitempty"`
}
type DeleteKeyRequest struct {
Name string `json:"name,omitempty" validate:"required,min=4,printascii"`
Passphrase string `json:"passphrase,omitempty" validate:"required,min=10"`
}
type UpdateKeyRequest struct {
Name string `json:"name,omitempty" validate:"required,min=4,printascii"`
OldPass string `json:"passphrase,omitempty" validate:"required,min=10"`
NewPass string `json:"new_passphrase,omitempty" validate:"required,min=10"`
}
type SignRequest struct {
Name string `json:"name,omitempty" validate:"required,min=4,printascii"`
Password string `json:"password,omitempty" validate:"required,min=10"`
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"`
}
// 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:"amount"`
Multi bool `json:"multi,omitempty"`
Sequence uint32 `json:"sequence"`
To *basecoin.Actor `json:"to"`
From *basecoin.Actor `json:"from"`
}
// 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)
}