diff --git a/app/app.go b/app/app.go index bfc2d75e..c0ff9680 100644 --- a/app/app.go +++ b/app/app.go @@ -55,7 +55,7 @@ var ( staking.AppModuleBasic{}, mint.AppModuleBasic{}, distr.AppModuleBasic{}, - gov.NewAppModuleBasic(paramsclient.ProposalHandler, distr.ProposalHandler), + gov.NewAppModuleBasic(paramsclient.ProposalHandler, distr.ProposalHandler, committee.ProposalHandler), params.AppModuleBasic{}, crisis.AppModuleBasic{}, slashing.AppModuleBasic{}, @@ -207,11 +207,23 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, invCheckPeriod, app.supplyKeeper, auth.FeeCollectorName) + committeeGovRouter := gov.NewRouter() + committeeGovRouter. + AddRoute(gov.RouterKey, gov.ProposalHandler). + AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)). + AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)) + // Note: the committee proposal handler is not registered on the committee router. This means committees cannot create or update other committees. + // Adding the committee proposal handler to the router is possible but awkward as the handler depends on the keeper which depends on the handler. + app.committeeKeeper = committee.NewKeeper( + app.cdc, + keys[committee.StoreKey], + committeeGovRouter) // TODO blacklist module addresses?) govRouter := gov.NewRouter() govRouter. AddRoute(gov.RouterKey, gov.ProposalHandler). AddRoute(params.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)). - AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)) + AddRoute(distr.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)). + AddRoute(committee.RouterKey, committee.NewProposalHandler(app.committeeKeeper)) app.govKeeper = gov.NewKeeper( app.cdc, keys[gov.StoreKey], @@ -246,12 +258,6 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, app.auctionKeeper, app.supplyKeeper, cdp.DefaultCodespace) - app.committeeKeeper = committee.NewKeeper( - app.cdc, - keys[committee.StoreKey], - govRouter, - // TODO blacklist module addresses? - ) // register the staking hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks @@ -330,7 +336,8 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, // initialize the app app.SetInitChainer(app.InitChainer) app.SetBeginBlocker(app.BeginBlocker) - // app.SetAnteHandler(NewAnteHandler(app.accountKeeper, app.supplyKeeper, app.shutdownKeeper, auth.DefaultSigVerificationGasConsumer)) + // TODO app.SetAnteHandler(NewAnteHandler(app.accountKeeper, app.supplyKeeper, app.shutdownKeeper, auth.DefaultSigVerificationGasConsumer)) + app.SetAnteHandler(auth.NewAnteHandler(app.accountKeeper, app.supplyKeeper, auth.DefaultSigVerificationGasConsumer)) app.SetEndBlocker(app.EndBlocker) // load store diff --git a/x/committee/alias.go b/x/committee/alias.go index b3e3cc2a..c33b9b01 100644 --- a/x/committee/alias.go +++ b/x/committee/alias.go @@ -3,6 +3,7 @@ package committee import ( + "github.com/kava-labs/kava/x/committee/client" "github.com/kava-labs/kava/x/committee/keeper" "github.com/kava-labs/kava/x/committee/types" ) @@ -49,6 +50,7 @@ var ( Uint64FromBytes = types.Uint64FromBytes // variable aliases + ProposalHandler = client.ProposalHandler CommitteeKeyPrefix = types.CommitteeKeyPrefix MaxProposalDuration = types.MaxProposalDuration ModuleCdc = types.ModuleCdc diff --git a/x/committee/client/cli/query.go b/x/committee/client/cli/query.go index 23deeb76..4dfeea25 100644 --- a/x/committee/client/cli/query.go +++ b/x/committee/client/cli/query.go @@ -10,7 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/codec" - comclient "github.com/kava-labs/kava/x/committee/client" + "github.com/kava-labs/kava/x/committee/client/common" "github.com/kava-labs/kava/x/committee/types" ) @@ -381,7 +381,7 @@ func GetCmdQueryProposer(queryRoute string, cdc *codec.Codec) *cobra.Command { return fmt.Errorf("proposal-id %s is not a valid uint", args[0]) } - prop, err := comclient.QueryProposer(cliCtx, proposalID) + prop, err := common.QueryProposer(cliCtx, proposalID) if err != nil { return err } diff --git a/x/committee/client/cli/tx.go b/x/committee/client/cli/tx.go index 46674426..321c4e7e 100644 --- a/x/committee/client/cli/tx.go +++ b/x/committee/client/cli/tx.go @@ -13,6 +13,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/kava-labs/kava/x/committee/types" ) @@ -139,7 +140,7 @@ func GetCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { func GetCmdVote(cdc *codec.Codec) *cobra.Command { return &cobra.Command{ Use: "vote [proposal-id]", - Args: cobra.ExactArgs(2), + Args: cobra.ExactArgs(1), Short: "Vote for an active proposal", // TODO // Long: strings.TrimSpace( // fmt.Sprintf(`Submit a vote for an active proposal. You can @@ -175,3 +176,51 @@ func GetCmdVote(cdc *codec.Codec) *cobra.Command { }, } } + +// TODO this could replace the whole gov submit-proposal cmd, remove and replace the gov cmd in kvcli main.go +// would want the documentation/examples though +func GetGovCmdSubmitProposal(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "committee [proposal-file] [deposit]", + Short: "Submit a governance proposal to change a committee.", + Long: "This command will work with either CommitteeChange proposals or CommitteeDelete proposals.", // TODO + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // Get proposing address + proposer := cliCtx.GetFromAddress() + + // Get the deposit + deposit, err := sdk.ParseCoins(args[0]) + if err != nil { + return err + } + + // Get the proposal + bz, err := ioutil.ReadFile(args[0]) + if err != nil { + return err + } + var content govtypes.Content + if err := cdc.UnmarshalJSON(bz, &content); err != nil { + return err + } + if err = content.ValidateBasic(); err != nil { + return err + } + + // Build message and run basic validation + msg := govtypes.NewMsgSubmitProposal(content, deposit, proposer) + err = msg.ValidateBasic() + if err != nil { + return err + } + + // Sign and broadcast message + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return cmd +} diff --git a/x/committee/client/query_proposer.go b/x/committee/client/common/query_proposer.go similarity index 99% rename from x/committee/client/query_proposer.go rename to x/committee/client/common/query_proposer.go index 29e61552..0508f53d 100644 --- a/x/committee/client/query_proposer.go +++ b/x/committee/client/common/query_proposer.go @@ -1,4 +1,4 @@ -package client +package common import ( "fmt" diff --git a/x/committee/client/proposal_handler.go b/x/committee/client/proposal_handler.go new file mode 100644 index 00000000..0bbd31ea --- /dev/null +++ b/x/committee/client/proposal_handler.go @@ -0,0 +1,11 @@ +package client + +import ( + govclient "github.com/cosmos/cosmos-sdk/x/gov/client" + + "github.com/kava-labs/kava/x/committee/client/cli" + "github.com/kava-labs/kava/x/committee/client/rest" +) + +// ProposalHandler is a struct containing handler funcs for submiting CommitteeChange/Delete proposal txs to the gov module through the cli or rest. +var ProposalHandler = govclient.NewProposalHandler(cli.GetGovCmdSubmitProposal, rest.ProposalRESTHandler) diff --git a/x/committee/client/rest/query.go b/x/committee/client/rest/query.go index e7c2be72..4ec04e7d 100644 --- a/x/committee/client/rest/query.go +++ b/x/committee/client/rest/query.go @@ -10,7 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/types/rest" - "github.com/kava-labs/kava/x/committee/client" + "github.com/kava-labs/kava/x/committee/client/common" "github.com/kava-labs/kava/x/committee/types" ) @@ -181,7 +181,7 @@ func queryProposerHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { } // Query - res, err := client.QueryProposer(cliCtx, proposalID) + res, err := common.QueryProposer(cliCtx, proposalID) if err != nil { rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return diff --git a/x/committee/client/rest/tx.go b/x/committee/client/rest/tx.go index 20be14e6..983e3a55 100644 --- a/x/committee/client/rest/tx.go +++ b/x/committee/client/rest/tx.go @@ -10,6 +10,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/rest" "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/kava-labs/kava/x/committee/types" ) @@ -108,3 +110,46 @@ func postVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) } } + +// -------- -------- +// TODO this could replace the POST gov/proposals endpoint, would need to overwrite routes in kvcli main, hacky +type PostGovProposalReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Content govtypes.Content `json:"content" yaml:"content"` //TODO use same PubProposal name? + Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` +} + +func ProposalRESTHandler(cliCtx context.CLIContext) govrest.ProposalRESTHandler { + return govrest.ProposalRESTHandler{ + SubRoute: "committee", + Handler: postGovProposalHandlerFn(cliCtx), + } +} + +func postGovProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + // Parse and validate http request body + var req PostGovProposalReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + if err := req.Content.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + // Create and return a StdTx + msg := govtypes.NewMsgSubmitProposal(req.Content, req.Deposit, req.Proposer) + if err := msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/committee/keeper/keeper.go b/x/committee/keeper/keeper.go index 3b01c33e..c1471fbb 100644 --- a/x/committee/keeper/keeper.go +++ b/x/committee/keeper/keeper.go @@ -22,8 +22,7 @@ type Keeper struct { func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router) Keeper { // Logic in the keeper methods assume the set of gov handlers is fixed. // So the gov router must be sealed so no handlers can be added or removed after the keeper is created. - // Note: for some reason the gov router panics if it has already been sealed, so a helper func is used to make sealing idempotent. - sealGovRouterIdempotently(router) + router.Seal() return Keeper{ cdc: cdc, @@ -32,13 +31,6 @@ func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, router govtypes.Router) } } -func sealGovRouterIdempotently(router govtypes.Router) { - defer func() { - recover() - }() - router.Seal() -} - // ---------- Committees ---------- // GetCommittee gets a committee from the store. diff --git a/x/committee/proposal_handler_test.go b/x/committee/proposal_handler_test.go index 01a8ec75..c5150462 100644 --- a/x/committee/proposal_handler_test.go +++ b/x/committee/proposal_handler_test.go @@ -69,7 +69,8 @@ func (suite *ProposalHandlerTestSuite) TestProposalHandler_ChangeCommittee() { "A Title", "A proposal description.", committee.Committee{ - ID: 34, + ID: 34, + Members: suite.addresses[:1], }, ), expectPass: true,