diff --git a/Gopkg.lock b/Gopkg.lock index fa6fafb99dd7..ab0bac517948 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -374,6 +374,24 @@ revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" version = "v1.2.1" +[[projects]] + name = "github.com/swaggo/gin-swagger" + packages = [ + ".", + "swaggerFiles" + ] + revision = "8cf3fa9932e247205d1cf2be6fd13a9d09ceb9a4" + version = "v1.0.0" + +[[projects]] + name = "github.com/swaggo/swag" + packages = [ + ".", + "example/celler/httputil" + ] + revision = "db2ee6c14a35e4fca95b0d75054296ef2b699050" + version = "v1.3.2" + [[projects]] branch = "master" digest = "1:442d2ffa75ffae302ce8800bf4144696b92bef02917923ea132ce2d39efe7d65" diff --git a/Gopkg.toml b/Gopkg.toml index 4368699b6be9..3159111534f9 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -47,6 +47,14 @@ name = "github.com/stretchr/testify" version = "=1.2.1" +[[constraint]] + name = "github.com/swaggo/gin-swagger" + version = "=v1.0.0" + +[[constraint]] + name = "github.com/swaggo/swag" + version = "=v1.3.2" + [[override]] name = "github.com/tendermint/go-amino" version = "=v0.12.0-rc0" diff --git a/client/context/client_manager.go b/client/context/client_manager.go new file mode 100644 index 000000000000..8fffb9d65185 --- /dev/null +++ b/client/context/client_manager.go @@ -0,0 +1,46 @@ +package context + +import ( + "github.com/pkg/errors" + rpcclient "github.com/tendermint/tendermint/rpc/client" + "strings" + "sync" +) + +// ClientManager is a manager of a set of rpc clients to full nodes. +// This manager can do load balancing upon these rpc clients. +type ClientManager struct { + clients []rpcclient.Client + currentIndex int + mutex sync.Mutex +} + +// NewClientManager create a new ClientManager +func NewClientManager(nodeURIs string) (*ClientManager, error) { + if nodeURIs != "" { + nodeURLArray := strings.Split(nodeURIs, ",") + var clients []rpcclient.Client + for _, url := range nodeURLArray { + client := rpcclient.NewHTTP(url, "/websocket") + clients = append(clients, client) + } + mgr := &ClientManager{ + currentIndex: 0, + clients: clients, + } + return mgr, nil + } + return nil, errors.New("missing node URIs") +} + +func (mgr *ClientManager) getClient() rpcclient.Client { + mgr.mutex.Lock() + defer mgr.mutex.Unlock() + + client := mgr.clients[mgr.currentIndex] + mgr.currentIndex++ + if mgr.currentIndex >= len(mgr.clients) { + mgr.currentIndex = 0 + } + return client +} diff --git a/client/context/client_manager_test.go b/client/context/client_manager_test.go new file mode 100644 index 000000000000..1960f74ced25 --- /dev/null +++ b/client/context/client_manager_test.go @@ -0,0 +1,16 @@ +package context + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestClientManager(t *testing.T) { + nodeURIs := "10.10.10.10:26657,20.20.20.20:26657,30.30.30.30:26657" + clientMgr, err := NewClientManager(nodeURIs) + assert.Empty(t, err) + endpoint := clientMgr.getClient() + assert.NotEqual(t, endpoint, clientMgr.getClient()) + clientMgr.getClient() + assert.Equal(t, endpoint, clientMgr.getClient()) +} diff --git a/client/context/context.go b/client/context/context.go index 743c923552c8..b57e336657c2 100644 --- a/client/context/context.go +++ b/client/context/context.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/viper" rpcclient "github.com/tendermint/tendermint/rpc/client" + tendermintLite "github.com/tendermint/tendermint/lite" ) const ctxAccStoreName = "acc" @@ -32,6 +33,8 @@ type CLIContext struct { Async bool JSON bool PrintResponse bool + Certifier tendermintLite.Certifier + ClientManager *ClientManager } // NewCLIContext returns a new initialized CLIContext with parameters from the @@ -117,3 +120,15 @@ func (ctx CLIContext) WithUseLedger(useLedger bool) CLIContext { ctx.UseLedger = useLedger return ctx } + +// WithCertifier - return a copy of the context with an updated Certifier +func (ctx CLIContext) WithCertifier(certifier tendermintLite.Certifier) CLIContext { + ctx.Certifier = certifier + return ctx +} + +// WithClientManager - return a copy of the context with an updated ClientManager +func (ctx CLIContext) WithClientManager(clientManager *ClientManager) CLIContext { + ctx.ClientManager = clientManager + return ctx +} diff --git a/client/context/query.go b/client/context/query.go index e526c0abbc90..0cc845306fff 100644 --- a/client/context/query.go +++ b/client/context/query.go @@ -13,11 +13,19 @@ import ( cmn "github.com/tendermint/tendermint/libs/common" rpcclient "github.com/tendermint/tendermint/rpc/client" ctypes "github.com/tendermint/tendermint/rpc/core/types" + "github.com/cosmos/cosmos-sdk/store" + "github.com/cosmos/cosmos-sdk/wire" + "strings" + tendermintLiteProxy "github.com/tendermint/tendermint/lite/proxy" + abci "github.com/tendermint/tendermint/abci/types" ) // GetNode returns an RPC client. If the context's client is not defined, an // error is returned. func (ctx CLIContext) GetNode() (rpcclient.Client, error) { + if ctx.ClientManager != nil { + return ctx.ClientManager.getClient(), nil + } if ctx.Client == nil { return nil, errors.New("no RPC client defined") } @@ -159,6 +167,22 @@ func (ctx CLIContext) BroadcastTxAsync(tx []byte) (*ctypes.ResultBroadcastTx, er return res, err } +// BroadcastTxSync broadcasts transaction bytes to a Tendermint node +// synchronously. +func (ctx CLIContext) BroadcastTxSync(tx []byte) (*ctypes.ResultBroadcastTx, error) { + node, err := ctx.GetNode() + if err != nil { + return nil, err + } + + res, err := node.BroadcastTxSync(tx) + if err != nil { + return res, err + } + + return res, err +} + // EnsureAccountExists ensures that an account exists for a given context. An // error is returned if it does not. func (ctx CLIContext) EnsureAccountExists() error { @@ -281,6 +305,47 @@ func (ctx CLIContext) ensureBroadcastTx(txBytes []byte) error { return nil } +// verifyProof perform response proof verification +func (ctx CLIContext) verifyProof(path string, resp abci.ResponseQuery) error { + + // TODO: Later we consider to return error for missing valid certifier to verify data from untrusted node + if ctx.Certifier == nil { + if ctx.Logger != nil { + io.WriteString(ctx.Logger, fmt.Sprintf("Missing valid certifier to verify data from untrusted node\n")) + } + return nil + } + + node, err := ctx.GetNode() + if err != nil { + return err + } + // AppHash for height H is in header H+1 + commit, err := tendermintLiteProxy.GetCertifiedCommit(resp.Height+1, node, ctx.Certifier) + if err != nil { + return err + } + + var multiStoreProof store.MultiStoreProof + cdc := wire.NewCodec() + err = cdc.UnmarshalBinary(resp.Proof, &multiStoreProof) + if err != nil { + return errors.Wrap(err, "failed to unmarshalBinary rangeProof") + } + + // Validate the substore commit hash against trusted appHash + substoreCommitHash, err := store.VerifyMultiStoreCommitInfo(multiStoreProof.StoreName, + multiStoreProof.CommitIDList, commit.Header.AppHash) + if err != nil { + return errors.Wrap(err, "failed in verifying the proof against appHash") + } + err = store.VerifyRangeProof(resp.Key, resp.Value, substoreCommitHash, &multiStoreProof.RangeProof) + if err != nil { + return errors.Wrap(err, "failed in the range proof verification") + } + return nil +} + // query performs a query from a Tendermint node with the provided store name // and path. func (ctx CLIContext) query(path string, key cmn.HexBytes) (res []byte, err error) { @@ -304,6 +369,16 @@ func (ctx CLIContext) query(path string, key cmn.HexBytes) (res []byte, err erro return res, errors.Errorf("query failed: (%d) %s", resp.Code, resp.Log) } + // Data from trusted node or subspace query doesn't need verification + if ctx.TrustNode || !isQueryStoreWithProof(path) { + return resp.Value, nil + } + + err = ctx.verifyProof(path, resp) + if err != nil { + return nil, err + } + return resp.Value, nil } @@ -313,3 +388,22 @@ func (ctx CLIContext) queryStore(key cmn.HexBytes, storeName, endPath string) ([ path := fmt.Sprintf("/store/%s/%s", storeName, endPath) return ctx.query(path, key) } + +// isQueryStoreWithProof expects a format like /// +// queryType can be app or store +// if subpath equals to store or key, then return true +func isQueryStoreWithProof(path string) (bool) { + if !strings.HasPrefix(path, "/") { + return false + } + paths := strings.SplitN(path[1:], "/", 3) + if len(paths) != 3 { + return false + } + // Currently, only when query path[2] is store or key, will proof be included in response. + // If there are some changes about proof building in iavlstore.go, we must change code here to keep consistency with iavlstore.go + if paths[2] == "store" || paths[2] == "key" { + return true + } + return false +} diff --git a/client/flags.go b/client/flags.go index 81e06706784a..330369694186 100644 --- a/client/flags.go +++ b/client/flags.go @@ -23,6 +23,11 @@ const ( FlagAsync = "async" FlagJson = "json" FlagPrintResponse = "print-response" + FlagListenAddr = "laddr" + FlagSwaggerHostIP = "swagger-host-ip" + FlagModules = "modules" + FlagNodeList = "node-list" + FlagTrustStore = "trust-store" ) // LineBreak can be included in a command list to provide a blank line diff --git a/client/httputils/httputils.go b/client/httputils/httputils.go new file mode 100644 index 000000000000..45747c22a947 --- /dev/null +++ b/client/httputils/httputils.go @@ -0,0 +1,32 @@ +package httputils + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +// NewError create error http response +func NewError(ctx *gin.Context, errCode int, err error) { + errorResponse := HTTPError{ + API: "2.0", + Code: errCode, + } + if err != nil { + errorResponse.ErrMsg = err.Error() + } + + ctx.JSON(errCode, errorResponse) +} + +// NormalResponse create normal http response +func NormalResponse(ctx *gin.Context, data []byte) { + ctx.Status(http.StatusOK) + ctx.Writer.Write(data) +} + +// HTTPError is http response with error +type HTTPError struct { + API string `json:"rest api" example:"2.0"` + Code int `json:"code" example:"500"` + ErrMsg string `json:"error message"` +} \ No newline at end of file diff --git a/client/keys/add.go b/client/keys/add.go index d462db1c0697..33670d1782d7 100644 --- a/client/keys/add.go +++ b/client/keys/add.go @@ -16,6 +16,9 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys" "github.com/tendermint/tendermint/libs/cli" + "github.com/gin-gonic/gin" + "github.com/cosmos/cosmos-sdk/client/httputils" + "regexp/syntax" ) const ( @@ -177,34 +180,107 @@ func AddNewKeyRequestHandler(w http.ResponseWriter, r *http.Request) { } body, err := ioutil.ReadAll(r.Body) - err = json.Unmarshal(body, &m) + err = cdc.UnmarshalJSON(body, &m) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) return } - if m.Name == "" { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("You have to specify a name for the locally stored account.")) + + errCode, err := paramCheck(kb, m.Name, m.Password) + if err != nil { + w.WriteHeader(errCode) + w.Write([]byte(err.Error())) return } - if m.Password == "" { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("You have to specify a password for the locally stored account.")) + + // create account + seed := m.Seed + if seed == "" { + seed = getSeed(keys.Secp256k1) + } + info, err := kb.CreateKey(m.Name, seed, m.Password) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) return } + keyOutput, err := Bech32KeyOutput(info) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + keyOutput.Seed = seed + + bz, err := cdc.MarshalJSON(keyOutput) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + w.Write(bz) +} + +// paramCheck performs add new key parameters checking +func paramCheck(kb keys.Keybase, name, password string) (int, error) { + if len(name) < 1 || len(name) > 16 { + return http.StatusBadRequest, fmt.Errorf("account name length should not be longer than 16") + } + for _, char := range []rune(name) { + if !syntax.IsWordChar(char) { + return http.StatusBadRequest, fmt.Errorf("account name should not contains any char beyond [_0-9A-Za-z]") + } + } + if len(password) < 8 || len(password) > 16 { + return http.StatusBadRequest, fmt.Errorf("account password length should be no less than 8 and no greater than 16") + } + // check if already exists infos, err := kb.List() + if err != nil { + return http.StatusInternalServerError, err + } + for _, i := range infos { - if i.GetName() == m.Name { - w.WriteHeader(http.StatusConflict) - w.Write([]byte(fmt.Sprintf("Account with name %s already exists.", m.Name))) - return + if i.GetName() == name { + return http.StatusConflict, fmt.Errorf("account with name %s already exists", name) } } + return 0, nil +} + +// AddNewKeyRequest is the handler of adding new key in swagger rest server +func AddNewKeyRequest(gtx *gin.Context) { + var m NewKeyBody + body, err := ioutil.ReadAll(gtx.Request.Body) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + err = cdc.UnmarshalJSON(body, &m) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + kb, err := GetKeyBase() + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + errCode, err := paramCheck(kb, m.Name, m.Password) + if err != nil { + httputils.NewError(gtx, errCode, err) + return + } + // create account seed := m.Seed if seed == "" { @@ -212,15 +288,13 @@ func AddNewKeyRequestHandler(w http.ResponseWriter, r *http.Request) { } info, err := kb.CreateKey(m.Name, seed, m.Password) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + httputils.NewError(gtx, http.StatusInternalServerError, err) return } keyOutput, err := Bech32KeyOutput(info) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + httputils.NewError(gtx, http.StatusInternalServerError, err) return } @@ -228,12 +302,12 @@ func AddNewKeyRequestHandler(w http.ResponseWriter, r *http.Request) { bz, err := json.Marshal(keyOutput) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + httputils.NewError(gtx, http.StatusInternalServerError, err) return } - w.Write(bz) + httputils.NormalResponse(gtx, bz) + } // function to just a new seed to display in the UI before actually persisting it in the keybase @@ -258,3 +332,57 @@ func SeedRequestHandler(w http.ResponseWriter, r *http.Request) { seed := getSeed(algo) w.Write([]byte(seed)) } + +// RecoverKeyBody is recover key request REST body +type RecoverKeyBody struct { + Password string `json:"password"` + Seed string `json:"seed"` +} + +// RecoverResuest is the handler of creating seed in swagger rest server +func RecoverResuest(gtx *gin.Context) { + name := gtx.Param("name") + var m RecoverKeyBody + body, err := ioutil.ReadAll(gtx.Request.Body) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + err = cdc.UnmarshalJSON(body, &m) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + kb, err := GetKeyBase() + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + errCode, err := paramCheck(kb, name, m.Password) + if err != nil { + httputils.NewError(gtx, errCode, err) + return + } + + info, err := kb.CreateKey(name, m.Seed, m.Password) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + keyOutput, err := Bech32KeyOutput(info) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + bz, err := cdc.MarshalJSON(keyOutput) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + httputils.NormalResponse(gtx, bz) +} \ No newline at end of file diff --git a/client/keys/delete.go b/client/keys/delete.go index 944feb4b19b2..722460f94726 100644 --- a/client/keys/delete.go +++ b/client/keys/delete.go @@ -10,6 +10,9 @@ import ( "github.com/gorilla/mux" "github.com/spf13/cobra" + "github.com/gin-gonic/gin" + "github.com/cosmos/cosmos-sdk/client/httputils" + "io/ioutil" ) func deleteKeyCommand() *cobra.Command { @@ -90,3 +93,35 @@ func DeleteKeyRequestHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) } + +// DeleteKeyRequest is the handler of deleting specified key in swagger rest server +func DeleteKeyRequest(gtx *gin.Context) { + name := gtx.Param("name") + var m DeleteKeyBody + + body, err := ioutil.ReadAll(gtx.Request.Body) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + err = cdc.UnmarshalJSON(body, &m) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + kb, err := GetKeyBase() + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + // TODO handle error if key is not available or pass is wrong + err = kb.Delete(name, m.Password) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + httputils.NormalResponse(gtx, []byte("success")) +} \ No newline at end of file diff --git a/client/keys/list.go b/client/keys/list.go index 22f163f1d8ff..e54d95d70f01 100644 --- a/client/keys/list.go +++ b/client/keys/list.go @@ -1,10 +1,11 @@ package keys import ( - "encoding/json" "net/http" "github.com/spf13/cobra" + "github.com/gin-gonic/gin" + "github.com/cosmos/cosmos-sdk/client/httputils" ) // CMD @@ -59,7 +60,7 @@ func QueryKeysRequestHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte(err.Error())) return } - output, err := json.MarshalIndent(keysOutput, "", " ") + output, err := cdc.MarshalJSONIndent(keysOutput, "", " ") if err != nil { w.WriteHeader(500) w.Write([]byte(err.Error())) @@ -67,3 +68,33 @@ func QueryKeysRequestHandler(w http.ResponseWriter, r *http.Request) { } w.Write(output) } + +// QueryKeysRequest is the handler of listing all keys in swagger rest server +func QueryKeysRequest(gtx *gin.Context) { + kb, err := GetKeyBase() + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + infos, err := kb.List() + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + // an empty list will be JSONized as null, but we want to keep the empty list + if len(infos) == 0 { + httputils.NormalResponse(gtx, nil) + return + } + keysOutput, err := Bech32KeysOutput(infos) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + output, err := cdc.MarshalJSONIndent(keysOutput, "", " ") + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + httputils.NormalResponse(gtx, output) +} \ No newline at end of file diff --git a/client/keys/root.go b/client/keys/root.go index c8f6aea693af..a8a1b685ac41 100644 --- a/client/keys/root.go +++ b/client/keys/root.go @@ -4,6 +4,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/gorilla/mux" "github.com/spf13/cobra" + "github.com/gin-gonic/gin" ) // Commands registers a sub-tree of commands to interact with @@ -22,6 +23,7 @@ func Commands() *cobra.Command { addKeyCommand(), listKeysCmd, showKeysCmd, + keySignCmd, client.LineBreak, deleteKeyCommand(), updateKeyCommand(), @@ -38,3 +40,14 @@ func RegisterRoutes(r *mux.Router) { r.HandleFunc("/keys/{name}", UpdateKeyRequestHandler).Methods("PUT") r.HandleFunc("/keys/{name}", DeleteKeyRequestHandler).Methods("DELETE") } + +// RegisterSwaggerRoutes registers key management related routes to Gaia-lite server +func RegisterSwaggerRoutes(routerGroup *gin.RouterGroup) { + routerGroup.GET("/keys", QueryKeysRequest) + routerGroup.POST("/keys", AddNewKeyRequest) + routerGroup.POST("/keys/:name/recover", RecoverResuest) + routerGroup.GET("/keys/:name", GetKeyRequest) + routerGroup.PUT("/keys/:name", UpdateKeyRequest) + routerGroup.DELETE("/keys/:name", DeleteKeyRequest) + routerGroup.POST("/keys/:name/sign", SignResuest) +} \ No newline at end of file diff --git a/client/keys/show.go b/client/keys/show.go index e9d692ece579..9236e7648800 100644 --- a/client/keys/show.go +++ b/client/keys/show.go @@ -9,8 +9,10 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/spf13/viper" + "github.com/spf13/viper" "github.com/tendermint/tmlibs/cli" + "github.com/gin-gonic/gin" + "github.com/cosmos/cosmos-sdk/client/httputils" ) const ( @@ -100,3 +102,27 @@ func GetKeyRequestHandler(w http.ResponseWriter, r *http.Request) { w.Write(output) } + +// GetKeyRequest is the handler of getting specified key in swagger rest server +func GetKeyRequest(gtx *gin.Context) { + name := gtx.Param("name") + + info, err := getKey(name) + // TODO check for the error if key actually does not exist, instead of assuming this as the reason + if err != nil { + httputils.NewError(gtx, http.StatusNotFound, err) + return + } + + keyOutput, err := Bech32KeyOutput(info) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + output, err := json.MarshalIndent(keyOutput, "", " ") + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + httputils.NormalResponse(gtx, output) +} \ No newline at end of file diff --git a/client/keys/sign.go b/client/keys/sign.go new file mode 100644 index 000000000000..ce311a41ae7e --- /dev/null +++ b/client/keys/sign.go @@ -0,0 +1,89 @@ +package keys + +import ( + "github.com/gin-gonic/gin" + "io/ioutil" + "github.com/cosmos/cosmos-sdk/client/httputils" + "net/http" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "encoding/base64" + "fmt" +) + +const ( + flagFrom = "from" + flagPassword = "password" + flagTx = "tx" +) + +func init() { + keySignCmd.Flags().String(flagFrom, "", "Name of private key with which to sign") + keySignCmd.Flags().String(flagPassword, "", "Password of private key") + keySignCmd.Flags().String(flagTx, "", "Base64 encoded tx data for sign") +} + +var keySignCmd = &cobra.Command{ + Use: "sign", + Short: "Sign user specified data", + Long: `Sign user data with specified key and password`, + RunE: func(cmd *cobra.Command, args []string) error { + name := viper.GetString(flagFrom) + password := viper.GetString(flagPassword) + tx := viper.GetString(flagTx) + + decodedTx, err := base64.StdEncoding.DecodeString(tx) + if err != nil { + return err + } + + kb, err := GetKeyBase() + if err != nil { + return err + } + + sig, _, err := kb.Sign(name, password, decodedTx) + if err != nil { + return err + } + encoded := base64.StdEncoding.EncodeToString(sig) + fmt.Println(string(encoded)) + return nil + }, +} + +type keySignBody struct { + Tx []byte `json:"tx_bytes"` + Password string `json:"password"` +} + +// SignResuest is the handler of creating seed in swagger rest server +func SignResuest(gtx *gin.Context) { + name := gtx.Param("name") + var m keySignBody + body, err := ioutil.ReadAll(gtx.Request.Body) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + err = cdc.UnmarshalJSON(body, &m) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + kb, err := GetKeyBase() + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + sig, _, err := kb.Sign(name, m.Password, m.Tx) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + encoded := base64.StdEncoding.EncodeToString(sig) + + httputils.NormalResponse(gtx, []byte(encoded)) +} \ No newline at end of file diff --git a/client/keys/update.go b/client/keys/update.go index 78a81bf0e605..f731bc2216d3 100644 --- a/client/keys/update.go +++ b/client/keys/update.go @@ -10,6 +10,10 @@ import ( "github.com/gorilla/mux" "github.com/spf13/cobra" + "github.com/gin-gonic/gin" + "errors" + "github.com/cosmos/cosmos-sdk/client/httputils" + "io/ioutil" ) func updateKeyCommand() *cobra.Command { @@ -93,3 +97,42 @@ func UpdateKeyRequestHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) } + +// UpdateKeyRequest is the handler of updating specified key in swagger rest server +func UpdateKeyRequest(gtx *gin.Context) { + name := gtx.Param("name") + var m UpdateKeyBody + + body, err := ioutil.ReadAll(gtx.Request.Body) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + err = cdc.UnmarshalJSON(body, &m) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + kb, err := GetKeyBase() + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + if len(m.NewPassword) < 8 || len(m.NewPassword) > 16 { + httputils.NewError(gtx, http.StatusBadRequest, errors.New("account password length should be between 8 and 16")) + return + } + + getNewpass := func() (string, error) { return m.NewPassword, nil } + + // TODO check if account exists and if password is correct + err = kb.Update(name, m.OldPassword, getNewpass) + if err != nil { + httputils.NewError(gtx, http.StatusUnauthorized, err) + return + } + + httputils.NormalResponse(gtx, []byte("success")) +} \ No newline at end of file diff --git a/client/lcd/docs/docs.go b/client/lcd/docs/docs.go new file mode 100644 index 000000000000..490968c9df9f --- /dev/null +++ b/client/lcd/docs/docs.go @@ -0,0 +1,2242 @@ +// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by swaggo/swag at +// 2018-07-09 10:52:33.917498846 +0800 CST m=+0.182625290 + +package docs + +import ( + "github.com/swaggo/swag" + "github.com/spf13/viper" + "encoding/json" + "strings" + "github.com/cosmos/cosmos-sdk/client" + "bytes" + "fmt" + "reflect" + "github.com/tendermint/tendermint/libs/cli" +) + +var doc = `{ + "swagger": "2.0", + "info": { + "description": "All Gaia-lite supported APIs will be shown by this swagger-ui page. You can access these APIs through this page.", + "title": "Gaia-lite Swagger-UI", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "localhost:1317", + "basePath": "/", + "paths": { + "/stake/delegators/{delegatorAddr}": { + "get": { + "description": "Get all delegations (delegation, undelegation and redelegation) from a delegator", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Staking" + ], + "summary": "Get all delegations from a delegator", + "parameters": [ + { + "type": "string", + "description": "delegator address, example: cosmosaccaddr1t48m77vw08fqygkz96l3neqdzrnuvh6ansk7ks", + "name": "delegatorAddr", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/stake.DelegationSummary" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/stake/delegators/{delegatorAddr}/txs": { + "get": { + "description": "Get all staking txs (i.e msgs) from a delegator", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Staking" + ], + "summary": "Get all staking txs from a delegator", + "parameters": [ + { + "type": "string", + "description": "delegator address, example: cosmosaccaddr1t48m77vw08fqygkz96l3neqdzrnuvh6ansk7ks", + "name": "delegatorAddr", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/stake.txInfoArray" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/stake/delegators/{delegatorAddr}/validators": { + "get": { + "description": "Query all validators that a delegator is bonded to", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Staking" + ], + "summary": "Query all validators that a delegator is bonded to", + "parameters": [ + { + "type": "string", + "description": "delegator address, example: cosmosaccaddr1t48m77vw08fqygkz96l3neqdzrnuvh6ansk7ks", + "name": "delegatorAddr", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/stake.BechValidatorArray" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/stake/delegators/{delegatorAddr}/validators/{validatorAddr}": { + "get": { + "description": "Query a validator that a delegator is bonded to", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Staking" + ], + "summary": "Query a validator that a delegator is bonded to", + "parameters": [ + { + "type": "string", + "description": "delegator address, example: cosmosaccaddr1t48m77vw08fqygkz96l3neqdzrnuvh6ansk7ks", + "name": "delegatorAddr", + "in": "path" + }, + { + "type": "string", + "description": "validator address, example: cosmosaccaddr1t48m77vw08fqygkz96l3neqdzrnuvh6ansk7ks", + "name": "validatorAddr", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/stake.BechValidator" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/stake/delegators/{delegatorAddr}/delegations/{validatorAddr}": { + "get": { + "description": "Query a delegation between a delegator and a validator", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Staking" + ], + "summary": "Query a delegation between a delegator and a validator", + "parameters": [ + { + "type": "string", + "description": "delegator address, example: cosmosaccaddr1t48m77vw08fqygkz96l3neqdzrnuvh6ansk7ks", + "name": "delegatorAddr", + "in": "path" + }, + { + "type": "string", + "description": "validator address, example: cosmosaccaddr1t48m77vw08fqygkz96l3neqdzrnuvh6ansk7ks", + "name": "validatorAddr", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/stake.DelegationWithoutRat" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/stake/delegators/{delegatorAddr}/unbonding_delegations/{validatorAddr}": { + "get": { + "description": "Query all unbonding_delegations between a delegator and a validator", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Staking" + ], + "summary": "Query all unbonding_delegations between a delegator and a validator", + "parameters": [ + { + "type": "string", + "description": "delegator address, example: cosmosaccaddr1t48m77vw08fqygkz96l3neqdzrnuvh6ansk7ks", + "name": "delegatorAddr", + "in": "path" + }, + { + "type": "string", + "description": "validator address, example: cosmosaccaddr1t48m77vw08fqygkz96l3neqdzrnuvh6ansk7ks", + "name": "validatorAddr", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/stake.UnbondingDelegationArray" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/stake/validators": { + "get": { + "description": "Get all validators", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Staking" + ], + "summary": "Get all validators", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/stake.BechValidatorArray" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/stake/validators/{addr}": { + "get": { + "description": "Get a single validator info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Staking" + ], + "summary": "Get a single validator info", + "parameters": [ + { + "type": "string", + "description": "validator address, example: cosmosaccaddr1t48m77vw08fqygkz96l3neqdzrnuvh6ansk7ks", + "name": "addr", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/stake.BechValidator" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/stake/pool": { + "get": { + "description": "Query the staking pool information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Staking" + ], + "summary": "Query the staking pool information", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/stake.pool" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/stake/parameters": { + "get": { + "description": "Query the staking params values", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Staking" + ], + "summary": "Query the staking params values", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/stake.params" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/stake/delegators/{delegatorAddr}/delegations": { + "post": { + "description": "Send stake related transactions", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Staking" + ], + "summary": "Send stake related transaction", + "parameters": [ + { + "type": "string", + "description": "delegator address, example: cosmosaccaddr1t48m77vw08fqygkz96l3neqdzrnuvh6ansk7ks", + "name": "delegatorAddr", + "in": "path" + }, + { + "description": "delegation parameters", + "name": "EditDelegationsBody", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/stake.EditDelegationsBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/stake.transactionResult" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/bank/balances/{address}": { + "get": { + "description": "Get the detailed information for specific address", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Bank" + ], + "summary": "Query account information", + "parameters": [ + { + "type": "string", + "description": "address", + "name": "address", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/sdk.Coins" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/bank/transfers": { + "post": { + "description": "This API require the Gaia-lite has keystore module. It will ask keystore module for transaction signature", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Bank" + ], + "summary": "Send coins to a address", + "parameters": [ + { + "description": "transfer asset", + "name": "sendAsset", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/bank.transferBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/bank.ResultBroadcastTxCommit" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/keys": { + "get": { + "description": "Get all keys in the key store", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Key Management" + ], + "summary": "list all keys", + "operationId": "queryKeysRequest", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/keys.KeyOutputs" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + }, + "post": { + "description": "Create a new key and persistent it to the key store", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Key Management" + ], + "summary": "Create a account", + "parameters": [ + { + "description": "name and password for a new key", + "name": "nameAndPwd", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/keys.NewKeyBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/keys.NewKeyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/keys/{name}/recover": { + "post": { + "description": "Recover a key from seed", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Key Management" + ], + "parameters": [ + { + "type": "string", + "description": "key name", + "name": "name", + "in": "path" + }, + { + "description": "seed and password for a new key", + "name": "seedAndPwd", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/keys.RecoverKeyBody" + } + } + ], + "summary": "Recover a key from seed", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/keys.KeyOutput" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/keys/{name}/sign": { + "post": { + "description": "Sign user specified bytes array", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Key Management" + ], + "parameters": [ + { + "type": "string", + "description": "key name", + "name": "name", + "in": "path" + }, + { + "description": "seed and password for a new key", + "name": "txAndPwd", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/keys.SignBody" + } + } + ], + "summary": "Sign user specified bytes array", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/keys/{name}": { + "get": { + "description": "Get detailed information for specific key name", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Key Management" + ], + "summary": "Get key information", + "parameters": [ + { + "type": "string", + "description": "key name", + "name": "name", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/keys.KeyOutput" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + }, + "put": { + "description": "The keys are protected by the password, here this API provides a way to change the password", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Key Management" + ], + "summary": "Change key password", + "parameters": [ + { + "type": "string", + "description": "key name", + "name": "name", + "in": "path" + }, + { + "description": "key name", + "name": "pwd", + "in": "body", + "schema": { + "type": "object", + "$ref": "#/definitions/keys.UpdateKeyBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + }, + "delete": { + "description": "delete specific name", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Key Management" + ], + "summary": "Delete key", + "parameters": [ + { + "type": "string", + "description": "key name", + "name": "name", + "in": "path" + }, + { + "description": "password", + "name": "pwd", + "in": "body", + "schema": { + "type": "object", + "$ref": "#/definitions/keys.DeleteKeyBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/auth/accounts/{address}": { + "get": { + "description": "Get the detailed information for specific address", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Key Management" + ], + "summary": "Query account information", + "parameters": [ + { + "type": "string", + "description": "address", + "name": "address", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/auth.BaseAccount" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/txs": { + "post": { + "description": "Broadcast transaction", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Transaction" + ], + "parameters": [ + { + "description": "seed and password for a new key", + "name": "txAndReturn", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/tx.TxBody" + } + } + ], + "summary": "Broadcast transaction", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/bank.ResultBroadcastTxCommit" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/node_version": { + "get": { + "description": "Get connected full node version", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "General" + ], + "summary": "Get connected full node version", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + }, + "/version": { + "get": { + "description": "Get Gaia-lite version", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "General" + ], + "summary": "Get Gaia-lite version", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/httputil.HTTPError" + } + } + } + } + } + }, + "definitions": { + "auth.BaseAccount": { + "type": "object", + "properties": { + "account_number": { + "type": "integer" + }, + "address": { + "type": "string" + }, + "coins": { + "type": "object", + "$ref": "#/definitions/sdk.Coins" + }, + "public_key": { + "type": "string" + }, + "sequence": { + "type": "integer" + } + } + }, + "bank.ResponseCheckTx": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": { + "type": "string" + }, + "gas_used": { + "type": "integer" + }, + "gas_wanted": { + "type": "integer" + }, + "info": { + "type": "string" + }, + "log": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/common.KVPairs" + } + } + } + }, + "bank.ResponseDeliverTx": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": { + "type": "string" + }, + "gas_used": { + "type": "integer" + }, + "gas_wanted": { + "type": "integer" + }, + "info": { + "type": "string" + }, + "log": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/common.KVPairs" + } + } + } + }, + "bank.ResultBroadcastTxCommit": { + "type": "object", + "properties": { + "check_tx": { + "type": "object", + "$ref": "#/definitions/bank.ResponseCheckTx" + }, + "deliver_tx": { + "type": "object", + "$ref": "#/definitions/bank.ResponseDeliverTx" + }, + "hash": { + "type": "string" + }, + "height": { + "type": "integer" + } + } + }, + "bank.signedBody": { + "type": "object", + "properties": { + "public_key_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "signature_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "transaction_data": { + "type": "string" + } + } + }, + "bank.transferBody": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "to_address": { + "type": "string" + }, + "from_address": { + "type": "string" + }, + "amount": { + "type": "object", + "$ref": "#/definitions/sdk.Coins" + }, + "password": { + "type": "string" + }, + "chain_id": { + "type": "string" + }, + "account_number": { + "type": "string" + }, + "gas": { + "type": "string" + }, + "fee": { + "type": "string", + "example": "1steak" + }, + "sequence": { + "type": "string" + }, + "generate": { + "type": "boolean", + "example": false + }, + "ensure_account_sequence": { + "type": "boolean", + "example": false + } + } + }, + "sdk.Coin": { + "type": "object", + "properties": { + "denom": { + "type": "string" + }, + "amount": { + "type": "string" + } + } + }, + "sdk.Coins": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/sdk.Coin" + } + }, + "common.KVPair": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + }, + "common.KVPairs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/common.KVPair" + } + }, + "common.KI64Pair": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + }, + "keys.DeleteKeyBody": { + "type": "object", + "properties": { + "password": { + "type": "string" + } + } + }, + "keys.KeyOutput": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "name": { + "type": "string" + }, + "pub_key": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "keys.KeyOutputs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/keys.KeyOutput" + } + }, + "keys.NewKeyResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "pub_key": { + "type": "string" + }, + "seed": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "keys.NewKeyBody": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "seed": { + "type": "string" + } + } + }, + "keys.RecoverKeyBody": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "seed": { + "type": "string" + } + } + }, + "keys.SignBody": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "tx_bytes": { + "type": "string" + } + } + }, + "keys.UpdateKeyBody": { + "type": "object", + "properties": { + "new_password": { + "type": "string" + }, + "old_password": { + "type": "string" + } + } + }, + "tx.TxBody": { + "type": "object", + "properties": { + "transaction": { + "type": "string" + }, + "return": { + "type": "string", + "example": "block" + } + } + }, + "httputil.HTTPError": { + "type": "object", + "properties": { + "rest api": { + "type": "string", + "example": "2.0" + }, + "code": { + "type": "integer", + "example": 500 + }, + "error message": { + "type": "string" + } + } + }, + "stake.msgDelegationsInput": { + "type": "object", + "properties": { + "delegator_addr": { + "type": "string" + }, + "validator_addr": { + "type": "string" + }, + "delegation": { + "type": "object", + "$ref": "#/definitions/sdk.Coin" + } + } + }, + "stake.msgBeginUnbondingInput": { + "type": "object", + "properties": { + "delegator_addr": { + "type": "string" + }, + "validator_addr": { + "type": "string" + }, + "shares": { + "type": "string" + } + } + }, + "stake.msgCompleteUnbondingInput": { + "type": "object", + "properties": { + "delegator_addr": { + "type": "string" + }, + "validator_addr": { + "type": "string" + } + } + }, + "stake.msgBeginRedelegateInput": { + "type": "object", + "properties": { + "delegator_addr": { + "type": "string" + }, + "validator_src_addr": { + "type": "string" + }, + "validator_dst_addr": { + "type": "string" + }, + "shares": { + "type": "string" + } + } + }, + "stake.msgCompleteRedelegateInput": { + "type": "object", + "properties": { + "delegator_addr": { + "type": "string" + }, + "validator_src_addr": { + "type": "string" + }, + "validator_dst_addr": { + "type": "string" + } + } + }, + "stake.EditDelegationsBody": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "chain_id": { + "type": "string" + }, + "account_number": { + "type": "integer" + }, + "sequence": { + "type": "integer" + }, + "gas": { + "type": "integer" + }, + "delegations": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/stake.msgDelegationsInput" + } + }, + "begin_unbondings": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/stake.msgBeginUnbondingInput" + } + }, + "complete_unbondings": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/stake.msgCompleteUnbondingInput" + } + }, + "begin_redelegates": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/stake.msgBeginRedelegateInput" + } + }, + "complete_redelegates": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/stake.msgCompleteRedelegateInput" + } + } + } + }, + "stake.transactionResult": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/bank.ResultBroadcastTxCommit" + } + }, + "stake.UnbondingDelegation": { + "type": "object", + "properties": { + "delegator_addr": { + "type": "string" + }, + "validator_addr": { + "type": "string" + }, + "initial_balance": { + "type": "object", + "$ref": "#/definitions/sdk.Coin" + }, + "balance": { + "type": "object", + "$ref": "#/definitions/sdk.Coin" + }, + "creation_height": { + "type": "integer" + }, + "min_time": { + "type": "integer" + } + } + }, + "stake.UnbondingDelegationArray": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/stake.UnbondingDelegation" + } + }, + "stake.Redelegation": { + "type": "object", + "properties": { + "delegator_addr": { + "type": "string" + }, + "validator_src_addr": { + "type": "string" + }, + "validator_dst_addr": { + "type": "string" + }, + "creation_height": { + "type": "integer" + }, + "min_time": { + "type": "integer" + }, + "initial_balance": { + "type": "object", + "$ref": "#/definitions/sdk.Coin" + }, + "balance": { + "type": "object", + "$ref": "#/definitions/sdk.Coin" + }, + "shares_src": { + "type": "string" + }, + "shares_dst": { + "type": "string" + } + } + }, + "stake.RedelegationArray": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/stake.Redelegation" + } + }, + "stake.Description": { + "type": "object", + "properties": { + "moniker": { + "type": "string" + }, + "identity": { + "type": "string" + }, + "website": { + "type": "string" + }, + "details": { + "type": "string" + } + } + }, + "stake.BechValidator": { + "type": "object", + "properties": { + "operator": { + "type": "string" + }, + "pub_key": { + "type": "string" + }, + "revoked": { + "type": "boolean" + }, + "status": { + "type": "integer" + }, + "tokens": { + "type": "string" + }, + "delegator_shares": { + "type": "string" + }, + "description": { + "type": "object", + "$ref": "#/definitions/stake.Description" + }, + "bond_height": { + "type": "integer" + }, + "bond_intra_tx_counter": { + "type": "integer" + }, + "proposer_reward_pool": { + "type": "object", + "$ref": "#/definitions/sdk.Coins" + }, + "commission": { + "type": "string" + }, + "commission_max": { + "type": "string" + }, + "commission_change_rate": { + "type": "string" + }, + "commission_change_today": { + "type": "string" + }, + "prev_bonded_shares": { + "type": "string" + } + } + }, + "stake.BechValidatorArray": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/stake.BechValidator" + } + }, + "stake.DelegationWithoutRat": { + "type": "object", + "properties": { + "delegator_addr": { + "type": "string" + }, + "validator_addr": { + "type": "string" + }, + "shares": { + "type": "string" + }, + "height": { + "type": "integer" + } + } + }, + "stake.DelegationWithoutRatArray": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/stake.DelegationWithoutRat" + } + }, + "stake.DelegationSummary": { + "type": "object", + "properties": { + "redelegations": { + "type": "object", + "$ref": "#/definitions/stake.RedelegationArray" + }, + "unbonding_delegations": { + "type": "object", + "$ref": "#/definitions/stake.UnbondingDelegationArray" + }, + "delegations": { + "type": "object", + "$ref": "#/definitions/stake.DelegationWithoutRatArray" + } + } + }, + "stake.txInfo": { + "type": "object", + "properties": { + "hash": { + "type": "string" + }, + "height": { + "type": "integer" + }, + "tx": { + "type": "string" + }, + "result": { + "type": "object", + "$ref": "#/definitions/bank.ResponseDeliverTx" + } + } + }, + "stake.txInfoArray": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/stake.txInfo" + } + }, + "stake.pool": { + "type": "object", + "properties": { + "loose_tokens": { + "type": "string" + }, + "bonded_tokens": { + "type": "integer" + }, + "inflation_last_time": { + "type": "string" + }, + "inflation": { + "type": "string" + }, + "date_last_commission_reset": { + "type": "integer" + }, + "prev_bonded_shares": { + "type": "string" + } + } + }, + "stake.params": { + "type": "object", + "properties": { + "inflation_rate_change": { + "type": "string" + }, + "inflation_max": { + "type": "string" + }, + "inflation_min": { + "type": "string" + }, + "goal_bonded": { + "type": "string" + }, + "unbonding_time": { + "type": "string" + }, + "max_validators": { + "type": "integer" + }, + "bond_denom": { + "type": "string" + } + } + } + } +}` + +var tagToModuleDesc = ` +{ + "General":"general", + "Key Management":"key", + "Bank":"bank", + "Staking":"staking", + "Transaction":"transaction" +} +` + +type s struct{} + +func addOptionsToDesc (desc string) string { + home := viper.GetString(cli.HomeFlag) + listenAddr := viper.GetString(client.FlagListenAddr) + swaggerHost := viper.GetString(client.FlagSwaggerHostIP) + nodeList := viper.GetString(client.FlagNodeList) + chainID := viper.GetString(client.FlagChainID) + trustNode := viper.GetString(client.FlagTrustNode) + modules := viper.GetString(client.FlagModules) + + var buffer bytes.Buffer + buffer.WriteString(desc) + buffer.WriteString("\n") + + buffer.WriteString("Gaid-lite starting options:") + buffer.WriteString("\n") + + buffer.WriteString(cli.HomeFlag) + buffer.WriteString(": ") + buffer.WriteString(home) + buffer.WriteString("\n") + + buffer.WriteString(client.FlagListenAddr) + buffer.WriteString(": ") + buffer.WriteString(listenAddr) + buffer.WriteString("\n") + + buffer.WriteString(client.FlagSwaggerHostIP) + buffer.WriteString(": ") + buffer.WriteString(swaggerHost) + buffer.WriteString("\n") + + buffer.WriteString(client.FlagNodeList) + buffer.WriteString(": ") + buffer.WriteString(nodeList) + buffer.WriteString("\n") + + buffer.WriteString(client.FlagChainID) + buffer.WriteString(": ") + buffer.WriteString(chainID) + buffer.WriteString("\n") + + buffer.WriteString(client.FlagTrustNode) + buffer.WriteString(": ") + buffer.WriteString(trustNode) + buffer.WriteString("\n") + + buffer.WriteString(client.FlagModules) + buffer.WriteString(": ") + buffer.WriteString(modules) + buffer.WriteString("\n") + + return buffer.String() +} + +func moduleEnabled(modules []string, name string) bool { + for _, moduleName := range modules { + if moduleName == name { + return true + } + } + return false +} + +func modularizeAPIs(modules string, paths map[string]interface{}) map[string]interface{} { + filteredAPIs := make(map[string]interface{}) + + var moduleToTag map[string]string + if err := json.Unmarshal([]byte(tagToModuleDesc), &moduleToTag); err != nil { + panic(err) + } + moduleArray := strings.Split(modules,",") + + for path,operations := range paths { + if reflect.TypeOf(operations).String() != "map[string]interface {}" { + panic(fmt.Errorf("unexpected data type, expected: map[string]interface {}, got: %s", + reflect.TypeOf(operations).String())) + } + operationAPIs := operations.(map[string]interface{}) + for operation,API := range operationAPIs { + if reflect.TypeOf(API).String() != "map[string]interface {}" { + panic(fmt.Errorf("unexpected data type, expected: map[string]interface {}, got: %s", + reflect.TypeOf(API).String())) + } + APIInfo := API.(map[string]interface{}) + tags := APIInfo["tags"].([]interface{}) + if len(tags) != 1 { + panic(fmt.Errorf("only support one tag, got %d tags",len(tags))) + } + + if reflect.TypeOf(tags[0]).String() != "string" { + panic(fmt.Errorf("unexpected data type, expected: string, got: %s", + reflect.TypeOf(tags[0]).String())) + } + moduleName := moduleToTag[tags[0].(string)] + enable := moduleEnabled(moduleArray,moduleName) + + if enable { + if filteredAPIs[path] != nil { + originalOperations := filteredAPIs[path].(map[string]interface{}) + originalOperations[operation]=API + filteredAPIs[path] = originalOperations + } else { + originalOperations := make(map[string]interface{}) + originalOperations[operation]=API + filteredAPIs[path] = originalOperations + } + } + } + } + return filteredAPIs +} + +func (s *s) ReadDoc() string { + listenAddr := viper.GetString(client.FlagListenAddr) + swaggerHost := viper.GetString(client.FlagSwaggerHostIP) + modules := viper.GetString(client.FlagModules) + + var docs map[string]interface{} + if err := json.Unmarshal([]byte(doc), &docs); err != nil { + panic(err) + } + + addrInfo := strings.Split(listenAddr,":") + if len(addrInfo) != 2{ + panic(fmt.Errorf("invalid listen address")) + } + listenPort := addrInfo[1] + docs["host"] = swaggerHost + ":" + listenPort + + if reflect.TypeOf(docs["info"]).String() != "map[string]interface {}" { + panic(fmt.Errorf("unexpected data type, expected: map[string]interface {}, got: %s", + reflect.TypeOf(docs["info"]).String())) + } + infos := docs["info"].(map[string]interface{}) + description := infos["description"].(string) + + infos["description"] = addOptionsToDesc(description) + docs["info"] = infos + + if reflect.TypeOf(docs["paths"]).String() != "map[string]interface {}" { + panic(fmt.Errorf("unexpected data type, expected: map[string]interface {}, got: %s", + reflect.TypeOf(docs["paths"]).String())) + } + paths := docs["paths"].(map[string]interface{}) + docs["paths"] = modularizeAPIs(modules,paths) + + docString,err := json.Marshal(docs) + if err != nil { + panic(err) + } + + return string(docString) +} +func init() { + swag.Register(swag.Name, &s{}) +} \ No newline at end of file diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 298cf0027e47..5941a4e1800a 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -29,6 +29,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/slashing" "github.com/cosmos/cosmos-sdk/x/stake" "github.com/cosmos/cosmos-sdk/x/stake/client/rest" + "encoding/json" ) func init() { @@ -115,6 +116,92 @@ func TestKeys(t *testing.T) { require.Equal(t, http.StatusOK, res.StatusCode, body) } +func TestKeysSwaggerLCD(t *testing.T) { + name, password := "test", "1234567890" + addr, seed := CreateAddr(t, "test", password, GetKeyBase(t)) + cleanup, _, port := InitializeTestSwaggerLCD(t, 1, []sdk.AccAddress{addr}) + defer cleanup() + + // get seed + // TODO Do we really need this endpoint? + recoverKeyURL := fmt.Sprintf("/keys/%s/recover", "test_recover") + seedRecover := "divorce meat banana embody near until uncover wait uniform capital crawl test praise cloud foil monster garbage hedgehog wrong skate there bonus box odor" + passwordRecover := "1234567890" + jsonStrRecover := []byte(fmt.Sprintf(`{"seed":"%s", "password":"%s"}`, seedRecover, passwordRecover)) + res, body := Request(t, port, "POST", recoverKeyURL, jsonStrRecover) + require.Equal(t, http.StatusOK, res.StatusCode, body) + reg, err := regexp.Compile(`([a-z]+ ){12}`) + require.Nil(t, err) + match := reg.MatchString(seed) + require.True(t, match, "Returned seed has wrong format", seed) + + newName := "test_newname" + newPassword := "0987654321" + + // add key + jsonStr := []byte(fmt.Sprintf(`{"name":"%s", "password":"%s", "seed":"%s"}`, newName, newPassword, seed)) + res, body = Request(t, port, "POST", "/keys", jsonStr) + + require.Equal(t, http.StatusOK, res.StatusCode, body) + var keyOutput keys.KeyOutput + err = json.Unmarshal([]byte(body), &keyOutput) + require.Nil(t, err, body) + + addr2Bech32 := keyOutput.Address.String() + _, err = sdk.AccAddressFromBech32(addr2Bech32) + require.NoError(t, err, "Failed to return a correct bech32 address") + + // test if created account is the correct account + expectedInfo, _ := GetKeyBase(t).CreateKey(newName, seed, newPassword) + expectedAccount := sdk.AccAddress(expectedInfo.GetPubKey().Address().Bytes()) + assert.Equal(t, expectedAccount.String(), addr2Bech32) + + // existing keys + res, body = Request(t, port, "GET", "/keys", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + var m [3]keys.KeyOutput + err = cdc.UnmarshalJSON([]byte(body), &m) + require.Nil(t, err) + + addrBech32 := addr.String() + + require.Equal(t, name, m[0].Name, "Did not serve keys name correctly") + require.Equal(t, addrBech32, m[0].Address.String(), "Did not serve keys Address correctly") + require.Equal(t, newName, m[1].Name, "Did not serve keys name correctly") + require.Equal(t, addr2Bech32, m[1].Address.String(), "Did not serve keys Address correctly") + + // select key + keyEndpoint := fmt.Sprintf("/keys/%s", newName) + res, body = Request(t, port, "GET", keyEndpoint, nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + var m2 keys.KeyOutput + err = cdc.UnmarshalJSON([]byte(body), &m2) + require.Nil(t, err) + + require.Equal(t, newName, m2.Name, "Did not serve keys name correctly") + require.Equal(t, addr2Bech32, m2.Address.String(), "Did not serve keys Address correctly") + + // update key + jsonStr = []byte(fmt.Sprintf(`{ + "old_password":"%s", + "new_password":"12345678901" + }`, newPassword)) + + keyEndpoint = fmt.Sprintf("/keys/%s", newName) + res, body = Request(t, port, "PUT", keyEndpoint, jsonStr) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + // here it should say unauthorized as we changed the password before + res, body = Request(t, port, "PUT", keyEndpoint, jsonStr) + require.Equal(t, http.StatusUnauthorized, res.StatusCode, body) + + // delete key + jsonStr = []byte(`{"password":"12345678901"}`) + res, body = Request(t, port, "DELETE", keyEndpoint, jsonStr) + require.Equal(t, http.StatusOK, res.StatusCode, body) +} + func TestVersion(t *testing.T) { cleanup, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}) defer cleanup() @@ -138,6 +225,29 @@ func TestVersion(t *testing.T) { require.True(t, match, body) } +func TestVersionSwaggerLCD(t *testing.T) { + cleanup, _, port := InitializeTestSwaggerLCD(t, 1, []sdk.AccAddress{}) + defer cleanup() + + // node info + res, body := Request(t, port, "GET", "/version", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + reg, err := regexp.Compile(`\d+\.\d+\.\d+(-dev)?`) + require.Nil(t, err) + match := reg.MatchString(body) + require.True(t, match, body) + + // node info + res, body = Request(t, port, "GET", "/node_version", nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + reg, err = regexp.Compile(`\d+\.\d+\.\d+(-dev)?`) + require.Nil(t, err) + match = reg.MatchString(body) + require.True(t, match, body) +} + func TestNodeStatus(t *testing.T) { cleanup, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}) defer cleanup() @@ -234,7 +344,7 @@ func TestCoinSend(t *testing.T) { someFakeAddr := sdk.AccAddress(bz) // query empty - res, body := Request(t, port, "GET", fmt.Sprintf("/accounts/%s", someFakeAddr), nil) + res, body := Request(t, port, "GET", fmt.Sprintf("/auth/accounts/%s", someFakeAddr), nil) require.Equal(t, http.StatusNoContent, res.StatusCode, body) acc := getAccount(t, port, addr) @@ -273,6 +383,50 @@ func TestCoinSend(t *testing.T) { require.Equal(t, http.StatusOK, res.StatusCode, body) } +func TestCoinSendSwaggerLCD(t *testing.T) { + name, password := "test", "1234567890" + addr, seed := CreateAddr(t, "test", password, GetKeyBase(t)) + cleanup, _, port := InitializeTestSwaggerLCD(t, 1, []sdk.AccAddress{addr}) + defer cleanup() + + bz, err := hex.DecodeString("8FA6AB57AD6870F6B5B2E57735F38F2F30E73CB6") + require.NoError(t, err) + someFakeAddr := sdk.AccAddress(bz) + + // query empty + res, body := Request(t, port, "GET", fmt.Sprintf("/auth/accounts/%s", someFakeAddr), nil) + require.Equal(t, http.StatusNoContent, res.StatusCode, body) + + acc := getAccount(t, port, addr) + initialBalance := acc.GetCoins() + + // create TX + receiveAddr, resultTx := doSend(t, port, seed, name, password, addr) + // TODO current lcd rest server with swagger doesn't implement block interface + //tests.WaitForHeight(resultTx.Height+1, port) + time.Sleep(5 * time.Second) + + // check if tx was committed + require.Equal(t, uint32(0), resultTx.CheckTx.Code) + require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + + // query sender + acc = getAccount(t, port, addr) + coins := acc.GetCoins() + mycoins := coins[0] + + require.Equal(t, "steak", mycoins.Denom) + require.Equal(t, initialBalance[0].Amount.SubRaw(1), mycoins.Amount) + + // query receiver + acc = getAccount(t, port, receiveAddr) + coins = acc.GetCoins() + mycoins = coins[0] + + require.Equal(t, "steak", mycoins.Denom) + require.Equal(t, int64(1), mycoins.Amount.Int64()) +} + func TestIBCTransfer(t *testing.T) { name, password := "test", "1234567890" addr, seed := CreateAddr(t, "test", password, GetKeyBase(t)) @@ -414,6 +568,23 @@ func TestValidatorsQuery(t *testing.T) { require.True(t, foundVal, "pkBech %v, operator %v", pkBech, validators[0].Operator) } +func TestValidatorsQueryFromSwaggerLCD(t *testing.T) { + cleanup, pks, port := InitializeTestSwaggerLCD(t, 1, []sdk.AccAddress{}) + defer cleanup() + require.Equal(t, 1, len(pks)) + + validators := getValidators(t, port) + require.Equal(t, len(validators), 1) + + // make sure all the validators were found (order unknown because sorted by operator addr) + foundVal := false + pkBech := sdk.MustBech32ifyValPub(pks[0]) + if validators[0].PubKey == pkBech { + foundVal = true + } + require.True(t, foundVal, "pkBech %v, operator %v", pkBech, validators[0].Operator) +} + func TestValidatorQuery(t *testing.T) { cleanup, pks, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}) defer cleanup() @@ -424,6 +595,16 @@ func TestValidatorQuery(t *testing.T) { assert.Equal(t, validator.Operator, validator1Operator, "The returned validator does not hold the correct data") } +func TestValidatorQueryFromSwaggerLCD(t *testing.T) { + cleanup, pks, port := InitializeTestSwaggerLCD(t, 1, []sdk.AccAddress{}) + defer cleanup() + require.Equal(t, 1, len(pks)) + + validator1Operator := sdk.AccAddress(pks[0].Address()) + validator := getValidator(t, port, validator1Operator) + assert.Equal(t, validator.Operator, validator1Operator, "The returned validator does not hold the correct data") +} + func TestBonding(t *testing.T) { name, password, denom := "test", "1234567890", "steak" addr, seed := CreateAddr(t, name, password, GetKeyBase(t)) @@ -509,6 +690,95 @@ func TestBonding(t *testing.T) { assert.Len(t, txs, 1, "All unbonding txs found") } +func TestBondingFromSwaggerLCD(t *testing.T) { + name, password, denom := "test", "1234567890", "steak" + addr, seed := CreateAddr(t, name, password, GetKeyBase(t)) + cleanup, pks, port := InitializeTestSwaggerLCD(t, 1, []sdk.AccAddress{addr}) + defer cleanup() + + validator1Operator := sdk.AccAddress(pks[0].Address()) + validator := getValidator(t, port, validator1Operator) + + // create bond TX + resultTx := doDelegate(t, port, seed, name, password, addr, validator1Operator, 60) + // TODO current lcd rest server with swagger doesn't implement block interface + //tests.WaitForHeight(resultTx.Height+1, port) + time.Sleep(6 * time.Second) + + require.Equal(t, uint32(0), resultTx.CheckTx.Code) + require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + + acc := getAccount(t, port, addr) + coins := acc.GetCoins() + + require.Equal(t, int64(40), coins.AmountOf(denom).Int64()) + + // query validator + bond := getDelegation(t, port, addr, validator1Operator) + require.Equal(t, "60.0000000000", bond.Shares) + + summary := getDelegationSummary(t, port, addr) + + require.Len(t, summary.Delegations, 1, "Delegation summary holds all delegations") + require.Equal(t, "60.0000000000", summary.Delegations[0].Shares) + require.Len(t, summary.UnbondingDelegations, 0, "Delegation summary holds all unbonding-delegations") + + bondedValidators := getDelegatorValidators(t, port, addr) + require.Len(t, bondedValidators, 1) + require.Equal(t, validator1Operator, bondedValidators[0].Operator) + require.Equal(t, validator.DelegatorShares.Add(sdk.NewDec(60)).String(), bondedValidators[0].DelegatorShares.String()) + + bondedValidator := getDelegatorValidator(t, port, addr, validator1Operator) + require.Equal(t, validator1Operator, bondedValidator.Operator) + + ////////////////////// + // testing unbonding + + // create unbond TX + resultTx = doBeginUnbonding(t, port, seed, name, password, addr, validator1Operator, 60) + // TODO current lcd rest server with swagger doesn't implement block interface + //tests.WaitForHeight(resultTx.Height+1, port) + time.Sleep(6 * time.Second) + + require.Equal(t, uint32(0), resultTx.CheckTx.Code) + require.Equal(t, uint32(0), resultTx.DeliverTx.Code) + + // sender should have not received any coins as the unbonding has only just begun + acc = getAccount(t, port, addr) + coins = acc.GetCoins() + require.Equal(t, int64(40), coins.AmountOf("steak").Int64()) + + unbondings := getUndelegations(t, port, addr, validator1Operator) + require.Len(t, unbondings, 1, "Unbondings holds all unbonding-delegations") + require.Equal(t, "60", unbondings[0].Balance.Amount.String()) + + summary = getDelegationSummary(t, port, addr) + + require.Len(t, summary.Delegations, 0, "Delegation summary holds all delegations") + require.Len(t, summary.UnbondingDelegations, 1, "Delegation summary holds all unbonding-delegations") + require.Equal(t, "60", summary.UnbondingDelegations[0].Balance.Amount.String()) + + bondedValidators = getDelegatorValidators(t, port, addr) + require.Len(t, bondedValidators, 0, "There's no delegation as the user withdraw all funds") + + // TODO Undonding status not currently implemented + // require.Equal(t, sdk.Unbonding, bondedValidators[0].Status) + + // TODO add redelegation, need more complex capabilities such to mock context and + // TODO check summary for redelegation + // assert.Len(t, summary.Redelegations, 1, "Delegation summary holds all redelegations") + + // query txs + txs := getBondingTxs(t, port, addr, "") + assert.Len(t, txs, 2, "All Txs found") + + txs = getBondingTxs(t, port, addr, "bond") + assert.Len(t, txs, 1, "All bonding txs found") + + txs = getBondingTxs(t, port, addr, "unbond") + assert.Len(t, txs, 1, "All unbonding txs found") +} + func TestSubmitProposal(t *testing.T) { name, password := "test", "1234567890" addr, seed := CreateAddr(t, "test", password, GetKeyBase(t)) @@ -712,7 +982,7 @@ func TestProposalsQuery(t *testing.T) { //_____________________________________________________________________________ // get the account to get the sequence func getAccount(t *testing.T, port string, addr sdk.AccAddress) auth.Account { - res, body := Request(t, port, "GET", fmt.Sprintf("/accounts/%s", addr), nil) + res, body := Request(t, port, "GET", fmt.Sprintf("/auth/accounts/%s", addr), nil) require.Equal(t, http.StatusOK, res.StatusCode, body) var acc auth.Account err := cdc.UnmarshalJSON([]byte(body), &acc) @@ -751,10 +1021,11 @@ func doSendWithGas(t *testing.T, port, seed, name, password string, addr sdk.Acc "account_number":"%d", "sequence":"%d", "amount":[%s], - "chain_id":"%s" - }`, gasStr, name, password, accnum, sequence, coinbz, chainID)) - - res, body = Request(t, port, "POST", fmt.Sprintf("/accounts/%s/send", receiveAddr), jsonStr) + "chain_id":"%s", + "generate": false, + "to_address":"%s" + }`, gasStr, name, password, accnum, sequence, coinbz, chainID, receiveAddr)) + res, body = Request(t, port, "POST", "/bank/transfers", jsonStr) return } diff --git a/client/lcd/root.go b/client/lcd/root.go index bfa62f1cf0aa..c92231b3b4ba 100644 --- a/client/lcd/root.go +++ b/client/lcd/root.go @@ -4,11 +4,11 @@ import ( "net/http" "os" - client "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/context" - keys "github.com/cosmos/cosmos-sdk/client/keys" - rpc "github.com/cosmos/cosmos-sdk/client/rpc" - tx "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/client/rpc" + "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/wire" auth "github.com/cosmos/cosmos-sdk/x/auth/client/rest" bank "github.com/cosmos/cosmos-sdk/x/bank/client/rest" @@ -22,6 +22,13 @@ import ( cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/log" tmserver "github.com/tendermint/tendermint/rpc/lib/server" + "github.com/gin-gonic/gin" + "github.com/swaggo/gin-swagger" + "github.com/swaggo/gin-swagger/swaggerFiles" + "strings" + "github.com/tendermint/tendermint/libs/cli" + tendermintLiteProxy "github.com/tendermint/tendermint/lite/proxy" + "fmt" ) // ServeCommand will generate a long-running rest server @@ -96,3 +103,112 @@ func createHandler(cdc *wire.Codec) http.Handler { return r } + +// ServeSwaggerCommand will generate a long-running rest server +// that exposes functionality similar to the ServeCommand, but it provide swagger-ui +// Which is much friendly for further development +func ServeSwaggerCommand(cdc *wire.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "lite-server", + Short: "Start Gaia-lite (gaia light client daemon), a local REST server with swagger-ui, default url: http://localhost:1317/swagger/index.html", + RunE: func(cmd *cobra.Command, args []string) error { + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)). + With("module", "lite-server") + listenAddr := viper.GetString(client.FlagListenAddr) + //Create rest server + server := gin.New() + createSwaggerHandler(server, cdc) + go server.Run(listenAddr) + + logger.Info("REST server started") + + // Wait forever and cleanup + cmn.TrapSignal(func() { + logger.Info("Closing rest server...") + }) + + return nil + }, + } + + cmd.Flags().String(client.FlagListenAddr, "localhost:1317", "Address for server to listen on.") + cmd.Flags().String(client.FlagNodeList, "tcp://localhost:26657", "Node list to connect to, example: \"tcp://10.10.10.10:26657,tcp://20.20.20.20:26657\".") + cmd.Flags().String(client.FlagChainID, "", "ID of chain we connect to, must be specified.") + cmd.Flags().String(client.FlagSwaggerHostIP, "localhost", "The host IP of the Gaia-lite server, swagger-ui will send request to this host.") + cmd.Flags().String(client.FlagModules, "general,key,bank", "Enabled modules.") + cmd.Flags().Bool(client.FlagTrustNode, false, "Trust full nodes or not.") + + return cmd +} + +func createSwaggerHandler(server *gin.Engine, cdc *wire.Codec) { + rootDir := viper.GetString(cli.HomeFlag) + nodeAddrs := viper.GetString(client.FlagNodeList) + chainID := viper.GetString(client.FlagChainID) + modules := viper.GetString(client.FlagModules) + //Get key store + kb, err := keys.GetKeyBase() + if err != nil { + panic(err) + } + //Split the node list string into multi full node URIs + nodeAddrArray := strings.Split(nodeAddrs,",") + if len(nodeAddrArray) < 1 { + panic(fmt.Errorf("missing node URLs")) + } + //Tendermint certifier can only connect to one full node. Here we assign the first full node to it + certifier, err := tendermintLiteProxy.GetCertifier(chainID, rootDir, nodeAddrArray[0]) + if err != nil { + panic(err) + } + //Create load balancing engine + clientManager, err := context.NewClientManager(nodeAddrs) + if err != nil { + panic(err) + } + //Assign tendermint certifier and load balancing engine to ctx + ctx := context.NewCLIContext().WithCodec(cdc).WithLogger(os.Stdout).WithCertifier(certifier).WithClientManager(clientManager) + + server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + moduleArray := strings.Split(modules,",") + if moduleEnabled(moduleArray,"general") { + server.GET("/version", CLIVersionRequest) + server.GET("/node_version", NodeVersionRequest(ctx)) + } + + if moduleEnabled(moduleArray,"transaction") { + tx.RegisterSwaggerRoutes(server.Group("/"), ctx, cdc) + } + + if moduleEnabled(moduleArray,"key") { + keys.RegisterSwaggerRoutes(server.Group("/")) + auth.RegisterSwaggerRoutes(server.Group("/"), ctx, cdc, "acc") + } + + if moduleEnabled(moduleArray,"bank") { + bank.RegisterSwaggerRoutes(server.Group("/"), ctx, cdc, kb) + } + + if moduleEnabled(moduleArray,"staking") { + stake.RegisterSwaggerRoutes(server.Group("/"), ctx, cdc, kb) + } +/* + if moduleEnabled(moduleArray,"governance") { + gov.RegisterSwaggerRoutes(server.Group("/"), ctx, cdc, kb) + } + + if moduleEnabled(moduleArray,"slashing") { + slashing.RegisterSwaggerRoutes(server.Group("/"), ctx, cdc, kb) + } +*/ +} + +func moduleEnabled(modules []string, name string) bool { + for _, moduleName := range modules { + if moduleName == name { + return true + } + } + return false +} \ No newline at end of file diff --git a/client/lcd/test_helpers.go b/client/lcd/test_helpers.go index 818eae1e86f6..06c93b0169fc 100644 --- a/client/lcd/test_helpers.go +++ b/client/lcd/test_helpers.go @@ -37,6 +37,8 @@ import ( "github.com/tendermint/tendermint/proxy" tmrpc "github.com/tendermint/tendermint/rpc/lib/server" tmtypes "github.com/tendermint/tendermint/types" + "github.com/gin-gonic/gin" + "time" ) // makePathname creates a unique pathname for each test. It will panic if it @@ -206,6 +208,106 @@ func InitializeTestLCD(t *testing.T, nValidators int, initAddrs []sdk.AccAddress return cleanup, validatorsPKs, port } +// InitializeTestSwaggerLCD starts Tendermint and the LCD swagger rest server in process, listening on +// their respective sockets where nValidators is the total number of validators +// and initAddrs are the accounts to initialize with some steak tokens. It +// returns a cleanup function, a set of validator public keys, and a port. +func InitializeTestSwaggerLCD(t *testing.T, nValidators int, initAddrs []sdk.AccAddress) (func(), []crypto.PubKey, string) { + config := GetConfig() + config.Consensus.TimeoutCommit = 100 + config.Consensus.SkipTimeoutCommit = false + config.TxIndex.IndexAllTags = true + + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + logger = log.NewFilter(logger, log.AllowError()) + + privValidatorFile := config.PrivValidatorFile() + privVal := pvm.LoadOrGenFilePV(privValidatorFile) + privVal.Reset() + + db := dbm.NewMemDB() + app := gapp.NewGaiaApp(logger, db, nil) + cdc = gapp.MakeCodec() + + genesisFile := config.GenesisFile() + genDoc, err := tmtypes.GenesisDocFromFile(genesisFile) + require.NoError(t, err) + + if nValidators < 1 { + panic("InitializeTestLCD must use at least one validator") + } + + for i := 1; i < nValidators; i++ { + genDoc.Validators = append(genDoc.Validators, + tmtypes.GenesisValidator{ + PubKey: ed25519.GenPrivKey().PubKey(), + Power: 1, + Name: "val", + }, + ) + } + + var validatorsPKs []crypto.PubKey + + // NOTE: It's bad practice to reuse public key address for the owner + // address but doing in the test for simplicity. + var appGenTxs []json.RawMessage + for _, gdValidator := range genDoc.Validators { + pk := gdValidator.PubKey + validatorsPKs = append(validatorsPKs, pk) + + appGenTx, _, _, err := gapp.GaiaAppGenTxNF(cdc, pk, sdk.AccAddress(pk.Address()), "test_val1") + require.NoError(t, err) + + appGenTxs = append(appGenTxs, appGenTx) + } + + genesisState, err := gapp.GaiaAppGenState(cdc, appGenTxs[:]) + require.NoError(t, err) + + // add some tokens to init accounts + for _, addr := range initAddrs { + accAuth := auth.NewBaseAccountWithAddress(addr) + accAuth.Coins = sdk.Coins{sdk.NewInt64Coin("steak", 100)} + acc := gapp.NewGenesisAccount(&accAuth) + genesisState.Accounts = append(genesisState.Accounts, acc) + genesisState.StakeData.Pool.LooseTokens = genesisState.StakeData.Pool.LooseTokens.Add(sdk.NewDec(100)) + } + + appState, err := wire.MarshalJSONIndent(cdc, genesisState) + require.NoError(t, err) + genDoc.AppState = appState + + _, port, err := server.FreeTCPAddr() + require.NoError(t, err) + + // XXX: Need to set this so LCD knows the tendermint node address! + viper.Set(client.FlagNodeList, config.RPC.ListenAddress) + viper.Set(client.FlagChainID, genDoc.ChainID) + viper.Set(client.FlagListenAddr, fmt.Sprintf("localhost:%s",port)) + viper.Set(client.FlagSwaggerHostIP, "localhost") + viper.Set(client.FlagModules, "general,key,bank,staking") + viper.Set(client.FlagTrustNode, false) + + node, err := startTM(config, logger, genDoc, privVal, app) + require.NoError(t, err) + + time.Sleep(2 * time.Second) + startSwaggerLCD(cdc) + time.Sleep(1 * time.Second) + + //tests.WaitForLCDStart(port) + //tests.WaitForHeight(1, port) + + cleanup := func() { + logger.Debug("cleaning up LCD initialization") + node.Stop() + node.Wait() + } + + return cleanup, validatorsPKs, port +} + // startTM creates and starts an in-process Tendermint node with memDB and // in-process ABCI application. It returns the new node or any error that // occurred. @@ -248,6 +350,14 @@ func startLCD(logger log.Logger, listenAddr string, cdc *wire.Codec) (net.Listen return tmrpc.StartHTTPServer(listenAddr, createHandler(cdc), logger, tmrpc.Config{}) } +func startSwaggerLCD(cdc *wire.Codec) { + //Create swagger rest server + ginServer := gin.New() + createSwaggerHandler(ginServer, cdc) + listenAddr := viper.GetString(client.FlagListenAddr) + go ginServer.Run(listenAddr) +} + // Request makes a test LCD test request. It returns a response object and a // stringified response body. func Request(t *testing.T, port, method, path string, payload []byte) (*http.Response, string) { diff --git a/client/lcd/version.go b/client/lcd/version.go index a124388e6dfc..d2f51224bcef 100644 --- a/client/lcd/version.go +++ b/client/lcd/version.go @@ -6,6 +6,9 @@ import ( "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/version" + "github.com/gin-gonic/gin" + "github.com/swaggo/swag/example/celler/httputil" + "github.com/cosmos/cosmos-sdk/client/httputils" ) // cli version REST handler endpoint @@ -27,3 +30,21 @@ func NodeVersionRequestHandler(cliCtx context.CLIContext) http.HandlerFunc { w.Write(version) } } + +// CLIVersionRequest is the handler of getting rest server version +func CLIVersionRequest(gtx *gin.Context) { + v := version.GetVersion() + httputils.NormalResponse(gtx, []byte(v)) +} + +// NodeVersionRequest is the handler of getting connected node version +func NodeVersionRequest(cliCtx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + appVersion, err := cliCtx.Query("/app/version", nil) + if err != nil { + httputil.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("could't query version. error: %s", err.Error())) + return + } + httputils.NormalResponse(gtx, appVersion) + } +} \ No newline at end of file diff --git a/client/tx/broadcast.go b/client/tx/broadcast.go index 89ad48f43f81..cbed52d12660 100644 --- a/client/tx/broadcast.go +++ b/client/tx/broadcast.go @@ -5,8 +5,18 @@ import ( "net/http" "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/client/httputils" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/gin-gonic/gin" + "io/ioutil" + "fmt" ) +const ( + flagSync = "sync" + flagAsync = "async" + flagBlock = "block" +) // Tx Broadcast Body type BroadcastTxBody struct { TxBytes string `json:"tx"` @@ -35,3 +45,67 @@ func BroadcastTxRequestHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { w.Write([]byte(string(res.Height))) } } + +// BroadcastBody contains the data of tx and specify how to broadcast tx +type BroadcastBody struct { + Transaction string `json:"transaction"` + Return string `json:"return"` +} + +// BroadcastTxRequest REST Handler +// nolint: gocyclo +func BroadcastTxRequest(cdc *wire.Codec, ctx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + var txBody BroadcastBody + body, err := ioutil.ReadAll(gtx.Request.Body) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + err = cdc.UnmarshalJSON(body, &txBody) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + var output []byte + switch txBody.Return { + case flagBlock: + res, err := ctx.BroadcastTx([]byte(txBody.Transaction)) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + output, err = cdc.MarshalJSON(res) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + case flagSync: + res, err := ctx.BroadcastTxSync([]byte(txBody.Transaction)) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + output, err = cdc.MarshalJSON(res) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + case flagAsync: + res, err := ctx.BroadcastTxAsync([]byte(txBody.Transaction)) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + output, err = cdc.MarshalJSON(res) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + default: + httputils.NewError(gtx, http.StatusBadRequest, fmt.Errorf("unsupported return type. supported types: block, sync, async")) + return + } + httputils.NormalResponse(gtx,output) + } +} \ No newline at end of file diff --git a/client/tx/root.go b/client/tx/root.go index 7e18d5aae126..17341f830599 100644 --- a/client/tx/root.go +++ b/client/tx/root.go @@ -6,6 +6,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/wire" + "github.com/gin-gonic/gin" ) // AddCommands adds a number of tx-query related subcommands @@ -23,3 +24,8 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *wire.Codec) { // r.HandleFunc("/txs/sign", SignTxRequstHandler).Methods("POST") // r.HandleFunc("/txs/broadcast", BroadcastTxRequestHandler).Methods("POST") } + +// RegisterSwaggerRoutes registers transaction related REST routes to Gaia-lite server +func RegisterSwaggerRoutes(routerGroup *gin.RouterGroup, ctx context.CLIContext, cdc *wire.Codec) { + routerGroup.POST("/txs", BroadcastTxRequest(cdc, ctx)) +} \ No newline at end of file diff --git a/cmd/gaia/cmd/gaiacli/main.go b/cmd/gaia/cmd/gaiacli/main.go index df0fd3c1138e..35f8848429bc 100644 --- a/cmd/gaia/cmd/gaiacli/main.go +++ b/cmd/gaia/cmd/gaiacli/main.go @@ -19,6 +19,7 @@ import ( stakecmd "github.com/cosmos/cosmos-sdk/x/stake/client/cli" "github.com/cosmos/cosmos-sdk/cmd/gaia/app" + _ "github.com/cosmos/cosmos-sdk/client/lcd/docs" ) // rootCmd is the entry point for this binary @@ -66,6 +67,7 @@ func main() { tendermintCmd, ibcCmd, lcd.ServeCommand(cdc), + lcd.ServeSwaggerCommand(cdc), client.LineBreak, ) diff --git a/docs/light/api.md b/docs/light/api.md index 6c5e8aa5f02a..7bbfedc63607 100644 --- a/docs/light/api.md +++ b/docs/light/api.md @@ -32,7 +32,7 @@ Exposes the same functionality as the Tendermint RPC from a full node. It aims t ```json { "transaction": "string", - "return": "string", + "return": "async" } ``` @@ -40,15 +40,11 @@ Exposes the same functionality as the Tendermint RPC from a full node. It aims t ```json { - "rest api":"2.0", - "code":200, - "error":"", - "result":{ + "code":0, "hash":"0D33F2F03A5234F38706E43004489E061AC40A2E", "data":"", "log":"" - } } ``` @@ -63,26 +59,18 @@ This API exposes all functionality needed for key creation, signing and manageme - Returns on success: ```json -{ - "rest api":"1.0", - "code":200, - "error":"", - "result":{ - "account":[ - { - "name":"monkey", - "address":"cosmosaccaddr1fedh326uxqlxs8ph9ej7cf854gz7fd5zlym5pd", - "pub_key":"cosmosaccpub1zcjduc3q8s8ha96ry4xc5xvjp9tr9w9p0e5lk5y0rpjs5epsfxs4wmf72x3shvus0t" - }, - { - "name":"test", - "address":"cosmosaccaddr1thlqhjqw78zvcy0ua4ldj9gnazqzavyw4eske2", - "pub_key":"cosmosaccpub1zcjduc3qyx6hlf825jcnj39adpkaxjer95q7yvy25yhfj3dmqy2ctev0rxmse9cuak" - } - ], - "block_height":5241 - } -} +[ + { + "name":"monkey", + "address":"cosmosaccaddr1fedh326uxqlxs8ph9ej7cf854gz7fd5zlym5pd", + "pub_key":"cosmosaccpub1zcjduc3q8s8ha96ry4xc5xvjp9tr9w9p0e5lk5y0rpjs5epsfxs4wmf72x3shvus0t" + }, + { + "name":"test", + "address":"cosmosaccaddr1thlqhjqw78zvcy0ua4ldj9gnazqzavyw4eske2", + "pub_key":"cosmosaccpub1zcjduc3qyx6hlf825jcnj39adpkaxjer95q7yvy25yhfj3dmqy2ctev0rxmse9cuak" + } +] ``` ### POST /keys @@ -93,9 +81,9 @@ This API exposes all functionality needed for key creation, signing and manageme ```json { - "name": "string", - "password": "string", - "seed": "string", + "name": "string", + "password": "string", + "seed": "string" } ``` @@ -103,12 +91,11 @@ Returns on success: ```json { - "rest api":"1.0", - "code":200, - "error":"", - "result":{ - "seed":"crime carpet recycle erase simple prepare moral dentist fee cause pitch trigger when velvet animal abandon" - } + "name":"test2", + "type":"local", + "address":"cosmosaccaddr17pjnvcae9pktplx0jz5r0vn8ugsqmfuzwsvzzj", + "pub_key":"cosmosaccpub1addwnpepq0gxp200rysljv9v645llj3y3d3t4vjrdcnl8dnk540fnmu25h6tgustsyt", + "seed":"muffin novel usual evoke camp canal decade asthma creek lend record media adapt fresh brisk govern plate debris come mother behave coil process next" } ``` @@ -120,14 +107,10 @@ Returns on success: ```json { - "rest api":"1.0", - "code":200, - "error":"", - "result":{ - "name":"test", - "address":"cosmosaccaddr1thlqhjqw78zvcy0ua4ldj9gnazqzavyw4eske2", - "pub_key":"cosmosaccpub1zcjduc3qyx6hlf825jcnj39adpkaxjer95q7yvy25yhfj3dmqy2ctev0rxmse9cuak" - } + "name": "test1", + "type": "local", + "address": "cosmosaccaddr1dla9p5dycmwndmqfehlymwvvjjtfkfw4he576q", + "pub_key": "cosmosaccpub1addwnpepqgdykrxehg3k9vttjref3dhndtnsy5r3k5f30kekv8elqdluj5ktgl86nwd" } ``` @@ -140,19 +123,14 @@ Returns on success: ```json { "old_password": "string", - "new_password": "string", + "new_password": "string" } ``` - Returns on success: -```json -{ - "rest api":"2.0", - "code":200, - "error":"", - "result":{} -} +```string +success ``` ### DELETE /keys/{name} @@ -163,19 +141,14 @@ Returns on success: ```json { - "password": "string", + "password": "string" } ``` - Returns on success: -```json -{ - "rest api":"1.0", - "code":200, - "error":"", - "result":{} -} +```string +success ``` ### POST /keys/{name}/recover @@ -187,7 +160,7 @@ Returns on success: ```json { "password": "string", - "seed": "string", + "seed": "string" } ``` @@ -195,12 +168,10 @@ Returns on success: ```json { - "rest api":"1.0", - "code":200, - "error":"", - "result":{ - "address":"BD607C37147656A507A5A521AA9446EB72B2C907" - } + "name":"test2", + "type":"local", + "address":"cosmosaccaddr17pjnvcae9pktplx0jz5r0vn8ugsqmfuzwsvzzj", + "pub_key":"cosmosaccpub1addwnpepq0gxp200rysljv9v645llj3y3d3t4vjrdcnl8dnk540fnmu25h6tgustsyt" } ``` @@ -212,21 +183,27 @@ Returns on success: ```json { - "rest api":"1.0", - "code":200, - "error":"", - "result":{ - "address": "82A57F8575BDFA22F5164C75361A21D2B0E11089", - "public_key": "PubKeyEd25519{A0EEEED3C9CE1A6988DEBFE347635834A1C0EBA0B4BB1125896A7072D22E650D}", + "type": "auth/Account", + "value": { + "address": "cosmosaccaddr1zvl8p2w6v90k0geerqsnz3e6jxa2rzg3wvupmn", "coins": [ - {"atom": 300}, - {"photon": 15} + { + "denom": "monikerToken", + "amount": "990" + }, + { + "denom": "steak", + "amount": "49" + } ], - "account_number": 1, - "sequence": 7 + "public_key": { + "type": "tendermint/PubKeySecp256k1", + "value": "Aje2CWOpo0mcrfZy0Q+zSabeHjvT7oEuXuKljLU9agE/" + }, + "account_number": "0", + "sequence": "1" } } -} ``` ## ICS20 - TokenAPI @@ -240,38 +217,42 @@ The TokenAPI exposes all functionality needed to query account balances and send - Returns on success: ```json -{ - "rest api":"2.0", - "code":200, - "error":"", - "result": { - "atom":1000, - "photon":500, - "ether":20 +[ + { + "denom":"monikerToken", + "amount":"990" + }, + { + "denom":"steak", + "amount":"49" } -} +] ``` ### POST /bank/transfers - **URL**: `/bank/transfers` - **Functionality**: Create a transfer in the bank module. -- POST Body: +- POST Body, generate is false: ```json { + "account_number": "0", "amount": [ { - "denom": "string", - "amount": 64, + "amount": "10", + "denom": "monikerToken" } ], - "name": "string", - "password": "string", - "chain_id": "string", - "account_number": 64, - "sequence": 64, - "gas": 64, + "chain_id": "test-chain-KfHYRV", + "ensure_account_sequence": false, + "fee": "", + "gas": "10000", + "generate": false, + "name": "moniker", + "password": "12345678", + "sequence": "1", + "to_address": "cosmosaccaddr17pjnvcae9pktplx0jz5r0vn8ugsqmfuzwsvzzj" } ``` @@ -279,12 +260,97 @@ The TokenAPI exposes all functionality needed to query account balances and send ```json { - "rest api":"2.0", - "code":200, - "error":"", - "result":{ - "transaction":"TODO:" - } + "check_tx": { + "log": "Msg 0: ", + "gasWanted": "10000", + "gasUsed": "1242" + }, + "deliver_tx": { + "log": "Msg 0: ", + "gasWanted": "10000", + "gasUsed": "3288", + "tags": [ + { + "key": "c2VuZGVy", + "value": "Y29zbW9zYWNjYWRkcjF6dmw4cDJ3NnY5MGswZ2VlcnFzbnozZTZqeGEycnpnM3d2dXBtbg==" + }, + { + "key": "cmVjaXBpZW50", + "value": "Y29zbW9zYWNjYWRkcjE3cGpudmNhZTlwa3RwbHgwano1cjB2bjh1Z3NxbWZ1endzdnp6ag==" + } + ] + }, + "hash": "3474141FF827BDB85F39EF94D3B6B93E3D3C2A7A", + "height": "1574" +} +``` + +- POST Body, generate is true: + +```json +{ + "account_number": "0", + "amount": [ + { + "amount": "10", + "denom": "monikerToken" + } + ], + "chain_id": "test-chain-KfHYRV", + "ensure_account_sequence": false, + "fee": "1steak", + "gas": "10000", + "generate": true, + "name": "moniker", + "password": "12345678", + "sequence": "2", + "to_address": "cosmosaccaddr17pjnvcae9pktplx0jz5r0vn8ugsqmfuzwsvzzj" +} +``` + +- Returns on success: + +```json +{ + "account_number": "0", + "chain_id": "test-chain-KfHYRV", + "fee": { + "amount": [ + { + "amount": "1", + "denom": "steak" + } + ], + "gas": "10000" + }, + "memo": "", + "msgs": [ + { + "inputs": [ + { + "address": "cosmosaccaddr1zvl8p2w6v90k0geerqsnz3e6jxa2rzg3wvupmn", + "coins": [ + { + "amount": "10", + "denom": "monikerToken" + } + ] + } + ], + "outputs": [ + { + "address": "cosmosaccaddr17pjnvcae9pktplx0jz5r0vn8ugsqmfuzwsvzzj", + "coins": [ + { + "amount": "10", + "denom": "monikerToken" + } + ] + } + ] + } + ], + "sequence": "2" } ``` @@ -300,14 +366,16 @@ The StakingAPI exposes all functionality needed for validation and delegation in ```json { - "rest api":"2.1", - "code":200, - "error":"", - "result": { - "atom":1000, - "photon":500, - "ether":20 - } + "delegations": [ + { + "delegator_addr": "cosmosaccaddr1zvl8p2w6v90k0geerqsnz3e6jxa2rzg3wvupmn", + "validator_addr": "cosmosaccaddr1zvl8p2w6v90k0geerqsnz3e6jxa2rzg3wvupmn", + "shares": "100.0000000000", + "height": "0" + } + ], + "unbonding_delegations": null, + "redelegations": null } ``` @@ -318,12 +386,30 @@ The StakingAPI exposes all functionality needed for validation and delegation in - Returns on success: ```json -{ - "rest api":"2.1", - "code":200, - "error":"", - "result":{} -} +[ + { + "operator": "cosmosaccaddr1zvl8p2w6v90k0geerqsnz3e6jxa2rzg3wvupmn", + "pub_key": "cosmosvalpub1zcjduepq6lql7kxcsrss2wmaxmsv5afkprdqshhupafqc7h37h0wmlhvkn5qrcky3x", + "jailed": false, + "status": 2, + "tokens": "1000000000000", + "delegator_shares": "1000000000000", + "description": { + "moniker": "moniker", + "identity": "", + "website": "", + "details": "" + }, + "bond_height": "0", + "bond_intra_tx_counter": 0, + "proposer_reward_pool": null, + "commission": "0", + "commission_max": "0", + "commission_change_rate": "0", + "commission_change_today": "0", + "prev_bonded_shares": "0" + } +] ``` ### GET /stake/delegators/{delegatorAddr}/validators/{validatorAddr} @@ -334,10 +420,26 @@ The StakingAPI exposes all functionality needed for validation and delegation in ```json { - "rest api":"2.1", - "code":200, - "error":"", - "result":{} + "operator": "cosmosaccaddr1zvl8p2w6v90k0geerqsnz3e6jxa2rzg3wvupmn", + "pub_key": "cosmosvalpub1zcjduepq6lql7kxcsrss2wmaxmsv5afkprdqshhupafqc7h37h0wmlhvkn5qrcky3x", + "jailed": false, + "status": 2, + "tokens": "1000000000000", + "delegator_shares": "1000000000000", + "description": { + "moniker": "moniker", + "identity": "", + "website": "", + "details": "" + }, + "bond_height": "0", + "bond_intra_tx_counter": 0, + "proposer_reward_pool": null, + "commission": "0", + "commission_max": "0", + "commission_change_rate": "0", + "commission_change_today": "0", + "prev_bonded_shares": "0" } ``` @@ -348,14 +450,29 @@ The StakingAPI exposes all functionality needed for validation and delegation in - Returns on success: ```json -{ - "rest api":"2.1", - "code":200, - "error":"", - "result":{ - "transaction":"TODO" - } -} +[ + { + "hash": "string", + "height": 0, + "result": { + "code": 0, + "data": "string", + "gas_used": 0, + "gas_wanted": 0, + "info": "string", + "log": "string", + "tags": [ + [ + { + "key": "string", + "value": 0 + } + ] + ] + }, + "tx": "string" + } +] ``` ### POST /stake/delegators/{delegatorAddr}/delegations @@ -387,13 +504,13 @@ The StakingAPI exposes all functionality needed for validation and delegation in { "delegator_addr": "string", "validator_addr": "string", - "shares": "string", + "shares": "string" } ], "complete_unbondings": [ { "delegator_addr": "string", - "validator_addr": "string", + "validator_addr": "string" } ], "begin_redelegates": [ @@ -401,14 +518,14 @@ The StakingAPI exposes all functionality needed for validation and delegation in "delegator_addr": "string", "validator_src_addr": "string", "validator_dst_addr": "string", - "shares": "string", + "shares": "string" } ], "complete_redelegates": [ { "delegator_addr": "string", "validator_src_addr": "string", - "validator_dst_addr": "string", + "validator_dst_addr": "string" } ] } @@ -419,12 +536,28 @@ The StakingAPI exposes all functionality needed for validation and delegation in ```json { - "rest api":"2.1", - "code":200, - "error":"", - "result":{ - "transaction":"TODO" - } + "check_tx": { + "log": "Msg 0: ", + "gasWanted": "10000", + "gasUsed": "1242" + }, + "deliver_tx": { + "log": "Msg 0: ", + "gasWanted": "10000", + "gasUsed": "3288", + "tags": [ + { + "key": "c2VuZGVy", + "value": "Y29zbW9zYWNjYWRkcjF6dmw4cDJ3NnY5MGswZ2VlcnFzbnozZTZqeGEycnpnM3d2dXBtbg==" + }, + { + "key": "cmVjaXBpZW50", + "value": "Y29zbW9zYWNjYWRkcjE3cGpudmNhZTlwa3RwbHgwano1cjB2bjh1Z3NxbWZ1endzdnp6ag==" + } + ] + }, + "hash": "3474141FF827BDB85F39EF94D3B6B93E3D3C2A7A", + "height": "1574" } ``` @@ -436,12 +569,10 @@ The StakingAPI exposes all functionality needed for validation and delegation in ```json { - "rest api":"2.1", - "code":200, - "error":"", - "result":{ - "transaction":"TODO" - } + "delegator_addr": "cosmosaccaddr1zvl8p2w6v90k0geerqsnz3e6jxa2rzg3wvupmn", + "validator_addr": "cosmosaccaddr1zvl8p2w6v90k0geerqsnz3e6jxa2rzg3wvupmn", + "shares": "100.0000000000", + "height": "0" } ``` @@ -452,14 +583,22 @@ The StakingAPI exposes all functionality needed for validation and delegation in - Returns on success: ```json -{ - "rest api":"2.1", - "code":200, - "error":"", - "result":{ - "transaction":"TODO" - } -} +[ + { + "balance": { + "amount": "string", + "denom": "string" + }, + "creation_height": 0, + "delegator_addr": "string", + "initial_balance": { + "amount": "string", + "denom": "string" + }, + "min_time": 0, + "validator_addr": "string" + } +] ``` ### GET /stake/validators @@ -469,14 +608,30 @@ The StakingAPI exposes all functionality needed for validation and delegation in - Returns on success: ```json -{ - "rest api":"2.1", - "code":200, - "error":"", - "result":{ - "transaction":"TODO" +[ + { + "operator": "cosmosaccaddr1zvl8p2w6v90k0geerqsnz3e6jxa2rzg3wvupmn", + "pub_key": "cosmosvalpub1zcjduepq6lql7kxcsrss2wmaxmsv5afkprdqshhupafqc7h37h0wmlhvkn5qrcky3x", + "jailed": false, + "status": 2, + "tokens": "1000000000000", + "delegator_shares": "1000000000000", + "description": { + "moniker": "moniker", + "identity": "", + "website": "", + "details": "" + }, + "bond_height": "0", + "bond_intra_tx_counter": 0, + "proposer_reward_pool": null, + "commission": "0", + "commission_max": "0", + "commission_change_rate": "0", + "commission_change_today": "0", + "prev_bonded_shares": "0" } -} +] ``` ### GET /stake/validators/{validatorAddr} @@ -487,12 +642,26 @@ The StakingAPI exposes all functionality needed for validation and delegation in ```json { - "rest api":"2.1", - "code":200, - "error":"", - "result":{ - "transaction":"TODO" - } + "operator": "cosmosaccaddr1zvl8p2w6v90k0geerqsnz3e6jxa2rzg3wvupmn", + "pub_key": "cosmosvalpub1zcjduepq6lql7kxcsrss2wmaxmsv5afkprdqshhupafqc7h37h0wmlhvkn5qrcky3x", + "jailed": false, + "status": 2, + "tokens": "1000000000000", + "delegator_shares": "1000000000000", + "description": { + "moniker": "moniker", + "identity": "", + "website": "", + "details": "" + }, + "bond_height": "0", + "bond_intra_tx_counter": 0, + "proposer_reward_pool": null, + "commission": "0", + "commission_max": "0", + "commission_change_rate": "0", + "commission_change_today": "0", + "prev_bonded_shares": "0" } ``` @@ -504,18 +673,13 @@ The StakingAPI exposes all functionality needed for validation and delegation in ```json { - "rest api":"2.1", - "code":200, - "error":"", - "result":{ - "inflation_rate_change": 1300000000, - "inflation_max": 2000000000, - "inflation_min": 700000000, - "goal_bonded": 6700000000, - "unbonding_time": "72h0m0s", - "max_validators": 100, - "bond_denom": "atom" - } + "inflation_rate_change": "1300000000", + "inflation_max": "2000000000", + "inflation_min": "700000000", + "goal_bonded": "6700000000", + "unbonding_time": "259200000000000", + "max_validators": 100, + "bond_denom": "steak" } ``` @@ -527,17 +691,12 @@ The StakingAPI exposes all functionality needed for validation and delegation in ```json { - "rest api":"2.1", - "code":200, - "error":"", - "result":{ - "loose_tokens": 0, - "bonded_tokens": 0, - "inflation_last_time": "1970-01-01 01:00:00 +0100 CET", - "inflation": 700000000, - "date_last_commission_reset": 0, - "prev_bonded_shares": 0, - } + "loose_tokens": "500035934654", + "bonded_tokens": "1000000000000", + "inflation_last_time": "2018-08-28T06:40:39.617950067Z", + "inflation": "700002217", + "date_last_commission_reset": "0", + "prev_bonded_shares": "0" } ``` @@ -570,7 +729,7 @@ The GovernanceAPI exposes all functionality needed for casting votes on plain te - **Functionality**: Submit a proposal - POST Body: -```js +```json { "base_req": { // Name of key to use @@ -593,7 +752,7 @@ The GovernanceAPI exposes all functionality needed for casting votes on plain te "initial_deposit": [ { "denom": "string", - "amount": 64, + "amount": 64 } ] } @@ -607,7 +766,7 @@ The GovernanceAPI exposes all functionality needed for casting votes on plain te "code":200, "error":"", "result":{ - "TODO": "TODO", + "TODO": "TODO" } } ``` @@ -659,7 +818,7 @@ The GovernanceAPI exposes all functionality needed for casting votes on plain te "gas": 0 }, "depositer": "string", - "amount": 0, + "amount": 0 } ``` @@ -671,7 +830,7 @@ The GovernanceAPI exposes all functionality needed for casting votes on plain te "code":200, "error":"", "result":{ - "TODO": "TODO", + "TODO": "TODO" } } ``` @@ -738,7 +897,7 @@ The GovernanceAPI exposes all functionality needed for casting votes on plain te "proposal-id": 1, "voter": "cosmosaccaddr1849m9wncrqp6v4tkss6a3j8uzvuv0cp7f75lrq", "option": "yes" - }, + } ] } ``` @@ -751,7 +910,7 @@ The GovernanceAPI exposes all functionality needed for casting votes on plain te - **Functionality**: Vote for a specific proposal - POST Body: -```js +```json { "base_req": { "name": "string", @@ -764,7 +923,7 @@ The GovernanceAPI exposes all functionality needed for casting votes on plain te // A cosmosaccaddr address "voter": "string", // Value of the vote option `Yes`, `No` `Abstain`, `NoWithVeto` - "option": "string", + "option": "string" } ``` @@ -776,7 +935,7 @@ The GovernanceAPI exposes all functionality needed for casting votes on plain te "code":200, "error":"", "result":{ - "TODO": "TODO", + "TODO": "TODO" } } ``` @@ -827,7 +986,7 @@ The SlashingAPI exposes all functionalities needed to slash (*i.e* penalize) val - **Functionality**: Submit a message to unjail a validator after it has been penalized. - POST Body: -```js +```json { // Name of key to use "name": "string", @@ -836,7 +995,7 @@ The SlashingAPI exposes all functionalities needed to slash (*i.e* penalize) val "chain_id": "string", "account_number": 64, "sequence": 64, - "gas": 64, + "gas": 64 } ``` diff --git a/docs/light/getting_started.md b/docs/light/getting_started.md index 5f11956c0540..1799c6f11ee0 100644 --- a/docs/light/getting_started.md +++ b/docs/light/getting_started.md @@ -1,40 +1,41 @@ # Getting Started To start a rest server, we need to specify the following parameters: -| Parameter | Type | Default | Required | Description | -| ----------- | --------- | ----------------------- | -------- | ---------------------------------------------------- | -| chain-id | string | null | true | chain id of the full node to connect | -| node | URL | "tcp://localhost:46657" | true | address of the full node to connect | -| laddr | URL | "tcp://localhost:1317" | true | address to run the rest server on | -| trust-node | bool | "false" | true | Whether this LCD is connected to a trusted full node | -| trust-store | DIRECTORY | "$HOME/.lcd" | false | directory for save checkpoints and validator sets | -Sample command: - -```bash -gaiacli light-client --chain-id=test --laddr=tcp://localhost:1317 --node tcp://localhost:46657 --trust-node=false +| Parameter | Type | Default | Required | Description | +| --------------- | --------- | ----------------------- | -------- | ---------------------------------------------------- | +| chain-id | string | null | true | ID of chain we connect to, must be specified | +| home | string | "$HOME/.gaiacli" | false | directory for config and data, such as key and checkpoint | +| node-list | string | "tcp://localhost:26657" | false | Full node list to connect to, example: "tcp://10.10.10.10:26657,tcp://20.20.20.20:26657" | +| laddr | string | "tcp://localhost:1317" | false | Address for server to listen on | +| trust-node | bool | false | false | Trust full nodes or not | +| swagger-host-ip | string | "localhost" | false | The host IP of the Gaia-lite server, swagger-ui will send request to this host | +| modules | string | "general,key,bank" | false | Enabled modules | + +Sample command to start gaia-lite node: +``` +gaiacli lite-server --chain-id= +``` +Please refer to the following url for Swagger-UI: +``` +http://localhost:1317/swagger/index.html ``` -## Gaia Light Use Cases - -LCD could be very helpful for related service providers. For a wallet service provider, LCD could -make transaction faster and more reliable in the following cases. - -### Create an account - -![deposit](pics/create-account.png) - -First you need to get a new seed phrase :[get-seed](api.md#keysseed---get) - -After having new seed, you could generate a new account with it : [keys](api.md#keys---post) - -### Transfer a token - -![transfer](pics/transfer-tokens.png) +When the connected full node is trusted, then the proof is not necessary, so you can run gaia-lite node with trust-node option: +``` +gaiacli lite-server --chain-id= --trust-node +``` -The first step is to build an asset transfer transaction. Here we can post all necessary parameters -to /create_transfer to get the unsigned transaction byte array. Refer to this link for detailed -operation: [build transaction](api.md#create_transfer---post) +If you have want to run gaia-lite node in a remote server, then you must specify the public ip to swagger-host-ip +``` +gaiacli lite-server --chain-id= --swagger-host-ip= +``` -Then sign the returned transaction byte array with users' private key. Finally broadcast the signed -transaction. Refer to this link for how to broadcast the signed transaction: [broadcast transaction](api.md#create_transfer---post) +The gaia-lite node can connect to multiple full nodes. Then gaia-lite will do load balancing for full nodes which is helpful to improve reliability and TPS. You can do this by this command: +``` +gaiacli lite-server --chain-id= --node-list=tcp://10.10.10.10:26657,tcp://20.20.20.20:26657 +``` +Gaia-lite is built in a modular format. Each Cosmos module defines it's own RPC API. Currently the following modules are supported: general, transaction, key, bank, staking, governance, and slashing. +``` +gaiacli lite-server --chain-id= --modules=general,key,bank,staking +``` \ No newline at end of file diff --git a/docs/sdk/clients.md b/docs/sdk/clients.md index 0a1c2a38d990..f81aecd58232 100644 --- a/docs/sdk/clients.md +++ b/docs/sdk/clients.md @@ -384,6 +384,13 @@ With the `pool` command you will get the values for: ## Gaia-Lite -::: tip Note -🚧 We are actively working on documentation for Gaia-lite. -::: +The Gaia-Lite is a light gaiad node. Unlike gaiad full node, it doesn't participate consensus and execute transactions, so it only require very little storage and calculation resource. However, it can provide the same security as a gaia full node. + +It provides modular rest APIs. Through these APIs, you can send transactions and query blockchain information. Besides, all query results with proof will be verified against the validator set and merkle hash algorithm. + +You can start a gaia-lite node with the following command: +``` +gaiacli lite-server --chain-id= +``` + +Please refer to this [getting start](../light/getting_started.md) for detailed description about how to run gaia-lite. \ No newline at end of file diff --git a/examples/basecoin/cmd/basecli/main.go b/examples/basecoin/cmd/basecli/main.go index 23183c87a4d3..24714f503882 100644 --- a/examples/basecoin/cmd/basecli/main.go +++ b/examples/basecoin/cmd/basecli/main.go @@ -79,6 +79,7 @@ func main() { rootCmd.AddCommand( client.LineBreak, lcd.ServeCommand(cdc), + lcd.ServeSwaggerCommand(cdc), keys.Commands(), client.LineBreak, version.VersionCmd, diff --git a/store/multistoreproof.go b/store/multistoreproof.go new file mode 100644 index 000000000000..e98a198e935f --- /dev/null +++ b/store/multistoreproof.go @@ -0,0 +1,116 @@ +package store + +import ( + "bytes" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/pkg/errors" + "github.com/tendermint/iavl" + cmn "github.com/tendermint/tendermint/libs/common" +) + +// commitID of substores, such as acc store, gov store +type SubstoreCommitID struct { + Name string `json:"name"` + Version int64 `json:"version"` + CommitHash cmn.HexBytes `json:"commit_hash"` +} + +// proof of store which have multi substores +type MultiStoreProof struct { + CommitIDList []SubstoreCommitID `json:"commit_id_list"` + StoreName string `json:"store_name"` + RangeProof iavl.RangeProof `json:"range_proof"` +} + +// build MultiStoreProof based on iavl proof and storeInfos +func BuildMultiStoreProof(iavlProof []byte, storeName string, storeInfos []storeInfo) ([]byte, error) { + var rangeProof iavl.RangeProof + err := cdc.UnmarshalBinary(iavlProof, &rangeProof) + if err != nil { + return nil, err + } + + var multiStoreProof MultiStoreProof + for _, storeInfo := range storeInfos { + + commitID := SubstoreCommitID{ + Name: storeInfo.Name, + Version: storeInfo.Core.CommitID.Version, + CommitHash: storeInfo.Core.CommitID.Hash, + } + multiStoreProof.CommitIDList = append(multiStoreProof.CommitIDList, commitID) + } + multiStoreProof.StoreName = storeName + multiStoreProof.RangeProof = rangeProof + + proof, err := cdc.MarshalBinary(multiStoreProof) + if err != nil { + return nil, err + } + + return proof, nil +} + +// verify multiStoreCommitInfo against appHash +func VerifyMultiStoreCommitInfo(storeName string, multiStoreCommitInfo []SubstoreCommitID, appHash []byte) ([]byte, error) { + var substoreCommitHash []byte + var storeInfos []storeInfo + var height int64 + for _, multiStoreCommitID := range multiStoreCommitInfo { + + if multiStoreCommitID.Name == storeName { + substoreCommitHash = multiStoreCommitID.CommitHash + height = multiStoreCommitID.Version + } + storeInfo := storeInfo{ + Name: multiStoreCommitID.Name, + Core: storeCore{ + CommitID: sdk.CommitID{ + Version: multiStoreCommitID.Version, + Hash: multiStoreCommitID.CommitHash, + }, + }, + } + + storeInfos = append(storeInfos, storeInfo) + } + if len(substoreCommitHash) == 0 { + return nil, cmn.NewError("failed to get substore root commit hash by store name") + } + + ci := commitInfo{ + Version: height, + StoreInfos: storeInfos, + } + + if !bytes.Equal(appHash, ci.Hash()) { + return nil, cmn.NewError("the merkle root of multiStoreCommitInfo doesn't equal to appHash") + } + return substoreCommitHash, nil +} + +// verify iavl proof +func VerifyRangeProof(key, value []byte, substoreCommitHash []byte, rangeProof *iavl.RangeProof) error { + + // Validate the proof to ensure data integrity. + err := rangeProof.Verify(substoreCommitHash) + if err != nil { + return errors.Wrap(err, "proof root hash doesn't equal to substore commit root hash") + } + + if len(value) != 0 { + // Validate existence proof + err = rangeProof.VerifyItem(key, value) + if err != nil { + return errors.Wrap(err, "failed in existence verification") + } + } else { + // Validate absence proof + err = rangeProof.VerifyAbsence(key) + if err != nil { + return errors.Wrap(err, "failed in absence verification") + } + } + + return nil +} \ No newline at end of file diff --git a/store/multistoreproof_test.go b/store/multistoreproof_test.go new file mode 100644 index 000000000000..1538915677a1 --- /dev/null +++ b/store/multistoreproof_test.go @@ -0,0 +1,96 @@ +package store + +import ( + "encoding/hex" + "github.com/stretchr/testify/assert" + "github.com/tendermint/iavl" + cmn "github.com/tendermint/tendermint/libs/common" + "testing" +) + +func TestVerifyMultiStoreCommitInfo(t *testing.T) { + appHash, _ := hex.DecodeString("ebf3c1fb724d3458023c8fefef7b33add2fc1e84") + + substoreRootHash, _ := hex.DecodeString("ea5d468431015c2cd6295e9a0bb1fc0e49033828") + storeName := "acc" + + var multiStoreCommitInfo []SubstoreCommitID + + gocRootHash, _ := hex.DecodeString("62c171bb022e47d1f745608ff749e676dbd25f78") + multiStoreCommitInfo = append(multiStoreCommitInfo, SubstoreCommitID{ + Name: "gov", + Version: 689, + CommitHash: gocRootHash, + }) + + multiStoreCommitInfo = append(multiStoreCommitInfo, SubstoreCommitID{ + Name: "main", + Version: 689, + CommitHash: nil, + }) + + accRootHash, _ := hex.DecodeString("ea5d468431015c2cd6295e9a0bb1fc0e49033828") + multiStoreCommitInfo = append(multiStoreCommitInfo, SubstoreCommitID{ + Name: "acc", + Version: 689, + CommitHash: accRootHash, + }) + + multiStoreCommitInfo = append(multiStoreCommitInfo, SubstoreCommitID{ + Name: "ibc", + Version: 689, + CommitHash: nil, + }) + + stakeRootHash, _ := hex.DecodeString("987d1d27b8771d93aa3691262f661d2c85af7ca4") + multiStoreCommitInfo = append(multiStoreCommitInfo, SubstoreCommitID{ + Name: "stake", + Version: 689, + CommitHash: stakeRootHash, + }) + + slashingRootHash, _ := hex.DecodeString("388ee6e5b11f367069beb1eefd553491afe9d73e") + multiStoreCommitInfo = append(multiStoreCommitInfo, SubstoreCommitID{ + Name: "slashing", + Version: 689, + CommitHash: slashingRootHash, + }) + + commitHash, err := VerifyMultiStoreCommitInfo(storeName, multiStoreCommitInfo, appHash) + assert.Nil(t, err) + assert.Equal(t, commitHash, substoreRootHash) + + appHash, _ = hex.DecodeString("29de216bf5e2531c688de36caaf024cd3bb09ee3") + + _, err = VerifyMultiStoreCommitInfo(storeName, multiStoreCommitInfo, appHash) + assert.Error(t, err, "appHash doesn't match to the merkle root of multiStoreCommitInfo") +} + +func TestVerifyRangeProof(t *testing.T) { + tree := iavl.NewTree(nil, 0) + + rand := cmn.NewRand() + rand.Seed(0) // for determinism + for _, ikey := range []byte{0x11, 0x32, 0x50, 0x72, 0x99} { + key := []byte{ikey} + tree.Set(key, []byte(rand.Str(8))) + } + + root := tree.Hash() + + key := []byte{0x32} + val, proof, err := tree.GetWithProof(key) + assert.Nil(t, err) + assert.NotEmpty(t, val) + assert.NotEmpty(t, proof) + err = VerifyRangeProof(key, val, root, proof) + assert.Nil(t, err) + + key = []byte{0x40} + val, proof, err = tree.GetWithProof(key) + assert.Nil(t, err) + assert.Empty(t, val) + assert.NotEmpty(t, proof) + err = VerifyRangeProof(key, val, root, proof) + assert.Nil(t, err) +} diff --git a/store/rootmultistore.go b/store/rootmultistore.go index 04f8e44e6954..62cf70ce7058 100644 --- a/store/rootmultistore.go +++ b/store/rootmultistore.go @@ -291,6 +291,23 @@ func (rs *rootMultiStore) Query(req abci.RequestQuery) abci.ResponseQuery { // trim the path and make the query req.Path = subpath res := queryable.Query(req) + + // WARNING This should be consistent with query method in iavlstore.go + if !req.Prove || subpath != "/store" && subpath != "/key" { + return res + } + + //Load commit info from db + commitInfo, errMsg := getCommitInfo(rs.db,res.Height) + if errMsg != nil { + return sdk.ErrInternal(errMsg.Error()).QueryResult() + } + + res.Proof, errMsg = BuildMultiStoreProof(res.Proof, storeName, commitInfo.StoreInfos) + if errMsg != nil { + return sdk.ErrInternal(errMsg.Error()).QueryResult() + } + return res } diff --git a/store/transientstore.go b/store/transientstore.go index 1c099fa0ddea..f04dfe255677 100644 --- a/store/transientstore.go +++ b/store/transientstore.go @@ -2,6 +2,7 @@ package store import ( dbm "github.com/tendermint/tendermint/libs/db" + sdk "github.com/cosmos/cosmos-sdk/types" ) var _ KVStore = (*transientStore)(nil) @@ -41,3 +42,8 @@ func (ts *transientStore) Prefix(prefix []byte) KVStore { func (ts *transientStore) Gas(meter GasMeter, config GasConfig) KVStore { return NewGasKVStore(meter, config, ts) } + +// Implements Store. +func (ts *transientStore) GetStoreType() StoreType { + return sdk.StoreTypeTransient +} \ No newline at end of file diff --git a/x/auth/client/context/context.go b/x/auth/client/context/context.go index 8d0a94136d89..848417611ca2 100644 --- a/x/auth/client/context/context.go +++ b/x/auth/client/context/context.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/viper" + "github.com/tendermint/tendermint/crypto" ) // TxContext implements a transaction context created in SDK modules. @@ -148,3 +149,34 @@ func (ctx TxContext) BuildAndSign(name, passphrase string, msgs []sdk.Msg) ([]by return ctx.Sign(name, passphrase, msg) } + +// build the transaction from the msg +func (ctx TxContext) BuildTxWithSignature(cdc *wire.Codec, msgs auth.StdSignMsg, signatureBytes []byte, publicKeyBytes []byte) ([]byte, error) { + + var publicKey crypto.PubKey + err := cdc.UnmarshalBinaryBare(publicKeyBytes, &publicKey) + if err != nil { + return nil, err + } + + stdSignatures := []auth.StdSignature{{ + AccountNumber: ctx.AccountNumber, + Sequence: ctx.Sequence, + PubKey: publicKey, + Signature: signatureBytes, + }} + + memo := ctx.Memo + fee := sdk.Coin{} + if ctx.Fee != "" { + parsedFee, err := sdk.ParseCoin(ctx.Fee) + if err != nil { + return nil, err + } + fee = parsedFee + } + + tx := auth.NewStdTx(msgs.Msgs, auth.NewStdFee(ctx.Gas, fee), stdSignatures, memo) + + return ctx.Codec.MarshalBinary(tx) +} \ No newline at end of file diff --git a/x/auth/client/rest/query.go b/x/auth/client/rest/query.go index 07b109d40315..26f44e265ff5 100644 --- a/x/auth/client/rest/query.go +++ b/x/auth/client/rest/query.go @@ -12,12 +12,14 @@ import ( authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" "github.com/gorilla/mux" + "github.com/gin-gonic/gin" + "github.com/cosmos/cosmos-sdk/client/httputils" ) // register REST routes func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *wire.Codec, storeName string) { r.HandleFunc( - "/accounts/{address}", + "/auth/accounts/{address}", QueryAccountRequestHandlerFn(storeName, cdc, authcmd.GetAccountDecoder(cdc), cliCtx), ).Methods("GET") } @@ -66,3 +68,49 @@ func QueryAccountRequestHandlerFn( w.Write(output) } } + +// RegisterSwaggerRoutes registers account query related routes to Gaia-lite server +func RegisterSwaggerRoutes(routerGroup *gin.RouterGroup, ctx context.CLIContext, cdc *wire.Codec, storeName string) { + routerGroup.GET("/auth/accounts/:address",queryAccountRequestHandler(storeName,cdc,authcmd.GetAccountDecoder(cdc),ctx)) +} + +func queryAccountRequestHandler(storeName string, cdc *wire.Codec, decoder auth.AccountDecoder, ctx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + + bech32addr := gtx.Param("address") + + addr, err := sdk.AccAddressFromBech32(bech32addr) + if err != nil { + httputils.NewError(gtx, http.StatusConflict, err) + return + } + + res, err := ctx.QueryStore(auth.AddressStoreKey(addr), storeName) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't query account. Error: %s", err.Error())) + return + } + + // the query will return empty if there is no data for this account + if len(res) == 0 { + httputils.NewError(gtx, http.StatusNoContent, fmt.Errorf("this account info is nil+")) + return + } + + // decode the value + account, err := decoder(res) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't parse query result. Result: %s. Error: %s", res, err.Error())) + return + } + + // print out whole account + output, err := cdc.MarshalJSON(account) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't marshall query result. Error: %s", err.Error())) + return + } + + httputils.NormalResponse(gtx,output) + } +} \ No newline at end of file diff --git a/x/bank/client/rest/query.go b/x/bank/client/rest/query.go new file mode 100644 index 000000000000..640a09326e6e --- /dev/null +++ b/x/bank/client/rest/query.go @@ -0,0 +1,61 @@ +package rest + +import ( + "fmt" + "net/http" + + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/auth" + authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" + + "github.com/gin-gonic/gin" + "github.com/cosmos/cosmos-sdk/client/httputils" +) + +// registerSwaggerQueryRoutes - Central function to define account query related routes that get registered by the main application +func registerSwaggerQueryRoutes(routerGroup *gin.RouterGroup, ctx context.CLIContext, cdc *wire.Codec, storeName string) { + routerGroup.GET("/bank/balances/:account",queryAccountRequestHandler(storeName,cdc,authcmd.GetAccountDecoder(cdc),ctx)) +} + +func queryAccountRequestHandler(storeName string, cdc *wire.Codec, decoder auth.AccountDecoder, ctx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + + bech32addr := gtx.Param("account") + + addr, err := sdk.AccAddressFromBech32(bech32addr) + if err != nil { + httputils.NewError(gtx, http.StatusConflict, err) + return + } + + res, err := ctx.QueryStore(auth.AddressStoreKey(addr), storeName) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't query account. Error: %s", err.Error())) + return + } + + // the query will return empty if there is no data for this account + if len(res) == 0 { + httputils.NewError(gtx, http.StatusNoContent, fmt.Errorf("this account info is nil+")) + return + } + + // decode the value + account, err := decoder(res) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't parse query result. Result: %s. Error: %s", res, err.Error())) + return + } + + // print out whole account + output, err := cdc.MarshalJSON(account.GetCoins()) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't marshall query result. Error: %s", err.Error())) + return + } + + httputils.NormalResponse(gtx,output) + } +} \ No newline at end of file diff --git a/x/bank/client/rest/rest.go b/x/bank/client/rest/rest.go new file mode 100644 index 000000000000..ee1866f91e6f --- /dev/null +++ b/x/bank/client/rest/rest.go @@ -0,0 +1,15 @@ +package rest + + +import ( + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/gin-gonic/gin" + "github.com/cosmos/cosmos-sdk/crypto/keys" +) + +// RegisterRoutes registers bank-related REST handlers to Gaia-lite server +func RegisterSwaggerRoutes(routerGroup *gin.RouterGroup, ctx context.CLIContext, cdc *wire.Codec, kb keys.Keybase) { + registerSwaggerQueryRoutes(routerGroup, ctx, cdc, "acc") + registerSwaggerTxRoutes(routerGroup, ctx, cdc, kb) +} \ No newline at end of file diff --git a/x/bank/client/rest/sendtx.go b/x/bank/client/rest/sendtx.go index c7baa96910d8..b821d58e1308 100644 --- a/x/bank/client/rest/sendtx.go +++ b/x/bank/client/rest/sendtx.go @@ -14,23 +14,33 @@ import ( "github.com/cosmos/cosmos-sdk/x/bank/client" "github.com/gorilla/mux" + "github.com/gin-gonic/gin" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/client/httputils" + authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" + "bytes" + "fmt" + "github.com/tendermint/tendermint/libs/bech32" ) // RegisterRoutes - Central function to define routes that get registered by the main application func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *wire.Codec, kb keys.Keybase) { - r.HandleFunc("/accounts/{address}/send", SendRequestHandlerFn(cdc, kb, cliCtx)).Methods("POST") + r.HandleFunc("/bank/transfers", SendRequestHandlerFn(cdc, kb, cliCtx)).Methods("POST") } -type sendBody struct { - // fees is not used currently - // Fees sdk.Coin `json="fees"` - Amount sdk.Coins `json:"amount"` - LocalAccountName string `json:"name"` +type transferBody struct { + Name string `json:"name"` Password string `json:"password"` + FromAddress string `json:"from_address"` + ToAddress string `json:"to_address"` + Amount sdk.Coins `json:"amount"` ChainID string `json:"chain_id"` AccountNumber int64 `json:"account_number"` Sequence int64 `json:"sequence"` Gas int64 `json:"gas"` + Fee string `json:"fee"` + Generate bool `json:"generate"` + EnsureAccAndSeq bool `json:"ensure_account_sequence"` } var msgCdc = wire.NewCodec() @@ -42,76 +52,229 @@ func init() { // SendRequestHandlerFn - http request handler to send coins to a address func SendRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // collect data - vars := mux.Vars(r) - bech32addr := vars["address"] - - to, err := sdk.AccAddressFromBech32(bech32addr) + var transferBody transferBody + body, err := ioutil.ReadAll(r.Body) if err != nil { utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) return } - - var m sendBody - body, err := ioutil.ReadAll(r.Body) + err = msgCdc.UnmarshalJSON(body, &transferBody) if err != nil { utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) return } - err = msgCdc.UnmarshalJSON(body, &m) + transferBody, errCode, err := paramPreprocess(transferBody, kb) if err != nil { - utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + utils.WriteErrorResponse(&w, errCode, err.Error()) return } - info, err := kb.Get(m.LocalAccountName) + txForSign, _, errMsg := composeTx(cdc, cliCtx, transferBody) if err != nil { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, err.Error()) + if errMsg.Code() == sdk.CodeInternal { + utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + } else { + utils.WriteErrorResponse(&w, http.StatusBadRequest, err.Error()) + } return } - // build message - msg := client.BuildMsg(sdk.AccAddress(info.GetPubKey().Address()), to, m.Amount) - if err != nil { // XXX rechecking same error ? - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + if transferBody.Generate { + w.Write(txForSign.Bytes()) return } - txCtx := authctx.TxContext{ - Codec: cdc, - Gas: m.Gas, - ChainID: m.ChainID, - AccountNumber: m.AccountNumber, - Sequence: m.Sequence, + output, errCode, err := signAndBroadcase(cdc, cliCtx, txForSign, transferBody, kb) + if err != nil { + w.WriteHeader(errCode) + w.Write([]byte(err.Error())) + return } + w.Write(output) + } +} - if m.Gas == 0 { - newCtx, err := utils.EnrichCtxWithGas(txCtx, cliCtx, m.LocalAccountName, m.Password, []sdk.Msg{msg}) - if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) - return - } - txCtx = newCtx - } +// registerSwaggerTxRoutes - Central function to define routes that get registered by the main application +func registerSwaggerTxRoutes(routerGroup *gin.RouterGroup, ctx context.CLIContext, cdc *wire.Codec, kb keys.Keybase) { + routerGroup.POST("/bank/transfers", transferRequestFn(cdc, ctx, kb)) +} - txBytes, err := txCtx.BuildAndSign(m.LocalAccountName, m.Password, []sdk.Msg{msg}) +// handler of creating transfer transaction +func transferRequestFn(cdc *wire.Codec, ctx context.CLIContext, kb keys.Keybase) gin.HandlerFunc { + return func(gtx *gin.Context) { + var transferBody transferBody + body, err := ioutil.ReadAll(gtx.Request.Body) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + err = cdc.UnmarshalJSON(body, &transferBody) if err != nil { - utils.WriteErrorResponse(&w, http.StatusUnauthorized, err.Error()) + httputils.NewError(gtx, http.StatusBadRequest, err) return } + transferBody, errCode, err := paramPreprocess(transferBody, kb) + if err != nil { + httputils.NewError(gtx, errCode, err) + } - res, err := cliCtx.BroadcastTx(txBytes) + txForSign, _, errMsg := composeTx(cdc, ctx, transferBody) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + if errMsg.Code() == sdk.CodeInternal { + httputils.NewError(gtx, http.StatusInternalServerError, err) + } else { + httputils.NewError(gtx, http.StatusBadRequest, err) + } return } - output, err := wire.MarshalJSONIndent(cdc, res) + if transferBody.Generate { + httputils.NormalResponse(gtx, txForSign.Bytes()) + return + } + + output, errCode, err := signAndBroadcase(cdc, ctx, txForSign, transferBody, kb) if err != nil { - utils.WriteErrorResponse(&w, http.StatusInternalServerError, err.Error()) + httputils.NewError(gtx, errCode, err) return } + httputils.NormalResponse(gtx, output) + } +} - w.Write(output) +// paramPreprocess performs transferBody preprocess +func paramPreprocess(body transferBody, kb keys.Keybase) (transferBody, int, error) { + if body.Name == "" { + if !body.Generate { + return transferBody{}, http.StatusBadRequest, fmt.Errorf("missing key name, can't sign transaction") + } + if body.FromAddress == "" { + return transferBody{}, http.StatusBadRequest, fmt.Errorf("both the key name and fromAddreass are missed") + } } + + if body.Name != "" { + info, err := kb.Get(body.Name) + if err != nil { + return transferBody{}, http.StatusBadRequest, err + } + if body.FromAddress == "" { + addressFromPubKey, err := bech32.ConvertAndEncode(sdk.Bech32PrefixAccAddr, info.GetPubKey().Address().Bytes()) + if err != nil { + return transferBody{}, http.StatusInternalServerError, err + } + body.FromAddress = addressFromPubKey + } else { + fromAddress, err := sdk.AccAddressFromBech32(body.FromAddress) + if err != nil { + return transferBody{}, http.StatusBadRequest, err + } + + if !bytes.Equal(info.GetPubKey().Address(), fromAddress) { + return transferBody{}, http.StatusBadRequest, fmt.Errorf("the fromAddress doesn't equal to the address of sign key") + } + } + } + return body, 0, nil } + +// signAndBroadcase perform transaction sign and broadcast operation +func signAndBroadcase(cdc *wire.Codec, ctx context.CLIContext, txForSign auth.StdSignMsg, transferBody transferBody, kb keys.Keybase) ([]byte, int, error) { + + if transferBody.Name == "" || transferBody.Password == "" { + return nil, http.StatusBadRequest, fmt.Errorf("missing key name or password in signning transaction") + } + + sig, pubkey, err := kb.Sign(transferBody.Name, transferBody.Password, txForSign.Bytes()) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + sigs := []auth.StdSignature{{ + AccountNumber: txForSign.AccountNumber, + Sequence: txForSign.Sequence, + PubKey: pubkey, + Signature: sig, + }} + + txBytes, err := ctx.Codec.MarshalBinary(auth.NewStdTx(txForSign.Msgs, txForSign.Fee, sigs, txForSign.Memo)) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + res, err := ctx.BroadcastTx(txBytes) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + output, err := wire.MarshalJSONIndent(cdc, res) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + return output, 0, nil +} + +// composeTx perform StdSignMsg building operation +func composeTx(cdc *wire.Codec, ctx context.CLIContext, transferBody transferBody) (auth.StdSignMsg, authctx.TxContext, sdk.Error) { + + emptyMsg := auth.StdSignMsg{} + emptyTxContext := authctx.TxContext{} + + fromAddress, err := sdk.AccAddressFromBech32(transferBody.FromAddress) + if err != nil { + return emptyMsg, emptyTxContext, sdk.ErrInvalidAddress(err.Error()) + } + + toAddress, err := sdk.AccAddressFromBech32(transferBody.ToAddress) + if err != nil { + return emptyMsg, emptyTxContext, sdk.ErrInvalidAddress(err.Error()) + } + + // build message + msg := client.BuildMsg(fromAddress, toAddress, transferBody.Amount) + + accountNumber := transferBody.AccountNumber + sequence := transferBody.Sequence + gas := transferBody.Gas + fee := transferBody.Fee + + if transferBody.EnsureAccAndSeq { + if ctx.AccDecoder == nil { + ctx = ctx.WithAccountDecoder(authcmd.GetAccountDecoder(cdc)) + } + accountNumber, err = ctx.GetAccountNumber(fromAddress) + if err != nil { + return emptyMsg, emptyTxContext, sdk.ErrInternal(err.Error()) + } + + sequence, err = ctx.GetAccountSequence(fromAddress) + if err != nil { + return emptyMsg, emptyTxContext, sdk.ErrInternal(err.Error()) + } + } + + txCtx := authctx.TxContext{ + Codec: cdc, + Gas: gas, + Fee: fee, + ChainID: transferBody.ChainID, + AccountNumber: accountNumber, + Sequence: sequence, + } + + if txCtx.Gas == 0 { + newCtx, err := utils.EnrichCtxWithGas(txCtx, ctx, transferBody.Name, transferBody.Password, []sdk.Msg{msg}) + if err != nil { + return emptyMsg, emptyTxContext, sdk.ErrInternal(err.Error()) + } + txCtx = newCtx + } + + txForSign, err := txCtx.Build([]sdk.Msg{msg}) + if err != nil { + return emptyMsg, emptyTxContext, sdk.ErrInternal(err.Error()) + } + + return txForSign, txCtx, nil +} \ No newline at end of file diff --git a/x/stake/client/rest/query.go b/x/stake/client/rest/query.go index a040c7541c0a..bb409f3d04d2 100644 --- a/x/stake/client/rest/query.go +++ b/x/stake/client/rest/query.go @@ -14,6 +14,8 @@ import ( "github.com/cosmos/cosmos-sdk/x/stake/types" "github.com/gorilla/mux" + "github.com/gin-gonic/gin" + "github.com/cosmos/cosmos-sdk/client/httputils" ) const storeName = "stake" @@ -567,6 +569,7 @@ func validatorHandlerFn(cliCtx context.CLIContext, cdc *wire.Codec) http.Handler } } + // HTTP request handler to query the pool information func poolHandlerFn(cliCtx context.CLIContext, cdc *wire.Codec) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -626,3 +629,483 @@ func paramsHandlerFn(cliCtx context.CLIContext, cdc *wire.Codec) http.HandlerFun w.Write(output) } } + +func registerSwaggerQueryRoutes(routerGroup *gin.RouterGroup, ctx context.CLIContext, cdc *wire.Codec) { + routerGroup.GET("/stake/delegators/:delegatorAddr", delegatorHandlerCreation(cdc, ctx)) + routerGroup.GET("/stake/delegators/:delegatorAddr/txs", delegatorTxsHandlerCreation(cdc, ctx)) + routerGroup.GET("/stake/delegators/:delegatorAddr/validators", delegatorValidatorsHandlerCreation(cdc, ctx)) + routerGroup.GET("/stake/delegators/:delegatorAddr/validators/:validatorAddr", delegatorValidatorHandlerCreation(cdc, ctx)) + routerGroup.GET("/stake/delegators/:delegatorAddr/delegations/:validatorAddr", delegationHandlerCreation(cdc, ctx)) + routerGroup.GET("/stake/delegators/:delegatorAddr/unbonding_delegations/:validatorAddr", unbondingDelegationsHandlerCreation(cdc, ctx)) + routerGroup.GET("/stake/validators", validatorsHandlerCreation(cdc, ctx)) + routerGroup.GET("/stake/validators/:addr", validatorHandlerCreation(cdc, ctx)) + routerGroup.GET("/stake/pool", poolHandlerCreation(cdc, ctx)) + routerGroup.GET("/stake/parameters", paramsHandlerCreation(cdc, ctx)) +} + +func delegatorHandlerCreation(cdc *wire.Codec, cliCtx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + var validatorAddr sdk.AccAddress + var delegationSummary = DelegationSummary{} + + // read parameters + bech32delegator := gtx.Param("delegatorAddr") + + delegatorAddr, err := sdk.AccAddressFromBech32(bech32delegator) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + // Get all validators using key + validators, statusCode, errMsg, err := getBech32Validators(storeName, cliCtx, cdc) + if err != nil { + httputils.NewError(gtx, statusCode, fmt.Errorf("%s%s", errMsg, err.Error())) + return + } + + for _, validator := range validators { + validatorAddr = validator.Operator + + // Delegations + delegations, statusCode, errMsg, err := getDelegatorDelegations(cliCtx, cdc, delegatorAddr, validatorAddr) + if err != nil { + httputils.NewError(gtx, statusCode, fmt.Errorf("%s%s", errMsg, err.Error())) + return + } + if statusCode != http.StatusNoContent { + delegationSummary.Delegations = append(delegationSummary.Delegations, delegations) + } + + // Undelegations + unbondingDelegation, statusCode, errMsg, err := getDelegatorUndelegations(cliCtx, cdc, delegatorAddr, validatorAddr) + if err != nil { + httputils.NewError(gtx, statusCode, fmt.Errorf("%s%s", errMsg, err.Error())) + return + } + if statusCode != http.StatusNoContent { + delegationSummary.UnbondingDelegations = append(delegationSummary.UnbondingDelegations, unbondingDelegation) + } + + // Redelegations + // only querying redelegations to a validator as this should give us already all relegations + // if we also would put in redelegations from, we would have every redelegation double + redelegations, statusCode, errMsg, err := getDelegatorRedelegations(cliCtx, cdc, delegatorAddr, validatorAddr) + if err != nil { + httputils.NewError(gtx, statusCode, fmt.Errorf("%s%s", errMsg, err.Error())) + return + } + if statusCode != http.StatusNoContent { + delegationSummary.Redelegations = append(delegationSummary.Redelegations, redelegations) + } + } + + output, err := cdc.MarshalJSON(delegationSummary) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + httputils.NormalResponse(gtx, output) + } +} + +// nolint: gocyclo +func delegatorTxsHandlerCreation(cdc *wire.Codec, cliCtx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + var output []byte + var typesQuerySlice []string + + delegatorAddr := gtx.Param("delegatorAddr") + + _, err := sdk.AccAddressFromBech32(delegatorAddr) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + node, err := cliCtx.GetNode() + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("Couldn't get current Node information. Error: %s", err.Error())) + return + } + + // Get values from query + typesQuery := gtx.Query("type") + trimmedQuery := strings.TrimSpace(typesQuery) + if len(trimmedQuery) != 0 { + typesQuerySlice = strings.Split(trimmedQuery, " ") + } + + noQuery := len(typesQuerySlice) == 0 + isBondTx := contains(typesQuerySlice, "bond") + isUnbondTx := contains(typesQuerySlice, "unbond") + isRedTx := contains(typesQuerySlice, "redelegate") + var txs = []tx.Info{} + var actions []string + + switch { + case isBondTx: + actions = append(actions, string(tags.ActionDelegate)) + case isUnbondTx: + actions = append(actions, string(tags.ActionBeginUnbonding)) + actions = append(actions, string(tags.ActionCompleteUnbonding)) + case isRedTx: + actions = append(actions, string(tags.ActionBeginRedelegation)) + actions = append(actions, string(tags.ActionCompleteRedelegation)) + case noQuery: + actions = append(actions, string(tags.ActionDelegate)) + actions = append(actions, string(tags.ActionBeginUnbonding)) + actions = append(actions, string(tags.ActionCompleteUnbonding)) + actions = append(actions, string(tags.ActionBeginRedelegation)) + actions = append(actions, string(tags.ActionCompleteRedelegation)) + default: + httputils.NewError(gtx, http.StatusNoContent, nil) + return + } + + for _, action := range actions { + foundTxs, errQuery := queryTxs(node, cdc, action, delegatorAddr) + if errQuery != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("error querying transactions. Error: %s", errQuery.Error())) + } + txs = append(txs, foundTxs...) + } + + output, err = cdc.MarshalJSON(txs) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + httputils.NormalResponse(gtx, output) + } +} + +func delegatorValidatorsHandlerCreation(cdc *wire.Codec, cliCtx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + var validatorAccAddr sdk.AccAddress + var bondedValidators []types.BechValidator + + // read parameters + bech32delegator := gtx.Param("delegatorAddr") + + delegatorAddr, err := sdk.AccAddressFromBech32(bech32delegator) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + // Get all validators using key + kvs, err := cliCtx.QuerySubspace(stake.ValidatorsKey, storeName) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't query validators. Error: %s", err.Error())) + return + } else if len(kvs) == 0 { + // the query will return empty if there are no validators + httputils.NewError(gtx, http.StatusNoContent, nil) + return + } + + validators, err := getValidators(kvs, cdc) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + for _, validator := range validators { + // get all transactions from the delegator to val and append + validatorAccAddr = validator.Operator + + validator, statusCode, errMsg, errRes := getDelegatorValidator(cliCtx, cdc, delegatorAddr, validatorAccAddr) + if errRes != nil { + httputils.NewError(gtx, statusCode, fmt.Errorf("%s%s", errMsg, errRes.Error())) + return + } else if statusCode == http.StatusNoContent { + continue + } + + bondedValidators = append(bondedValidators, validator) + } + output, err := cdc.MarshalJSON(bondedValidators) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + httputils.NormalResponse(gtx, output) + } +} + +func delegatorValidatorHandlerCreation(cdc *wire.Codec, cliCtx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + // read parameters + var output []byte + bech32delegator := gtx.Param("delegatorAddr") + bech32validator := gtx.Param("validatorAddr") + + delegatorAddr, err := sdk.AccAddressFromBech32(bech32delegator) + validatorAccAddr, err := sdk.AccAddressFromBech32(bech32validator) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + // Check if there if the delegator is bonded or redelegated to the validator + + validator, statusCode, errMsg, err := getDelegatorValidator(cliCtx, cdc, delegatorAddr, validatorAccAddr) + if err != nil { + httputils.NewError(gtx, statusCode, fmt.Errorf("%s%s", errMsg, err.Error())) + return + } else if statusCode == http.StatusNoContent { + httputils.NewError(gtx, http.StatusNoContent, nil) + return + } + output, err = cdc.MarshalJSON(validator) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + httputils.NormalResponse(gtx, output) + } +} + +func delegationHandlerCreation(cdc *wire.Codec, cliCtx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + // read parameters + bech32delegator := gtx.Param("delegatorAddr") + bech32validator := gtx.Param("validatorAddr") + + delegatorAddr, err := sdk.AccAddressFromBech32(bech32delegator) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + validatorAddr, err := sdk.AccAddressFromBech32(bech32validator) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + validatorAddrAcc := sdk.AccAddress(validatorAddr) + + key := stake.GetDelegationKey(delegatorAddr, validatorAddrAcc) + + res, err := cliCtx.QueryStore(key, storeName) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't query delegation. Error: %s", err.Error())) + return + } + + // the query will return empty if there is no data for this record + if len(res) == 0 { + httputils.NewError(gtx, http.StatusNoContent, nil) + return + } + + delegation, err := types.UnmarshalDelegation(cdc, key, res) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + outputDelegation := DelegationWithoutRat{ + DelegatorAddr: delegation.DelegatorAddr, + ValidatorAddr: delegation.ValidatorAddr, + Height: delegation.Height, + Shares: delegation.Shares.String(), + } + + output, err := cdc.MarshalJSON(outputDelegation) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + httputils.NormalResponse(gtx, output) + } +} + +func unbondingDelegationsHandlerCreation(cdc *wire.Codec, cliCtx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + bech32delegator := gtx.Param("delegatorAddr") + bech32validator := gtx.Param("validatorAddr") + + delegatorAddr, err := sdk.AccAddressFromBech32(bech32delegator) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + validatorAddr, err := sdk.AccAddressFromBech32(bech32validator) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + validatorAddrAcc := sdk.AccAddress(validatorAddr) + + key := stake.GetUBDKey(delegatorAddr, validatorAddrAcc) + + res, err := cliCtx.QueryStore(key, storeName) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't query unbonding-delegation. Error: %s", err.Error())) + return + } + + // the query will return empty if there is no data for this record + if len(res) == 0 { + httputils.NewError(gtx, http.StatusNoContent, nil) + return + } + + ubd, err := types.UnmarshalUBD(cdc, key, res) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't unmarshall unbonding-delegation. Error: %s", err.Error())) + return + } + + // unbondings will be a list in the future but is not yet, but we want to keep the API consistent + ubdArray := []stake.UnbondingDelegation{ubd} + + output, err := cdc.MarshalJSON(ubdArray) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't marshall unbonding-delegation. Error: %s", err.Error())) + return + } + + httputils.NormalResponse(gtx, output) + } +} + +func validatorsHandlerCreation(cdc *wire.Codec, cliCtx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + kvs, err := cliCtx.QuerySubspace(stake.ValidatorsKey, storeName) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't query validators. Error: %s", err.Error())) + return + } + + // the query will return empty if there are no validators + if len(kvs) == 0 { + httputils.NewError(gtx, http.StatusNoContent, nil) + return + } + + validators, err := getValidators(kvs, cdc) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + output, err := cdc.MarshalJSON(validators) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + httputils.NormalResponse(gtx, output) + } +} + +func validatorHandlerCreation(cdc *wire.Codec, cliCtx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + var output []byte + // read parameters + bech32validatorAddr := gtx.Param("addr") + valAddress, err := sdk.AccAddressFromBech32(bech32validatorAddr) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + key := stake.GetValidatorKey(valAddress) + + res, err := cliCtx.QueryStore(key, storeName) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't query validator, error: %s", err.Error())) + return + } + + // the query will return empty if there is no data for this record + if len(res) == 0 { + httputils.NewError(gtx, http.StatusNoContent, nil) + return + } + + validator, err := types.UnmarshalValidator(cdc, valAddress, res) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + bech32Validator, err := validator.Bech32Validator() + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + output, err = cdc.MarshalJSON(bech32Validator) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("Error: %s", err.Error())) + return + } + + if output == nil { + httputils.NewError(gtx, http.StatusNoContent, nil) + return + } + httputils.NormalResponse(gtx, output) + } +} + +// HTTP request handler to query the pool information +func poolHandlerCreation(cdc *wire.Codec, cliCtx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + key := stake.PoolKey + + res, err := cliCtx.QueryStore(key, storeName) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't query pool. Error: %s", err.Error())) + return + } + + pool, err := types.UnmarshalPool(cdc, res) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + output, err := cdc.MarshalJSON(pool) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + httputils.NormalResponse(gtx, output) + } +} + +// HTTP request handler to query the staking params values +func paramsHandlerCreation(cdc *wire.Codec, cliCtx context.CLIContext) gin.HandlerFunc { + return func(gtx *gin.Context) { + key := stake.ParamKey + + res, err := cliCtx.QueryStore(key, storeName) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't query parameters. Error: %s", err.Error())) + return + } + + params, err := types.UnmarshalParams(cdc, res) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + output, err := cdc.MarshalJSON(params) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + httputils.NormalResponse(gtx, output) + } +} diff --git a/x/stake/client/rest/rest.go b/x/stake/client/rest/rest.go index 7c2fdf905207..036685122413 100644 --- a/x/stake/client/rest/rest.go +++ b/x/stake/client/rest/rest.go @@ -6,6 +6,7 @@ import ( "github.com/cosmos/cosmos-sdk/wire" "github.com/gorilla/mux" + "github.com/gin-gonic/gin" ) // RegisterRoutes registers staking-related REST handlers to a router @@ -13,3 +14,9 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *wire.Codec, k registerQueryRoutes(cliCtx, r, cdc) registerTxRoutes(cliCtx, r, cdc, kb) } + +// RegisterSwaggerRoutes registers staking related routes to Gaia-lite server +func RegisterSwaggerRoutes(routerGroup *gin.RouterGroup, ctx context.CLIContext, cdc *wire.Codec, kb keys.Keybase) { + registerSwaggerQueryRoutes(routerGroup, ctx, cdc) + registerSwaggerTxRoutes(routerGroup, ctx, cdc, kb) +} \ No newline at end of file diff --git a/x/stake/client/rest/tx.go b/x/stake/client/rest/tx.go index 3d7d419a3a64..dff3cb6c63d1 100644 --- a/x/stake/client/rest/tx.go +++ b/x/stake/client/rest/tx.go @@ -17,6 +17,8 @@ import ( "github.com/gorilla/mux" ctypes "github.com/tendermint/tendermint/rpc/core/types" + "github.com/gin-gonic/gin" + "github.com/cosmos/cosmos-sdk/client/httputils" ) func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *wire.Codec, kb keys.Keybase) { @@ -316,3 +318,253 @@ func delegationsRequestHandlerFn(cdc *wire.Codec, kb keys.Keybase, cliCtx contex w.Write(output) } } + +func registerSwaggerTxRoutes(routerGroup *gin.RouterGroup, ctx context.CLIContext, cdc *wire.Codec, kb keys.Keybase) { + routerGroup.POST("/stake/delegators/:delegatorAddr/delegations", delegationsSwaggerTxHandlerFn(cdc, ctx, kb)) +} + +// nolint: gocyclo +// TODO: Split this up into several smaller functions, and remove the above nolint +// TODO: use sdk.ValAddress instead of sdk.AccAddress for validators in messages +func delegationsSwaggerTxHandlerFn(cdc *wire.Codec, cliCtx context.CLIContext, kb keys.Keybase) gin.HandlerFunc { + return func(gtx *gin.Context) { + var m EditDelegationsBody + + body, err := ioutil.ReadAll(gtx.Request.Body) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + err = cdc.UnmarshalJSON(body, &m) + if err != nil { + httputils.NewError(gtx, http.StatusBadRequest, err) + return + } + + info, err := kb.Get(m.LocalAccountName) + if err != nil { + httputils.NewError(gtx, http.StatusUnauthorized, err) + return + } + + // build messages + messages := make([]sdk.Msg, len(m.Delegations)+ + len(m.BeginRedelegates)+ + len(m.CompleteRedelegates)+ + len(m.BeginUnbondings)+ + len(m.CompleteUnbondings)) + + i := 0 + for _, msg := range m.Delegations { + delegatorAddr, err := sdk.AccAddressFromBech32(msg.DelegatorAddr) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode delegator. error: %s", err.Error())) + return + } + + validatorAddr, err := sdk.AccAddressFromBech32(msg.ValidatorAddr) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode validator. error: %s", err.Error())) + return + } + + if !bytes.Equal(info.GetPubKey().Address(), delegatorAddr) { + httputils.NewError(gtx, http.StatusUnauthorized, fmt.Errorf("must use own delegator address")) + return + } + + messages[i] = stake.MsgDelegate{ + DelegatorAddr: delegatorAddr, + ValidatorAddr: validatorAddr, + Delegation: msg.Delegation, + } + + i++ + } + + for _, msg := range m.BeginRedelegates { + delegatorAddr, err := sdk.AccAddressFromBech32(msg.DelegatorAddr) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode delegator. error: %s", err.Error())) + return + } + + if !bytes.Equal(info.GetPubKey().Address(), delegatorAddr) { + httputils.NewError(gtx, http.StatusUnauthorized, fmt.Errorf("must use own delegator address")) + return + } + + validatorSrcAddr, err := sdk.AccAddressFromBech32(msg.ValidatorSrcAddr) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode validator. error: %s", err.Error())) + return + } + validatorDstAddr, err := sdk.AccAddressFromBech32(msg.ValidatorDstAddr) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode validator. error: %s", err.Error())) + return + } + + shares, err := sdk.NewDecFromStr(msg.SharesAmount) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode shares amount. error: %s", err.Error())) + return + } + + messages[i] = stake.MsgBeginRedelegate{ + DelegatorAddr: delegatorAddr, + ValidatorSrcAddr: validatorSrcAddr, + ValidatorDstAddr: validatorDstAddr, + SharesAmount: shares, + } + + i++ + } + + for _, msg := range m.CompleteRedelegates { + delegatorAddr, err := sdk.AccAddressFromBech32(msg.DelegatorAddr) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode delegator. error: %s", err.Error())) + return + } + + validatorSrcAddr, err := sdk.AccAddressFromBech32(msg.ValidatorSrcAddr) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode validator. error: %s", err.Error())) + return + } + validatorDstAddr, err := sdk.AccAddressFromBech32(msg.ValidatorDstAddr) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode validator. error: %s", err.Error())) + return + } + + if !bytes.Equal(info.GetPubKey().Address(), delegatorAddr) { + httputils.NewError(gtx, http.StatusUnauthorized, fmt.Errorf("must use own delegator address")) + return + } + + messages[i] = stake.MsgCompleteRedelegate{ + DelegatorAddr: delegatorAddr, + ValidatorSrcAddr: validatorSrcAddr, + ValidatorDstAddr: validatorDstAddr, + } + + i++ + } + + for _, msg := range m.BeginUnbondings { + delegatorAddr, err := sdk.AccAddressFromBech32(msg.DelegatorAddr) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode delegator. error: %s", err.Error())) + return + } + + if !bytes.Equal(info.GetPubKey().Address(), delegatorAddr) { + httputils.NewError(gtx, http.StatusUnauthorized, fmt.Errorf("must use own delegator address")) + return + } + + validatorAddr, err := sdk.AccAddressFromBech32(msg.ValidatorAddr) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode validator. error: %s", err.Error())) + return + } + + shares, err := sdk.NewDecFromStr(msg.SharesAmount) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode shares amount. error: %s", err.Error())) + return + } + + messages[i] = stake.MsgBeginUnbonding{ + DelegatorAddr: delegatorAddr, + ValidatorAddr: validatorAddr, + SharesAmount: shares, + } + + i++ + } + + for _, msg := range m.CompleteUnbondings { + delegatorAddr, err := sdk.AccAddressFromBech32(msg.DelegatorAddr) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode delegator. error: %s", err.Error())) + return + } + + validatorAddr, err := sdk.AccAddressFromBech32(msg.ValidatorAddr) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf("couldn't decode validator. error: %s", err.Error())) + return + } + + if !bytes.Equal(info.GetPubKey().Address(), delegatorAddr) { + httputils.NewError(gtx, http.StatusUnauthorized, fmt.Errorf("must use own delegator address")) + return + } + + messages[i] = stake.MsgCompleteUnbonding{ + DelegatorAddr: delegatorAddr, + ValidatorAddr: validatorAddr, + } + + i++ + } + + txCtx := authcliCtx.TxContext{ + Codec: cdc, + ChainID: m.ChainID, + Gas: m.Gas, + } + + // sign messages + signedTxs := make([][]byte, len(messages[:])) + for i, msg := range messages { + // increment sequence for each message + txCtx = txCtx.WithAccountNumber(m.AccountNumber) + txCtx = txCtx.WithSequence(m.Sequence) + + m.Sequence++ + + if m.Gas == 0 { + newCtx, err := utils.EnrichCtxWithGas(txCtx, cliCtx, m.LocalAccountName, m.Password, []sdk.Msg{msg}) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf(err.Error())) + return + } + txCtx = newCtx + } + + txBytes, err := txCtx.BuildAndSign(m.LocalAccountName, m.Password, []sdk.Msg{msg}) + if err != nil { + httputils.NewError(gtx, http.StatusUnauthorized, fmt.Errorf(err.Error())) + return + } + + signedTxs[i] = txBytes + } + + // send + // XXX the operation might not be atomic if a tx fails + // should we have a sdk.MultiMsg type to make sending atomic? + results := make([]*ctypes.ResultBroadcastTxCommit, len(signedTxs[:])) + for i, txBytes := range signedTxs { + res, err := cliCtx.BroadcastTx(txBytes) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, fmt.Errorf(err.Error())) + return + } + + results[i] = res + } + + output, err := wire.MarshalJSONIndent(cdc, results) + if err != nil { + httputils.NewError(gtx, http.StatusInternalServerError, err) + return + } + + httputils.NormalResponse(gtx, output) + } +} \ No newline at end of file