diff --git a/PENDING.md b/PENDING.md index c4a9637525..101a306508 100644 --- a/PENDING.md +++ b/PENDING.md @@ -28,9 +28,13 @@ FEATURES * Gaia REST API (`gaiacli advanced rest-server`) * [\#3067](https://github.com/cosmos/cosmos-sdk/issues/3067) Add support for fees on transactions * [\#3069](https://github.com/cosmos/cosmos-sdk/pull/3069) Add a custom memo on transactions + * [\#3027](https://github.com/cosmos/cosmos-sdk/issues/3027) Implement + `/gov/proposals/{proposalID}/proposer` to query for a proposal's proposer. * Gaia CLI (`gaiacli`) * \#2399 Implement `params` command to query slashing parameters. + * [\#3027](https://github.com/cosmos/cosmos-sdk/issues/3027) Implement + `query gov proposer [proposal-id]` to query for a proposal's proposer. * Gaia * [\#2182] [x/stake] Added querier for querying a single redelegation diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index 5c6d7de183..cff3bc70dc 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -566,10 +566,9 @@ func TestSubmitProposal(t *testing.T) { proposal := getProposal(t, port, proposalID) require.Equal(t, "Test", proposal.GetTitle()) - // query tx - txs := getTransactions(t, port, fmt.Sprintf("action=submit_proposal&proposer=%s", addr)) - require.Len(t, txs, 1) - require.Equal(t, resultTx.Height, txs[0].Height) + proposer := getProposer(t, port, proposalID) + require.Equal(t, addr.String(), proposer.Proposer) + require.Equal(t, proposalID, proposer.ProposalID) } func TestDeposit(t *testing.T) { diff --git a/client/lcd/swagger-ui/swagger.yaml b/client/lcd/swagger-ui/swagger.yaml index 27fdce75f8..1899eb5cb7 100644 --- a/client/lcd/swagger-ui/swagger.yaml +++ b/client/lcd/swagger-ui/swagger.yaml @@ -1307,6 +1307,28 @@ paths: description: Invalid proposal id 500: description: Internal Server Error + /gov/proposals/{proposalId}/proposer: + get: + summary: Query proposer + description: Query for the proposer for a proposal + produces: + - application/json + tags: + - ICS22 + parameters: + - type: string + name: proposalId + required: true + in: path + responses: + 200: + description: OK + schema: + $ref: "#/definitions/Proposer" + 400: + description: Invalid proposal ID + 500: + description: Internal Server Error /gov/proposals/{proposalId}/deposits: get: summary: Query deposits @@ -2268,6 +2290,13 @@ definitions: $ref: "#/definitions/Coin" voting_start_time: type: string + Proposer: + type: object + properties: + proposal_id: + type: integer + proposer: + type: string Deposit: type: object properties: diff --git a/client/lcd/test_helpers.go b/client/lcd/test_helpers.go index 850b4b9570..e6e2d2fa40 100644 --- a/client/lcd/test_helpers.go +++ b/client/lcd/test_helpers.go @@ -15,13 +15,14 @@ import ( "strings" "testing" + "github.com/tendermint/tendermint/crypto/secp256k1" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + cryptoKeys "github.com/cosmos/cosmos-sdk/crypto/keys" authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/slashing" stakeTypes "github.com/cosmos/cosmos-sdk/x/stake/types" - "github.com/tendermint/tendermint/crypto/secp256k1" - ctypes "github.com/tendermint/tendermint/rpc/core/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/keys" @@ -35,6 +36,7 @@ import ( "github.com/cosmos/cosmos-sdk/tests" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" + gcutils "github.com/cosmos/cosmos-sdk/x/gov/client/utils" "github.com/cosmos/cosmos-sdk/x/stake" "github.com/spf13/viper" @@ -1249,6 +1251,18 @@ func getVote(t *testing.T, port string, proposalID uint64, voterAddr sdk.AccAddr return vote } +// GET /gov/proposals/{proposalId}/proposer +func getProposer(t *testing.T, port string, proposalID uint64) gcutils.Proposer { + res, body := Request(t, port, "GET", fmt.Sprintf("/gov/proposals/%d/proposer", proposalID), nil) + require.Equal(t, http.StatusOK, res.StatusCode, body) + + var proposer gcutils.Proposer + err := cdc.UnmarshalJSON([]byte(body), &proposer) + + require.Nil(t, err) + return proposer +} + // GET /gov/parameters/deposit Query governance deposit parameters func getDepositParam(t *testing.T, port string) gov.DepositParams { res, body := Request(t, port, "GET", "/gov/parameters/deposit", nil) diff --git a/docs/gaia/gaiacli.md b/docs/gaia/gaiacli.md index 102de5c892..e1b07678c5 100644 --- a/docs/gaia/gaiacli.md +++ b/docs/gaia/gaiacli.md @@ -498,6 +498,12 @@ gaiacli query gov proposals You can also query proposals filtered by `voter` or `depositor` by using the corresponding flags. +To query for the proposer of a given governance proposal: + +```bash +gaiacli query gov proposer +``` + #### Increase deposit In order for a proposal to be broadcasted to the network, the amount deposited must be above a `minDeposit` value (default: `10 steak`). If the proposal you previously created didn't meet this requirement, you can still increase the total amount deposited to activate it. Once the minimum deposit is reached, the proposal enters voting period: diff --git a/x/gov/client/cli/query.go b/x/gov/client/cli/query.go index 61cc002428..e72d66a9ce 100644 --- a/x/gov/client/cli/query.go +++ b/x/gov/client/cli/query.go @@ -458,3 +458,31 @@ func GetCmdQueryParams(queryRoute string, cdc *codec.Codec) *cobra.Command { return cmd } + +// GetCmdQueryProposer implements the query proposer command. +func GetCmdQueryProposer(queryRoute string, cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "proposer [proposal-id]", + Args: cobra.ExactArgs(1), + Short: "Query the proposer of a governance proposal", + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + // validate that the proposalID is a uint + proposalID, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("proposal-id %s is not a valid uint", args[0]) + } + + res, err := gcutils.QueryProposerByTxQuery(cdc, cliCtx, proposalID) + if err != nil { + return err + } + + fmt.Println(string(res)) + return nil + }, + } + + return cmd +} diff --git a/x/gov/client/module_client.go b/x/gov/client/module_client.go index 1f67c9c071..4ad8f01fd0 100644 --- a/x/gov/client/module_client.go +++ b/x/gov/client/module_client.go @@ -32,6 +32,7 @@ func (mc ModuleClient) GetQueryCmd() *cobra.Command { govCli.GetCmdQueryVote(mc.storeKey, mc.cdc), govCli.GetCmdQueryVotes(mc.storeKey, mc.cdc), govCli.GetCmdQueryParams(mc.storeKey, mc.cdc), + govCli.GetCmdQueryProposer(mc.storeKey, mc.cdc), govCli.GetCmdQueryDeposit(mc.storeKey, mc.cdc), govCli.GetCmdQueryDeposits(mc.storeKey, mc.cdc), govCli.GetCmdQueryTally(mc.storeKey, mc.cdc))...) diff --git a/x/gov/client/rest/rest.go b/x/gov/client/rest/rest.go index 39f01932fe..96142941ef 100644 --- a/x/gov/client/rest/rest.go +++ b/x/gov/client/rest/rest.go @@ -41,6 +41,10 @@ func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec) r.HandleFunc("/gov/proposals", queryProposalsWithParameterFn(cdc, cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}", RestProposalID), queryProposalHandlerFn(cdc, cliCtx)).Methods("GET") + r.HandleFunc( + fmt.Sprintf("/gov/proposals/{%s}/proposer", RestProposalID), + queryProposerHandlerFn(cdc, cliCtx), + ).Methods("GET") r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/deposits", RestProposalID), queryDepositsHandlerFn(cdc, cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/deposits/{%s}", RestProposalID, RestDepositor), queryDepositHandlerFn(cdc, cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/tally", RestProposalID), queryTallyOnProposalHandlerFn(cdc, cliCtx)).Methods("GET") @@ -282,6 +286,26 @@ func queryDepositsHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.Ha } } +func queryProposerHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + strProposalID := vars[RestProposalID] + + proposalID, ok := utils.ParseUint64OrReturnBadRequest(w, strProposalID) + if !ok { + return + } + + res, err := gcutils.QueryProposerByTxQuery(cdc, cliCtx, proposalID) + if err != nil { + utils.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + utils.PostProcessResponse(w, cdc, res, cliCtx.Indent) + } +} + func queryDepositHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) diff --git a/x/gov/client/utils/query.go b/x/gov/client/utils/query.go index fdae6f3b05..9c62fc118a 100644 --- a/x/gov/client/utils/query.go +++ b/x/gov/client/utils/query.go @@ -10,6 +10,13 @@ import ( "github.com/cosmos/cosmos-sdk/x/gov/tags" ) +// Proposer contains metadata of a governance proposal used for querying a +// proposer. +type Proposer struct { + ProposalID uint64 `json:"proposal_id"` + Proposer string `json:"proposer"` +} + // QueryDepositsByTxQuery will query for deposits via a direct txs tags query. It // will fetch and build deposits directly from the returned txs and return a // JSON marshalled result or any error that occurred. @@ -133,8 +140,7 @@ func QueryVoteByTxQuery( } } - err = fmt.Errorf("address '%s' did not vote on proposalID %d", params.Voter, params.ProposalID) - return nil, err + return nil, fmt.Errorf("address '%s' did not vote on proposalID %d", params.Voter, params.ProposalID) } // QueryDepositByTxQuery will query for a single deposit via a direct txs tags @@ -175,6 +181,44 @@ func QueryDepositByTxQuery( } } - err = fmt.Errorf("address '%s' did not deposit to proposalID %d", params.Depositor, params.ProposalID) - return nil, err + return nil, fmt.Errorf("address '%s' did not deposit to proposalID %d", params.Depositor, params.ProposalID) +} + +// QueryProposerByTxQuery will query for a proposer of a governance proposal by +// ID. +func QueryProposerByTxQuery( + cdc *codec.Codec, cliCtx context.CLIContext, proposalID uint64, +) ([]byte, error) { + + tags := []string{ + fmt.Sprintf("%s='%s'", tags.Action, tags.ActionProposalSubmitted), + fmt.Sprintf("%s='%s'", tags.ProposalID, []byte(fmt.Sprintf("%d", proposalID))), + } + + infos, err := tx.SearchTxs(cliCtx, cdc, tags) + if err != nil { + return nil, err + } + + for _, info := range infos { + for _, msg := range info.Tx.GetMsgs() { + // there should only be a single proposal under the given conditions + if msg.Type() == gov.TypeMsgSubmitProposal { + subMsg := msg.(gov.MsgSubmitProposal) + + proposer := Proposer{ + ProposalID: proposalID, + Proposer: subMsg.Proposer.String(), + } + + if cliCtx.Indent { + return cdc.MarshalJSONIndent(proposer, "", " ") + } + + return cdc.MarshalJSON(proposer) + } + } + } + + return nil, fmt.Errorf("failed to find the proposer for proposalID %d", proposalID) }