Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x/gov: Queries to /gov/proposals/{proposalID}/votes support pagination #5405

Merged
merged 5 commits into from
Dec 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions x/gov/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
gcutils "github.com/cosmos/cosmos-sdk/x/gov/client/utils"
"github.com/cosmos/cosmos-sdk/x/gov/types"
)
Expand Down Expand Up @@ -242,15 +243,16 @@ $ %s query gov vote 1 cosmos1skjwj5whet0lpe65qaq4rpq03hjxlwd9nf39lk

// GetCmdQueryVotes implements the command to query for proposal votes.
func GetCmdQueryVotes(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
cmd := &cobra.Command{
Use: "votes [proposal-id]",
Args: cobra.ExactArgs(1),
Short: "Query votes on a proposal",
Long: strings.TrimSpace(
fmt.Sprintf(`Query vote details for a single proposal by its identifier.

Example:
$ %s query gov votes 1
$ %[1]s query gov votes 1
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
$ %[1]s query gov votes 1 --page=2 --limit=100
`,
version.ClientName,
),
Expand All @@ -263,8 +265,10 @@ $ %s query gov votes 1
if err != nil {
return fmt.Errorf("proposal-id %s not a valid int, please input a valid proposal-id", args[0])
}
page := viper.GetInt(flagPage)
limit := viper.GetInt(flagNumLimit)

params := types.NewQueryProposalParams(proposalID)
params := types.NewQueryProposalVotesParams(proposalID, page, limit)
bz, err := cdc.MarshalJSON(params)
if err != nil {
return err
Expand All @@ -281,7 +285,7 @@ $ %s query gov votes 1

propStatus := proposal.Status
if !(propStatus == types.StatusVotingPeriod || propStatus == types.StatusDepositPeriod) {
res, err = gcutils.QueryVotesByTxQuery(cliCtx, params)
res, err = gcutils.QueryVotesByTxQuery(cliCtx, params, utils.QueryTxsByEvents)
} else {
res, _, err = cliCtx.QueryWithData(fmt.Sprintf("custom/%s/votes", queryRoute), bz)
}
Expand All @@ -295,6 +299,9 @@ $ %s query gov votes 1
return cliCtx.PrintOutput(votes)
},
}
cmd.Flags().Int(flagPage, 1, "pagination page of votes to to query for")
cmd.Flags().Int(flagNumLimit, 100, "pagination limit of votes to query for")
return cmd
}

// Command to Get a specific Deposit Information
Expand Down
11 changes: 9 additions & 2 deletions x/gov/client/rest/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
gcutils "github.com/cosmos/cosmos-sdk/x/gov/client/utils"
"github.com/cosmos/cosmos-sdk/x/gov/types"
)
Expand Down Expand Up @@ -332,6 +333,12 @@ func queryVoteHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
// todo: Split this functionality into helper functions to remove the above
func queryVotesOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}

vars := mux.Vars(r)
strProposalID := vars[RestProposalID]

Expand All @@ -351,7 +358,7 @@ func queryVotesOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return
}

params := types.NewQueryProposalParams(proposalID)
params := types.NewQueryProposalVotesParams(proposalID, page, limit)

bz, err := cliCtx.Codec.MarshalJSON(params)
if err != nil {
Expand All @@ -375,7 +382,7 @@ func queryVotesOnProposalHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
// as they're no longer in state.
propStatus := proposal.Status
if !(propStatus == types.StatusVotingPeriod || propStatus == types.StatusDepositPeriod) {
res, err = gcutils.QueryVotesByTxQuery(cliCtx, params)
res, err = gcutils.QueryVotesByTxQuery(cliCtx, params, utils.QueryTxsByEvents)
} else {
res, _, err = cliCtx.QueryWithData("custom/gov/votes", bz)
}
Expand Down
78 changes: 45 additions & 33 deletions x/gov/client/utils/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package utils
import (
"fmt"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
Expand Down Expand Up @@ -72,45 +73,57 @@ func QueryDepositsByTxQuery(cliCtx context.CLIContext, params types.QueryProposa
return cliCtx.Codec.MarshalJSON(deposits)
}

// QueryVotesByTxQuery will query for votes via a direct txs tags query. It
// will fetch and build votes directly from the returned txs and return a JSON
// marshalled result or any error that occurred.
//
// NOTE: SearchTxs is used to facilitate the txs query which does not currently
// support configurable pagination.
func QueryVotesByTxQuery(cliCtx context.CLIContext, params types.QueryProposalParams) ([]byte, error) {
events := []string{
fmt.Sprintf("%s.%s='%s'", sdk.EventTypeMessage, sdk.AttributeKeyAction, types.TypeMsgVote),
fmt.Sprintf("%s.%s='%s'", types.EventTypeProposalVote, types.AttributeKeyProposalID, []byte(fmt.Sprintf("%d", params.ProposalID))),
}

// NOTE: SearchTxs is used to facilitate the txs query which does not currently
// support configurable pagination.
searchResult, err := utils.QueryTxsByEvents(cliCtx, events, defaultPage, defaultLimit)
if err != nil {
return nil, err
}

var votes []types.Vote

for _, info := range searchResult.Txs {
for _, msg := range info.Tx.GetMsgs() {
if msg.Type() == types.TypeMsgVote {
voteMsg := msg.(types.MsgVote)
// TxQuerier is a type that accepts query parameters (target events and pagination options) and returns sdk.SearchTxsResult.
// Mainly used for easier mocking of utils.QueryTxsByEvents in tests.
type TxQuerier func(cliCtx context.CLIContext, events []string, page, limit int) (*sdk.SearchTxsResult, error)

votes = append(votes, types.Vote{
Voter: voteMsg.Voter,
ProposalID: params.ProposalID,
Option: voteMsg.Option,
})
// QueryVotesByTxQuery will query for votes using provided TxQuerier implementation.
// In general utils.QueryTxsByEvents should be used that will do a direct tx query to a tendermint node.
// It will fetch and build votes directly from the returned txs and return a JSON
// marshalled result or any error that occurred.
func QueryVotesByTxQuery(cliCtx context.CLIContext, params types.QueryProposalVotesParams, querier TxQuerier) ([]byte, error) {
var (
events = []string{
fmt.Sprintf("%s.%s='%s'", sdk.EventTypeMessage, sdk.AttributeKeyAction, types.TypeMsgVote),
fmt.Sprintf("%s.%s='%s'", types.EventTypeProposalVote, types.AttributeKeyProposalID, []byte(fmt.Sprintf("%d", params.ProposalID))),
}
votes []types.Vote
nextTxPage = defaultPage
totalLimit = params.Limit * params.Page
)
// query interrupted either if we collected enough votes or tx indexer run out of relevant txs
for len(votes) < totalLimit {
searchResult, err := querier(cliCtx, events, nextTxPage, defaultLimit)
if err != nil {
return nil, err
}
nextTxPage++
for _, info := range searchResult.Txs {
for _, msg := range info.Tx.GetMsgs() {
if msg.Type() == types.TypeMsgVote {
voteMsg := msg.(types.MsgVote)

votes = append(votes, types.Vote{
Voter: voteMsg.Voter,
ProposalID: params.ProposalID,
Option: voteMsg.Option,
})
}
}
}
if len(searchResult.Txs) != defaultLimit {
break
}
}
start, end := client.Paginate(len(votes), params.Page, params.Limit, 100)
if start < 0 || end < 0 {
votes = []types.Vote{}
} else {
votes = votes[start:end]
}

if cliCtx.Indent {
return cliCtx.Codec.MarshalJSONIndent(votes, "", " ")
}

return cliCtx.Codec.MarshalJSON(votes)
}

Expand All @@ -128,7 +141,6 @@ func QueryVoteByTxQuery(cliCtx context.CLIContext, params types.QueryVoteParams)
if err != nil {
return nil, err
}

for _, info := range searchResult.Txs {
for _, msg := range info.Tx.GetMsgs() {
// there should only be a single vote under the given conditions
Expand Down
129 changes: 129 additions & 0 deletions x/gov/client/utils/query_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package utils

import (
"testing"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/stretchr/testify/require"
)

type txMock struct {
address sdk.AccAddress
msgNum int
}

func (tx txMock) ValidateBasic() sdk.Error {
return nil
}

func (tx txMock) GetMsgs() (msgs []sdk.Msg) {
for i := 0; i < tx.msgNum; i++ {
msgs = append(msgs, types.NewMsgVote(tx.address, 0, types.OptionYes))
}
return
}

func makeQuerier(txs []sdk.Tx) TxQuerier {
return func(cliCtx context.CLIContext, events []string, page, limit int) (*sdk.SearchTxsResult, error) {
start, end := client.Paginate(len(txs), page, limit, 100)
if start < 0 || end < 0 {
return nil, nil
}
rst := &sdk.SearchTxsResult{
TotalCount: len(txs),
PageNumber: page,
PageTotal: len(txs) / limit,
Limit: limit,
Count: end - start,
}
for _, tx := range txs[start:end] {
rst.Txs = append(rst.Txs, sdk.TxResponse{Tx: tx})
}
return rst, nil
}
}

func TestGetPaginatedVotes(t *testing.T) {
type testCase struct {
description string
page, limit int
txs []sdk.Tx
votes []types.Vote
}
acc1 := make(sdk.AccAddress, 20)
acc1[0] = 1
acc2 := make(sdk.AccAddress, 20)
acc2[0] = 2
for _, tc := range []testCase{
{
description: "1MsgPerTxAll",
page: 1,
limit: 2,
txs: []sdk.Tx{txMock{acc1, 1}, txMock{acc2, 1}},
votes: []types.Vote{
types.NewVote(0, acc1, types.OptionYes),
types.NewVote(0, acc2, types.OptionYes)},
},
{
description: "2MsgPerTx1Chunk",
page: 1,
limit: 2,
txs: []sdk.Tx{txMock{acc1, 2}, txMock{acc2, 2}},
votes: []types.Vote{
types.NewVote(0, acc1, types.OptionYes),
types.NewVote(0, acc1, types.OptionYes)},
},
{
description: "2MsgPerTx2Chunk",
page: 2,
limit: 2,
txs: []sdk.Tx{txMock{acc1, 2}, txMock{acc2, 2}},
votes: []types.Vote{
types.NewVote(0, acc2, types.OptionYes),
types.NewVote(0, acc2, types.OptionYes)},
},
{
description: "IncompleteSearchTx",
page: 1,
limit: 2,
txs: []sdk.Tx{txMock{acc1, 1}},
votes: []types.Vote{types.NewVote(0, acc1, types.OptionYes)},
},
{
description: "IncompleteSearchTx",
page: 1,
limit: 2,
txs: []sdk.Tx{txMock{acc1, 1}},
votes: []types.Vote{types.NewVote(0, acc1, types.OptionYes)},
},
{
description: "InvalidPage",
page: -1,
txs: []sdk.Tx{txMock{acc1, 1}},
},
{
description: "OutOfBounds",
page: 2,
limit: 10,
txs: []sdk.Tx{txMock{acc1, 1}},
},
} {
tc := tc
t.Run(tc.description, func(t *testing.T) {
ctx := context.CLIContext{}.WithCodec(codec.New())
params := types.NewQueryProposalVotesParams(0, tc.page, tc.limit)
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
votesData, err := QueryVotesByTxQuery(ctx, params, makeQuerier(tc.txs))
require.NoError(t, err)
votes := []types.Vote{}
require.NoError(t, ctx.Codec.UnmarshalJSON(votesData, &votes))
require.Equal(t, len(tc.votes), len(votes))
for i := range votes {
require.Equal(t, tc.votes[i], votes[i])
}
})
}
}
10 changes: 9 additions & 1 deletion x/gov/keeper/querier.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

abci "github.com/tendermint/tendermint/abci/types"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov/types"
Expand Down Expand Up @@ -177,7 +178,7 @@ func queryTally(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Ke

// nolint: unparam
func queryVotes(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
var params types.QueryProposalParams
var params types.QueryProposalVotesParams
err := keeper.cdc.UnmarshalJSON(req.Data, &params)

if err != nil {
Expand All @@ -187,6 +188,13 @@ func queryVotes(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Ke
votes := keeper.GetVotes(ctx, params.ProposalID)
alexanderbez marked this conversation as resolved.
Show resolved Hide resolved
if votes == nil {
votes = types.Votes{}
} else {
start, end := client.Paginate(len(votes), params.Page, params.Limit, 100)
if start < 0 || end < 0 {
votes = types.Votes{}
} else {
votes = votes[start:end]
}
}

bz, err := codec.MarshalJSONIndent(keeper.cdc, votes)
Expand Down
Loading