-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #190 from orijtech/rest-server
client/rest, cmd/baseserver: started a basecoin REST client
- Loading branch information
Showing
6 changed files
with
641 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, ""), | ||
} | ||
} |
Oops, something went wrong.