Skip to content

Commit

Permalink
Merge PR #5405: Queries to /gov/proposals/{proposalID}/votes support …
Browse files Browse the repository at this point in the history
…pagination
  • Loading branch information
dshulyak authored and alexanderbez committed Dec 18, 2019
1 parent eae10b0 commit cf46452
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 45 deletions.
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
$ %[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)
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)
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

0 comments on commit cf46452

Please sign in to comment.