diff --git a/PENDING.md b/PENDING.md index 5b3d5fc857..7a101cfbc9 100644 --- a/PENDING.md +++ b/PENDING.md @@ -34,6 +34,7 @@ FEATURES * Gaia * SDK + * [querier] added custom querier functionality, so ABCI query requests can be handled by keepers * Tendermint diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index cf63f1f4d2..365d7b31f2 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -41,13 +41,14 @@ const ( // BaseApp reflects the ABCI application implementation. type BaseApp struct { // initialized on creation - Logger log.Logger - name string // application name from abci.Info - db dbm.DB // common DB backend - cms sdk.CommitMultiStore // Main (uncached) state - router Router // handle any kind of message - codespacer *sdk.Codespacer // handle module codespacing - txDecoder sdk.TxDecoder // unmarshal []byte into sdk.Tx + Logger log.Logger + name string // application name from abci.Info + db dbm.DB // common DB backend + cms sdk.CommitMultiStore // Main (uncached) state + router Router // handle any kind of message + queryRouter QueryRouter // router for redirecting query calls + codespacer *sdk.Codespacer // handle module codespacing + txDecoder sdk.TxDecoder // unmarshal []byte into sdk.Tx anteHandler sdk.AnteHandler // ante handler for fee and auth @@ -84,13 +85,14 @@ var _ abci.Application = (*BaseApp)(nil) // Accepts variable number of option functions, which act on the BaseApp to set configuration choices func NewBaseApp(name string, logger log.Logger, db dbm.DB, txDecoder sdk.TxDecoder, options ...func(*BaseApp)) *BaseApp { app := &BaseApp{ - Logger: logger, - name: name, - db: db, - cms: store.NewCommitMultiStore(db), - router: NewRouter(), - codespacer: sdk.NewCodespacer(), - txDecoder: txDecoder, + Logger: logger, + name: name, + db: db, + cms: store.NewCommitMultiStore(db), + router: NewRouter(), + queryRouter: NewQueryRouter(), + codespacer: sdk.NewCodespacer(), + txDecoder: txDecoder, } // Register the undefined & root codespaces, which should not be used by @@ -266,6 +268,7 @@ func (app *BaseApp) FilterPeerByPubKey(info string) abci.ResponseQuery { return abci.ResponseQuery{} } +// Splits a string path using the delimter '/'. i.e. "this/is/funny" becomes []string{"this", "is", "funny"} func splitPath(requestPath string) (path []string) { path = strings.Split(requestPath, "/") // first element is empty string @@ -291,6 +294,8 @@ func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) { return handleQueryStore(app, path, req) case "p2p": return handleQueryP2P(app, path, req) + case "custom": + return handleQueryCustom(app, path, req) } msg := "unknown query path" @@ -362,6 +367,33 @@ func handleQueryP2P(app *BaseApp, path []string, req abci.RequestQuery) (res abc return sdk.ErrUnknownRequest(msg).QueryResult() } +func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) { + // path[0] should be "custom" because "/custom" prefix is required for keeper queries. + // the queryRouter routes using path[1]. For example, in the path "custom/gov/proposal", queryRouter routes using "gov" + if path[1] == "" { + sdk.ErrUnknownRequest("No route for custom query specified").QueryResult() + } + querier := app.queryRouter.Route(path[1]) + if querier == nil { + sdk.ErrUnknownRequest(fmt.Sprintf("no custom querier found for route %s", path[1])).QueryResult() + } + + ctx := sdk.NewContext(app.cms.CacheMultiStore(), app.checkState.ctx.BlockHeader(), true, app.Logger) + // Passes the rest of the path as an argument to the querier. + // For example, in the path "custom/gov/proposal/test", the gov querier gets []string{"proposal", "test"} as the path + resBytes, err := querier(ctx, path[2:], req) + if err != nil { + return abci.ResponseQuery{ + Code: uint32(err.ABCICode()), + Log: err.ABCILog(), + } + } + return abci.ResponseQuery{ + Code: uint32(sdk.ABCICodeOK), + Value: resBytes, + } +} + // BeginBlock implements the ABCI application interface. func (app *BaseApp) BeginBlock(req abci.RequestBeginBlock) (res abci.ResponseBeginBlock) { if app.cms.TracingEnabled() { diff --git a/baseapp/queryrouter.go b/baseapp/queryrouter.go new file mode 100644 index 0000000000..23cfad0721 --- /dev/null +++ b/baseapp/queryrouter.go @@ -0,0 +1,41 @@ +package baseapp + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// QueryRouter provides queryables for each query path. +type QueryRouter interface { + AddRoute(r string, h sdk.Querier) (rtr QueryRouter) + Route(path string) (h sdk.Querier) +} + +type queryrouter struct { + routes map[string]sdk.Querier +} + +// nolint +// NewRouter - create new router +// TODO either make Function unexported or make return type (router) Exported +func NewQueryRouter() *queryrouter { + return &queryrouter{ + routes: map[string]sdk.Querier{}, + } +} + +// AddRoute - Adds an sdk.Querier to the route provided. Panics on duplicate +func (rtr *queryrouter) AddRoute(r string, q sdk.Querier) QueryRouter { + if !isAlphaNumeric(r) { + panic("route expressions can only contain alphanumeric characters") + } + if rtr.routes[r] != nil { + panic("route has already been initialized") + } + rtr.routes[r] = q + return rtr +} + +// Returns the sdk.Querier for a certain route path +func (rtr *queryrouter) Route(path string) (h sdk.Querier) { + return rtr.routes[path] +} diff --git a/baseapp/setters.go b/baseapp/setters.go index 86a647d32c..a8b1591a73 100644 --- a/baseapp/setters.go +++ b/baseapp/setters.go @@ -74,6 +74,9 @@ func (app *BaseApp) Router() Router { } return app.router } +func (app *BaseApp) QueryRouter() QueryRouter { + return app.queryRouter +} func (app *BaseApp) Seal() { app.sealed = true } func (app *BaseApp) IsSealed() bool { return app.sealed } func (app *BaseApp) enforceSeal() { diff --git a/client/context/query.go b/client/context/query.go index 081f723b5c..68676f7415 100644 --- a/client/context/query.go +++ b/client/context/query.go @@ -31,6 +31,11 @@ func (ctx CLIContext) Query(path string) (res []byte, err error) { return ctx.query(path, nil) } +// Query information about the connected node with a data payload +func (ctx CLIContext) QueryWithData(path string, data []byte) (res []byte, err error) { + return ctx.query(path, data) +} + // QueryStore performs a query from a Tendermint node with the provided key and // store name. func (ctx CLIContext) QueryStore(key cmn.HexBytes, storeName string) (res []byte, err error) { diff --git a/cmd/gaia/app/app.go b/cmd/gaia/app/app.go index a96efa6b17..f79ffb3065 100644 --- a/cmd/gaia/app/app.go +++ b/cmd/gaia/app/app.go @@ -105,6 +105,9 @@ func NewGaiaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, baseAppOptio AddRoute("slashing", slashing.NewHandler(app.slashingKeeper)). AddRoute("gov", gov.NewHandler(app.govKeeper)) + app.QueryRouter(). + AddRoute("gov", gov.NewQuerier(app.govKeeper)) + // initialize BaseApp app.SetInitChainer(app.initChainer) app.SetBeginBlocker(app.BeginBlocker) diff --git a/docs/sdk/modules.md b/docs/sdk/modules.md index b9249cf3e9..7df8fb2bb0 100644 --- a/docs/sdk/modules.md +++ b/docs/sdk/modules.md @@ -6,7 +6,7 @@ The Cosmos SDK has all the necessary pre-built modules to add functionality on top of a `BaseApp`, which is the template to build a blockchain dApp in Cosmos. In this context, a `module` is a fundamental unit in the Cosmos SDK. -Each module is an extension of the `BaseApp`'s functionalities that defines transactions, handles application state and manages the state transition logic. Each module also contains handlers for messages and transactions, as well as REST and CLI for secure user interactions. +Each module is an extension of the `BaseApp`'s functionalities that defines transactions, handles application state and manages the state transition logic. Each module also contains handlers for messages and transactions, queriers for handling query requests, as well as REST and CLI for secure user interactions. Some of the most important modules in the SDK are: diff --git a/types/account.go b/types/account.go index 92e2988f24..00076b5299 100644 --- a/types/account.go +++ b/types/account.go @@ -1,6 +1,7 @@ package types import ( + "bytes" "encoding/hex" "encoding/json" "errors" @@ -108,6 +109,23 @@ func (bz AccAddress) Format(s fmt.State, verb rune) { } } +// Returns boolean for whether two AccAddresses are Equal +func (bz AccAddress) Equals(bz2 AccAddress) bool { + if bz.Empty() && bz2.Empty() { + return true + } + return bytes.Compare(bz.Bytes(), bz2.Bytes()) == 0 +} + +// Returns boolean for whether an AccAddress is empty +func (bz AccAddress) Empty() bool { + if bz == nil { + return true + } + bz2 := AccAddress{} + return bytes.Compare(bz.Bytes(), bz2.Bytes()) == 0 +} + //__________________________________________________________ // AccAddress a wrapper around bytes meant to represent a validator address @@ -192,6 +210,23 @@ func (bz ValAddress) Format(s fmt.State, verb rune) { } } +// Returns boolean for whether two ValAddresses are Equal +func (bz ValAddress) Equals(bz2 ValAddress) bool { + if bz.Empty() && bz2.Empty() { + return true + } + return bytes.Compare(bz.Bytes(), bz2.Bytes()) == 0 +} + +// Returns boolean for whether an AccAddress is empty +func (bz ValAddress) Empty() bool { + if bz == nil { + return true + } + bz2 := ValAddress{} + return bytes.Compare(bz.Bytes(), bz2.Bytes()) == 0 +} + // Bech32ifyAccPub takes AccountPubKey and returns the bech32 encoded string func Bech32ifyAccPub(pub crypto.PubKey) (string, error) { return bech32.ConvertAndEncode(Bech32PrefixAccPub, pub.Bytes()) diff --git a/types/queryable.go b/types/queryable.go new file mode 100644 index 0000000000..9223332bc8 --- /dev/null +++ b/types/queryable.go @@ -0,0 +1,6 @@ +package types + +import abci "github.com/tendermint/tendermint/abci/types" + +// Type for querier functions on keepers to implement to handle custom queries +type Querier = func(ctx Context, path []string, req abci.RequestQuery) (res []byte, err Error) diff --git a/x/gov/client/rest/rest.go b/x/gov/client/rest/rest.go index 22283a652e..5cdc7bda2b 100644 --- a/x/gov/client/rest/rest.go +++ b/x/gov/client/rest/rest.go @@ -3,7 +3,6 @@ package rest import ( "fmt" "net/http" - "strconv" "github.com/cosmos/cosmos-sdk/client/context" sdk "github.com/cosmos/cosmos-sdk/types" @@ -21,6 +20,7 @@ const ( RestDepositer = "depositer" RestVoter = "voter" RestProposalStatus = "status" + RestNumLatest = "latest" storeName = "gov" ) @@ -97,16 +97,13 @@ func depositHandlerFn(cdc *wire.Codec, cliCtx context.CLIContext) http.HandlerFu return } - proposalID, err := strconv.ParseInt(strProposalID, 10, 64) - if err != nil { - err := errors.Errorf("proposalID [%d] is not positive", proposalID) - w.Write([]byte(err.Error())) - + proposalID, ok := parseInt64OrReturnBadRequest(strProposalID, w) + if !ok { return } var req depositReq - err = buildReq(w, r, cdc, &req) + err := buildReq(w, r, cdc, &req) if err != nil { return } @@ -139,15 +136,13 @@ func voteHandlerFn(cdc *wire.Codec, cliCtx context.CLIContext) http.HandlerFunc return } - proposalID, err := strconv.ParseInt(strProposalID, 10, 64) - if err != nil { - err := errors.Errorf("proposalID [%d] is not positive", proposalID) - w.Write([]byte(err.Error())) + proposalID, ok := parseInt64OrReturnBadRequest(strProposalID, w) + if !ok { return } var req voteReq - err = buildReq(w, r, cdc, &req) + err := buildReq(w, r, cdc, &req) if err != nil { return } @@ -180,36 +175,33 @@ func queryProposalHandlerFn(cdc *wire.Codec) http.HandlerFunc { return } - proposalID, err := strconv.ParseInt(strProposalID, 10, 64) - if err != nil { - err := errors.Errorf("proposalID [%d] is not positive", proposalID) - w.Write([]byte(err.Error())) - + proposalID, ok := parseInt64OrReturnBadRequest(strProposalID, w) + if !ok { return } cliCtx := context.NewCLIContext().WithCodec(cdc) - res, err := cliCtx.QueryStore(gov.KeyProposal(proposalID), storeName) - if err != nil || len(res) == 0 { - err := errors.Errorf("proposalID [%d] does not exist", proposalID) - w.Write([]byte(err.Error())) - - return + params := gov.QueryProposalParams{ + ProposalID: proposalID, } - var proposal gov.Proposal - cdc.MustUnmarshalBinary(res, &proposal) - - output, err := wire.MarshalJSONIndent(cdc, proposal) + bz, err := cdc.MarshalJSON(params) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) + return + } + + res, err := cliCtx.QueryWithData("custom/gov/proposal", bz) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) return } - w.Write(output) + w.Write(res) } } @@ -227,12 +219,8 @@ func queryDepositHandlerFn(cdc *wire.Codec) http.HandlerFunc { return } - proposalID, err := strconv.ParseInt(strProposalID, 10, 64) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - err := errors.Errorf("proposalID [%d] is not positive", proposalID) - w.Write([]byte(err.Error())) - + proposalID, ok := parseInt64OrReturnBadRequest(strProposalID, w) + if !ok { return } @@ -255,36 +243,43 @@ func queryDepositHandlerFn(cdc *wire.Codec) http.HandlerFunc { cliCtx := context.NewCLIContext().WithCodec(cdc) - res, err := cliCtx.QueryStore(gov.KeyDeposit(proposalID, depositerAddr), storeName) - if err != nil || len(res) == 0 { - res, err := cliCtx.QueryStore(gov.KeyProposal(proposalID), storeName) - if err != nil || len(res) == 0 { - w.WriteHeader(http.StatusNotFound) - err := errors.Errorf("proposalID [%d] does not exist", proposalID) - w.Write([]byte(err.Error())) + params := gov.QueryDepositParams{ + ProposalID: proposalID, + Depositer: depositerAddr, + } - return - } + bz, err := cdc.MarshalJSON(params) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } - w.WriteHeader(http.StatusNotFound) - err = errors.Errorf("depositer [%s] did not deposit on proposalID [%d]", bechDepositerAddr, proposalID) + res, err := cliCtx.QueryWithData("custom/gov/deposit", bz) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } var deposit gov.Deposit - cdc.MustUnmarshalBinary(res, &deposit) - - output, err := wire.MarshalJSONIndent(cdc, deposit) - if err != nil { - w.WriteHeader(http.StatusBadRequest) + cdc.UnmarshalJSON(res, &deposit) + if deposit.Empty() { + res, err := cliCtx.QueryWithData("custom/gov/proposal", cdc.MustMarshalBinary(gov.QueryProposalParams{params.ProposalID})) + if err != nil || len(res) == 0 { + w.WriteHeader(http.StatusNotFound) + err := errors.Errorf("proposalID [%d] does not exist", proposalID) + w.Write([]byte(err.Error())) + return + } + w.WriteHeader(http.StatusNotFound) + err = errors.Errorf("depositer [%s] did not deposit on proposalID [%d]", bechDepositerAddr, proposalID) w.Write([]byte(err.Error())) - return } - w.Write(output) + w.Write(res) } } @@ -301,12 +296,8 @@ func queryVoteHandlerFn(cdc *wire.Codec) http.HandlerFunc { return } - proposalID, err := strconv.ParseInt(strProposalID, 10, 64) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - err := errors.Errorf("proposalID [%s] is not positive", proposalID) - w.Write([]byte(err.Error())) - + proposalID, ok := parseInt64OrReturnBadRequest(strProposalID, w) + if !ok { return } @@ -328,36 +319,47 @@ func queryVoteHandlerFn(cdc *wire.Codec) http.HandlerFunc { cliCtx := context.NewCLIContext().WithCodec(cdc) - res, err := cliCtx.QueryStore(gov.KeyVote(proposalID, voterAddr), storeName) - if err != nil || len(res) == 0 { - res, err := cliCtx.QueryStore(gov.KeyProposal(proposalID), storeName) - if err != nil || len(res) == 0 { - w.WriteHeader(http.StatusNotFound) - err := errors.Errorf("proposalID [%d] does not exist", proposalID) - w.Write([]byte(err.Error())) + params := gov.QueryVoteParams{ + Voter: voterAddr, + ProposalID: proposalID, + } + bz, err := cdc.MarshalJSON(params) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } - return - } - - w.WriteHeader(http.StatusNotFound) - err = errors.Errorf("voter [%s] did not vote on proposalID [%d]", bechVoterAddr, proposalID) + res, err := cliCtx.QueryWithData("custom/gov/vote", bz) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } var vote gov.Vote - cdc.MustUnmarshalBinary(res, &vote) - - output, err := wire.MarshalJSONIndent(cdc, vote) - if err != nil { - w.WriteHeader(http.StatusBadRequest) + cdc.UnmarshalJSON(res, &vote) + if vote.Empty() { + bz, err := cdc.MarshalJSON(gov.QueryProposalParams{params.ProposalID}) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + res, err := cliCtx.QueryWithData("custom/gov/proposal", bz) + if err != nil || len(res) == 0 { + w.WriteHeader(http.StatusNotFound) + err := errors.Errorf("proposalID [%d] does not exist", proposalID) + w.Write([]byte(err.Error())) + return + } + w.WriteHeader(http.StatusNotFound) + err = errors.Errorf("voter [%s] did not deposit on proposalID [%d]", bechVoterAddr, proposalID) w.Write([]byte(err.Error())) - return } - - w.Write(output) + w.Write(res) } } @@ -376,59 +378,31 @@ func queryVotesOnProposalHandlerFn(cdc *wire.Codec) http.HandlerFunc { return } - proposalID, err := strconv.ParseInt(strProposalID, 10, 64) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - err := errors.Errorf("proposalID [%s] is not positive", proposalID) - w.Write([]byte(err.Error())) - + proposalID, ok := parseInt64OrReturnBadRequest(strProposalID, w) + if !ok { return } cliCtx := context.NewCLIContext().WithCodec(cdc) - res, err := cliCtx.QueryStore(gov.KeyProposal(proposalID), storeName) - if err != nil || len(res) == 0 { - err := errors.Errorf("proposalID [%d] does not exist", proposalID) - w.Write([]byte(err.Error())) - - return + params := gov.QueryVotesParams{ + ProposalID: proposalID, } - - var proposal gov.Proposal - cdc.MustUnmarshalBinary(res, &proposal) - - if proposal.GetStatus() != gov.StatusVotingPeriod { - err := errors.Errorf("proposal is not in Voting Period", proposalID) - w.Write([]byte(err.Error())) - - return - } - - res2, err := cliCtx.QuerySubspace(gov.KeyVotesSubspace(proposalID), storeName) - if err != nil { - err = errors.New("ProposalID doesn't exist") - w.Write([]byte(err.Error())) - return - } - - var votes []gov.Vote - - for i := 0; i < len(res2); i++ { - var vote gov.Vote - cdc.MustUnmarshalBinary(res2[i].Value, &vote) - votes = append(votes, vote) - } - - output, err := wire.MarshalJSONIndent(cdc, votes) + bz, err := cdc.MarshalJSON(params) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) - return } - w.Write(output) + res, err := cliCtx.QueryWithData("custom/gov/votes", bz) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + w.Write(res) } } @@ -439,24 +413,23 @@ func queryProposalsWithParameterFn(cdc *wire.Codec) http.HandlerFunc { bechVoterAddr := r.URL.Query().Get(RestVoter) bechDepositerAddr := r.URL.Query().Get(RestDepositer) strProposalStatus := r.URL.Query().Get(RestProposalStatus) + strNumLatest := r.URL.Query().Get(RestNumLatest) - var err error - var voterAddr sdk.AccAddress - var depositerAddr sdk.AccAddress - var proposalStatus gov.ProposalStatus + params := gov.QueryProposalsParams{} if len(bechVoterAddr) != 0 { - voterAddr, err = sdk.AccAddressFromBech32(bechVoterAddr) + voterAddr, err := sdk.AccAddressFromBech32(bechVoterAddr) if err != nil { w.WriteHeader(http.StatusBadRequest) err := errors.Errorf("'%s' needs to be bech32 encoded", RestVoter) w.Write([]byte(err.Error())) return } + params.Voter = voterAddr } if len(bechDepositerAddr) != 0 { - depositerAddr, err = sdk.AccAddressFromBech32(bechDepositerAddr) + depositerAddr, err := sdk.AccAddressFromBech32(bechDepositerAddr) if err != nil { w.WriteHeader(http.StatusBadRequest) err := errors.Errorf("'%s' needs to be bech32 encoded", RestDepositer) @@ -464,10 +437,11 @@ func queryProposalsWithParameterFn(cdc *wire.Codec) http.HandlerFunc { return } + params.Depositer = depositerAddr } if len(strProposalStatus) != 0 { - proposalStatus, err = gov.ProposalStatusFromString(strProposalStatus) + proposalStatus, err := gov.ProposalStatusFromString(strProposalStatus) if err != nil { w.WriteHeader(http.StatusBadRequest) err := errors.Errorf("'%s' is not a valid Proposal Status", strProposalStatus) @@ -475,63 +449,33 @@ func queryProposalsWithParameterFn(cdc *wire.Codec) http.HandlerFunc { return } + params.ProposalStatus = proposalStatus + } + if len(strNumLatest) != 0 { + numLatest, ok := parseInt64OrReturnBadRequest(strNumLatest, w) + if !ok { + return + } + params.NumLatestProposals = numLatest + } + + bz, err := cdc.MarshalJSON(params) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return } cliCtx := context.NewCLIContext().WithCodec(cdc) - res, err := cliCtx.QueryStore(gov.KeyNextProposalID, storeName) + res, err := cliCtx.QueryWithData("custom/gov/proposals", bz) if err != nil { - err = errors.New("no proposals exist yet and proposalID has not been set") + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } - var maxProposalID int64 - cdc.MustUnmarshalBinary(res, &maxProposalID) - - matchingProposals := []gov.Proposal{} - - for proposalID := int64(0); proposalID < maxProposalID; proposalID++ { - if voterAddr != nil { - res, err = cliCtx.QueryStore(gov.KeyVote(proposalID, voterAddr), storeName) - if err != nil || len(res) == 0 { - continue - } - } - - if depositerAddr != nil { - res, err = cliCtx.QueryStore(gov.KeyDeposit(proposalID, depositerAddr), storeName) - if err != nil || len(res) == 0 { - continue - } - } - - res, err = cliCtx.QueryStore(gov.KeyProposal(proposalID), storeName) - if err != nil || len(res) == 0 { - continue - } - - var proposal gov.Proposal - cdc.MustUnmarshalBinary(res, &proposal) - - if len(strProposalStatus) != 0 { - if proposal.GetStatus() != proposalStatus { - continue - } - } - - matchingProposals = append(matchingProposals, proposal) - } - - output, err := wire.MarshalJSONIndent(cdc, matchingProposals) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) - - return - } - - w.Write(output) + w.Write(res) } } diff --git a/x/gov/client/rest/util.go b/x/gov/client/rest/util.go index 58d96b591f..de94a8324c 100644 --- a/x/gov/client/rest/util.go +++ b/x/gov/client/rest/util.go @@ -1,8 +1,10 @@ package rest import ( + "fmt" "io/ioutil" "net/http" + "strconv" "github.com/cosmos/cosmos-sdk/client/context" sdk "github.com/cosmos/cosmos-sdk/types" @@ -100,3 +102,15 @@ func signAndBuild(w http.ResponseWriter, cliCtx context.CLIContext, baseReq base w.Write(output) } + +func parseInt64OrReturnBadRequest(s string, w http.ResponseWriter) (n int64, ok bool) { + var err error + n, err = strconv.ParseInt(s, 10, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + err := fmt.Errorf("'%s' is not a valid int64", s) + w.Write([]byte(err.Error())) + return 0, false + } + return n, true + } \ No newline at end of file diff --git a/x/gov/depositsvotes.go b/x/gov/depositsvotes.go index 19ed97f69d..b5c65931fc 100644 --- a/x/gov/depositsvotes.go +++ b/x/gov/depositsvotes.go @@ -15,6 +15,17 @@ type Vote struct { Option VoteOption `json:"option"` // option from OptionSet chosen by the voter } +// Returns whether 2 votes are equal +func (voteA Vote) Equals(voteB Vote) bool { + return voteA.Voter.Equals(voteB.Voter) && voteA.ProposalID == voteB.ProposalID && voteA.Option == voteB.Option +} + +// Returns whether a vote is empty +func (voteA Vote) Empty() bool { + voteB := Vote{} + return voteA.Equals(voteB) +} + // Deposit type Deposit struct { Depositer sdk.AccAddress `json:"depositer"` // Address of the depositer @@ -22,6 +33,17 @@ type Deposit struct { Amount sdk.Coins `json:"amount"` // Deposit amount } +// Returns whether 2 deposits are equal +func (depositA Deposit) Equals(depositB Deposit) bool { + return depositA.Depositer.Equals(depositB.Depositer) && depositA.ProposalID == depositB.ProposalID && depositA.Amount.IsEqual(depositB.Amount) +} + +// Returns whether a deposit is empty +func (depositA Deposit) Empty() bool { + depositB := Deposit{} + return depositA.Equals(depositB) +} + // Type that represents VoteOption as a byte type VoteOption byte diff --git a/x/gov/keeper.go b/x/gov/keeper.go index 8a23ad2485..e8dbbc4a7f 100644 --- a/x/gov/keeper.go +++ b/x/gov/keeper.go @@ -108,6 +108,52 @@ func (keeper Keeper) DeleteProposal(ctx sdk.Context, proposal Proposal) { store.Delete(KeyProposal(proposal.GetProposalID())) } +// nolint: gocyclo +// Get Proposal from store by ProposalID +func (keeper Keeper) GetProposalsFiltered(ctx sdk.Context, voterAddr sdk.AccAddress, depositerAddr sdk.AccAddress, status ProposalStatus, numLatest int64) []Proposal { + + maxProposalID, err := keeper.peekCurrentProposalID(ctx) + if err != nil { + return nil + } + + matchingProposals := []Proposal{} + + if numLatest <= 0 { + numLatest = maxProposalID + } + + for proposalID := maxProposalID - numLatest; proposalID < maxProposalID; proposalID++ { + if voterAddr != nil && len(voterAddr) != 0 { + _, found := keeper.GetVote(ctx, proposalID, voterAddr) + if !found { + continue + } + } + + if depositerAddr != nil && len(depositerAddr) != 0 { + _, found := keeper.GetDeposit(ctx, proposalID, depositerAddr) + if !found { + continue + } + } + + proposal := keeper.GetProposal(ctx, proposalID) + if proposal == nil { + continue + } + + if validProposalStatus(status) { + if proposal.GetStatus() != status { + continue + } + } + + matchingProposals = append(matchingProposals, proposal) + } + return matchingProposals +} + func (keeper Keeper) setInitialProposalID(ctx sdk.Context, proposalID int64) sdk.Error { store := ctx.KVStore(keeper.storeKey) bz := store.Get(KeyNextProposalID) @@ -121,16 +167,15 @@ func (keeper Keeper) setInitialProposalID(ctx sdk.Context, proposalID int64) sdk // Get the last used proposal ID func (keeper Keeper) GetLastProposalID(ctx sdk.Context) (proposalID int64) { - store := ctx.KVStore(keeper.storeKey) - bz := store.Get(KeyNextProposalID) - if bz == nil { + proposalID, err := keeper.peekCurrentProposalID(ctx) + if err != nil { return 0 } - keeper.cdc.MustUnmarshalBinary(bz, &proposalID) proposalID-- return } +// Gets the next available ProposalID and increments it func (keeper Keeper) getNewProposalID(ctx sdk.Context) (proposalID int64, err sdk.Error) { store := ctx.KVStore(keeper.storeKey) bz := store.Get(KeyNextProposalID) @@ -143,6 +188,17 @@ func (keeper Keeper) getNewProposalID(ctx sdk.Context) (proposalID int64, err sd return proposalID, nil } +// Peeks the next available ProposalID without incrementing it +func (keeper Keeper) peekCurrentProposalID(ctx sdk.Context) (proposalID int64, err sdk.Error) { + store := ctx.KVStore(keeper.storeKey) + bz := store.Get(KeyNextProposalID) + if bz == nil { + return -1, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set") + } + keeper.cdc.MustUnmarshalBinary(bz, &proposalID) + return proposalID, nil +} + func (keeper Keeper) activateVotingPeriod(ctx sdk.Context, proposal Proposal) { proposal.SetVotingStartBlock(ctx.BlockHeight()) proposal.SetStatus(StatusVotingPeriod) diff --git a/x/gov/proposals.go b/x/gov/proposals.go index f05dabd08d..c52ab1e875 100644 --- a/x/gov/proposals.go +++ b/x/gov/proposals.go @@ -110,6 +110,7 @@ type ProposalKind byte //nolint const ( + ProposalTypeNil ProposalKind = 0x00 ProposalTypeText ProposalKind = 0x01 ProposalTypeParameterChange ProposalKind = 0x02 ProposalTypeSoftwareUpgrade ProposalKind = 0x03 @@ -203,6 +204,7 @@ type ProposalStatus byte //nolint const ( + StatusNil ProposalStatus = 0x00 StatusDepositPeriod ProposalStatus = 0x01 StatusVotingPeriod ProposalStatus = 0x02 StatusPassed ProposalStatus = 0x03 @@ -220,6 +222,8 @@ func ProposalStatusFromString(str string) (ProposalStatus, error) { return StatusPassed, nil case "Rejected": return StatusRejected, nil + case "": + return StatusNil, nil default: return ProposalStatus(0xff), errors.Errorf("'%s' is not a valid proposal status", str) } diff --git a/x/gov/queryable.go b/x/gov/queryable.go new file mode 100644 index 0000000000..e64d506d1e --- /dev/null +++ b/x/gov/queryable.go @@ -0,0 +1,213 @@ +package gov + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + abci "github.com/tendermint/tendermint/abci/types" +) + +func NewQuerier(keeper Keeper) sdk.Querier { + return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) { + switch path[0] { + case "proposal": + return queryProposal(ctx, path[1:], req, keeper) + case "deposit": + return queryDeposit(ctx, path[1:], req, keeper) + case "vote": + return queryVote(ctx, path[1:], req, keeper) + case "deposits": + return queryDeposits(ctx, path[1:], req, keeper) + case "votes": + return queryVotes(ctx, path[1:], req, keeper) + case "proposals": + return queryProposals(ctx, path[1:], req, keeper) + case "tally": + return queryTally(ctx, path[1:], req, keeper) + default: + return nil, sdk.ErrUnknownRequest("unknown gov query endpoint") + } + } +} + +// Params for query 'custom/gov/proposal' +type QueryProposalParams struct { + ProposalID int64 +} + +func queryProposal(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { + var params QueryProposalParams + err2 := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) + if err2 != nil { + return []byte{}, sdk.ErrUnknownRequest(fmt.Sprintf("incorrectly formatted request data - %s", err2.Error())) + } + + proposal := keeper.GetProposal(ctx, params.ProposalID) + if proposal == nil { + return []byte{}, ErrUnknownProposal(DefaultCodespace, params.ProposalID) + } + + bz, err2 := wire.MarshalJSONIndent(keeper.cdc, proposal) + if err2 != nil { + panic("could not marshal result to JSON") + } + return bz, nil +} + +// Params for query 'custom/gov/deposit' +type QueryDepositParams struct { + ProposalID int64 + Depositer sdk.AccAddress +} + +func queryDeposit(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { + var params QueryDepositParams + err2 := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) + if err2 != nil { + return []byte{}, sdk.ErrUnknownRequest(fmt.Sprintf("incorrectly formatted request data - %s", err2.Error())) + } + + deposit, _ := keeper.GetDeposit(ctx, params.ProposalID, params.Depositer) + bz, err2 := wire.MarshalJSONIndent(keeper.cdc, deposit) + if err2 != nil { + panic("could not marshal result to JSON") + } + return bz, nil +} + +// Params for query 'custom/gov/vote' +type QueryVoteParams struct { + ProposalID int64 + Voter sdk.AccAddress +} + +func queryVote(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { + var params QueryVoteParams + err2 := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) + if err2 != nil { + return []byte{}, sdk.ErrUnknownRequest(fmt.Sprintf("incorrectly formatted request data - %s", err2.Error())) + } + + vote, _ := keeper.GetVote(ctx, params.ProposalID, params.Voter) + bz, err2 := wire.MarshalJSONIndent(keeper.cdc, vote) + if err2 != nil { + panic("could not marshal result to JSON") + } + return bz, nil +} + +// Params for query 'custom/gov/deposits' +type QueryDepositsParams struct { + ProposalID int64 +} + +func queryDeposits(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { + var params QueryDepositParams + err2 := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) + if err2 != nil { + return []byte{}, sdk.ErrUnknownRequest(fmt.Sprintf("incorrectly formatted request data - %s", err2.Error())) + } + + var deposits []Deposit + depositsIterator := keeper.GetDeposits(ctx, params.ProposalID) + for ; depositsIterator.Valid(); depositsIterator.Next() { + deposit := Deposit{} + keeper.cdc.MustUnmarshalBinary(depositsIterator.Value(), &deposit) + deposits = append(deposits, deposit) + } + + bz, err2 := wire.MarshalJSONIndent(keeper.cdc, deposits) + if err2 != nil { + panic("could not marshal result to JSON") + } + return bz, nil +} + +// Params for query 'custom/gov/votes' +type QueryVotesParams struct { + ProposalID int64 +} + +func queryVotes(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { + var params QueryVotesParams + err2 := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) + + if err2 != nil { + return []byte{}, sdk.ErrUnknownRequest(fmt.Sprintf("incorrectly formatted request data - %s", err2.Error())) + } + + var votes []Vote + votesIterator := keeper.GetVotes(ctx, params.ProposalID) + for ; votesIterator.Valid(); votesIterator.Next() { + vote := Vote{} + keeper.cdc.MustUnmarshalBinary(votesIterator.Value(), &vote) + votes = append(votes, vote) + } + + bz, err2 := wire.MarshalJSONIndent(keeper.cdc, votes) + if err2 != nil { + panic("could not marshal result to JSON") + } + return bz, nil +} + +// Params for query 'custom/gov/proposals' +type QueryProposalsParams struct { + Voter sdk.AccAddress + Depositer sdk.AccAddress + ProposalStatus ProposalStatus + NumLatestProposals int64 +} + +func queryProposals(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { + var params QueryProposalsParams + err2 := keeper.cdc.UnmarshalJSON(req.Data, ¶ms) + if err2 != nil { + return []byte{}, sdk.ErrUnknownRequest(fmt.Sprintf("incorrectly formatted request data - %s", err2.Error())) + } + + proposals := keeper.GetProposalsFiltered(ctx, params.Voter, params.Depositer, params.ProposalStatus, params.NumLatestProposals) + + bz, err2 := wire.MarshalJSONIndent(keeper.cdc, proposals) + if err2 != nil { + panic("could not marshal result to JSON") + } + return bz, nil +} + +// Params for query 'custom/gov/tally' +type QueryTallyParams struct { + ProposalID int64 +} + +func queryTally(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) { + // TODO: Dependant on #1914 + + var proposalID int64 + err2 := keeper.cdc.UnmarshalJSON(req.Data, proposalID) + if err2 != nil { + return []byte{}, sdk.ErrUnknownRequest(fmt.Sprintf("incorrectly formatted request data - %s", err2.Error())) + } + + proposal := keeper.GetProposal(ctx, proposalID) + if proposal == nil { + return []byte{}, ErrUnknownProposal(DefaultCodespace, proposalID) + } + + var tallyResult TallyResult + + if proposal.GetStatus() == StatusDepositPeriod { + tallyResult = EmptyTallyResult() + } else if proposal.GetStatus() == StatusPassed || proposal.GetStatus() == StatusRejected { + tallyResult = proposal.GetTallyResult() + } else { + _, tallyResult, _ = tally(ctx, keeper, proposal) + } + + bz, err2 := wire.MarshalJSONIndent(keeper.cdc, tallyResult) + if err2 != nil { + panic("could not marshal result to JSON") + } + return bz, nil +}