Skip to content

Commit

Permalink
Merge pull request #190 from orijtech/rest-server
Browse files Browse the repository at this point in the history
client/rest, cmd/baseserver: started a basecoin REST client
  • Loading branch information
ethanfrey authored Jul 29, 2017
2 parents eae1883 + 7c28374 commit b08b630
Show file tree
Hide file tree
Showing 6 changed files with 641 additions and 0 deletions.
25 changes: 25 additions & 0 deletions client/rest/README.md
Original file line number Diff line number Diff line change
@@ -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.
284 changes: 284 additions & 0 deletions client/rest/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
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"
)

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")
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:<ADDRESS>
splits := strings.Split(signature, ":")
if len(splits) < 2 {
return "", &ErrorResponse{
Error: `expecting the signature of the form "sig:<ADDRESS>"`,
Code: 406,
}
}
if splits[0] != "sigs" {
return "", &ErrorResponse{
Error: `expecting the signature of the form "sig:<ADDRESS>"`,
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)
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 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)
}
35 changes: 35 additions & 0 deletions client/rest/proxy.go
Original file line number Diff line number Diff line change
@@ -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, ""),
}
}
Loading

0 comments on commit b08b630

Please sign in to comment.