Skip to content

Commit

Permalink
feat: add client query utils
Browse files Browse the repository at this point in the history
Signed-off-by: Artur Troian <[email protected]>
  • Loading branch information
troian committed Sep 6, 2024
1 parent c8b48a3 commit dcf9143
Show file tree
Hide file tree
Showing 2 changed files with 354 additions and 0 deletions.
153 changes: 153 additions & 0 deletions go/node/utils/auth_query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package utils

import (
"context"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"

coretypes "github.com/cometbft/cometbft/rpc/core/types"

"github.com/cosmos/cosmos-sdk/client"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)

// QueryTxsByEvents performs a search for transactions for a given set of events
// via the Tendermint RPC. An event takes the form of:
// "{eventAttribute}.{attributeKey} = '{attributeValue}'". Each event is
// concatenated with an 'AND' operand. It returns a slice of Info object
// containing txs and metadata. An error is returned if the query fails.
// If an empty string is provided it will order txs by asc
func QueryTxsByEvents(ctx context.Context, cctx client.Context, events []string, page, limit int, orderBy string) (*sdk.SearchTxsResult, error) {
if len(events) == 0 {
return nil, errors.New("must declare at least one event to search")
}

if page <= 0 {
return nil, errors.New("page must be greater than 0")
}

if limit <= 0 {
return nil, errors.New("limit must be greater than 0")
}

// XXX: implement ANY
query := strings.Join(events, " AND ")

node, err := cctx.GetNode()
if err != nil {
return nil, err
}

// TODO: this may not always need to be proven
// https://github.com/cosmos/cosmos-sdk/issues/6807
resTxs, err := node.TxSearch(ctx, query, true, &page, &limit, orderBy)
if err != nil {
return nil, err
}

resBlocks, err := getBlocksForTxResults(ctx, cctx, resTxs.Txs)
if err != nil {
return nil, err
}

txs, err := formatTxResults(cctx.TxConfig, resTxs.Txs, resBlocks)
if err != nil {
return nil, err
}

result := sdk.NewSearchTxsResult(uint64(resTxs.TotalCount), uint64(len(txs)), uint64(page), uint64(limit), txs)

return result, nil
}

// QueryTx queries for a single transaction by a hash string in hex format. An
// error is returned if the transaction does not exist or cannot be queried.
func QueryTx(ctx context.Context, cctx client.Context, hashHexStr string) (*sdk.TxResponse, error) {
hash, err := hex.DecodeString(hashHexStr)
if err != nil {
return nil, err
}

node, err := cctx.GetNode()
if err != nil {
return nil, err
}

// TODO: this may not always need to be proven
// https://github.com/cosmos/cosmos-sdk/issues/6807
resTx, err := node.Tx(ctx, hash, true)
if err != nil {
return nil, err
}

resBlocks, err := getBlocksForTxResults(ctx, cctx, []*coretypes.ResultTx{resTx})
if err != nil {
return nil, err
}

out, err := mkTxResult(cctx.TxConfig, resTx, resBlocks[resTx.Height])
if err != nil {
return out, err
}

return out, nil
}

// formatTxResults parses the indexed txs into a slice of TxResponse objects.
func formatTxResults(txConfig client.TxConfig, resTxs []*coretypes.ResultTx, resBlocks map[int64]*coretypes.ResultBlock) ([]*sdk.TxResponse, error) {
var err error
out := make([]*sdk.TxResponse, len(resTxs))
for i := range resTxs {
out[i], err = mkTxResult(txConfig, resTxs[i], resBlocks[resTxs[i].Height])
if err != nil {
return nil, err
}
}

return out, nil
}

func getBlocksForTxResults(ctx context.Context, cctx client.Context, resTxs []*coretypes.ResultTx) (map[int64]*coretypes.ResultBlock, error) {
node, err := cctx.GetNode()
if err != nil {
return nil, err
}

resBlocks := make(map[int64]*coretypes.ResultBlock)

for _, resTx := range resTxs {
if _, ok := resBlocks[resTx.Height]; !ok {
resBlock, err := node.Block(ctx, &resTx.Height)
if err != nil {
return nil, err
}

resBlocks[resTx.Height] = resBlock
}
}

return resBlocks, nil
}

func mkTxResult(txConfig client.TxConfig, resTx *coretypes.ResultTx, resBlock *coretypes.ResultBlock) (*sdk.TxResponse, error) {
txb, err := txConfig.TxDecoder()(resTx.Tx)
if err != nil {
return nil, err
}
p, ok := txb.(intoAny)
if !ok {
return nil, fmt.Errorf("expecting a type implementing intoAny, got: %T", txb)
}
any := p.AsAny()
return sdk.NewResponseResultTx(resTx, any, resBlock.Block.Time.Format(time.RFC3339)), nil
}

// Deprecated: this interface is used only internally for scenario we are
// deprecating (StdTxConfig support)
type intoAny interface {
AsAny() *codectypes.Any
}
201 changes: 201 additions & 0 deletions go/node/utils/gov_query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package utils

import (
"context"
"fmt"

"github.com/cosmos/cosmos-sdk/client"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/gov/types"
v1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1"
"github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1"
)

const (
defaultPage = 1
defaultLimit = 30
)

// Proposer contains metadata of a governance proposal used for querying a
// proposer.
type Proposer struct {
ProposalID uint64 `json:"proposal_id" yaml:"proposal_id"`
Proposer string `json:"proposer" yaml:"proposer"`
}

// NewProposer returns a new Proposer given id and proposer
func NewProposer(proposalID uint64, proposer string) Proposer {
return Proposer{proposalID, proposer}
}

// String implements the fmt.Stringer interface.
func (p Proposer) String() string {
return fmt.Sprintf("Proposal with ID %d was proposed by %s", p.ProposalID, p.Proposer)
}

// QueryVotesByTxQuery will query for votes via a direct txs tags query. It
// will fetch and build votes directly from the returned txs and returns a JSON
// marshalled result or any error that occurred.
func QueryVotesByTxQuery(ctx context.Context, cctx client.Context, params v1.QueryProposalVotesParams) ([]byte, error) {
var (
votes []*v1.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 {
// Search for both (legacy) votes and weighted votes.
q := fmt.Sprintf("%s.%s='%d'", types.EventTypeProposalVote, types.AttributeKeyProposalID, params.ProposalID)
searchResult, err := QueryTxsByEvents(ctx, cctx, []string{q}, nextTxPage, defaultLimit, "")
if err != nil {
return nil, err
}

for _, info := range searchResult.Txs {
for _, msg := range info.GetTx().GetMsgs() {
if voteMsg, ok := msg.(*v1beta1.MsgVote); ok {
votes = append(votes, &v1.Vote{
Voter: voteMsg.Voter,
ProposalId: params.ProposalID,
Options: v1.NewNonSplitVoteOption(v1.VoteOption(voteMsg.Option)),
})
}

if voteMsg, ok := msg.(*v1.MsgVote); ok {
votes = append(votes, &v1.Vote{
Voter: voteMsg.Voter,
ProposalId: params.ProposalID,
Options: v1.NewNonSplitVoteOption(voteMsg.Option),
})
}

if voteWeightedMsg, ok := msg.(*v1beta1.MsgVoteWeighted); ok {
votes = append(votes, convertVote(voteWeightedMsg))
}

if voteWeightedMsg, ok := msg.(*v1.MsgVoteWeighted); ok {
votes = append(votes, &v1.Vote{
Voter: voteWeightedMsg.Voter,
ProposalId: params.ProposalID,
Options: voteWeightedMsg.Options,
})
}
}
}
if len(searchResult.Txs) != defaultLimit {
break
}

nextTxPage++
}
start, end := client.Paginate(len(votes), params.Page, params.Limit, 100)
if start < 0 || end < 0 {
votes = []*v1.Vote{}
} else {
votes = votes[start:end]
}

bz, err := cctx.LegacyAmino.MarshalJSON(votes)
if err != nil {
return nil, err
}

return bz, nil
}

// QueryVoteByTxQuery will query for a single vote via a direct txs tags query.
func QueryVoteByTxQuery(ctx context.Context, cctx client.Context, params v1.QueryVoteParams) ([]byte, error) {
q1 := fmt.Sprintf("%s.%s='%d'", types.EventTypeProposalVote, types.AttributeKeyProposalID, params.ProposalID)
q2 := fmt.Sprintf("%s.%s='%s'", sdk.EventTypeMessage, sdk.AttributeKeySender, params.Voter.String())
q3 := fmt.Sprintf("%s.%s='%s'", sdk.EventTypeMessage, sdk.AttributeKeySender, params.Voter)
searchResult, err := QueryTxsByEvents(ctx, cctx, []string{fmt.Sprintf("%s AND (%s OR %s)", q1, q2, q3)}, 1, 30, "")
if err != nil {
return nil, err
}

for _, info := range searchResult.Txs {
for _, msg := range info.GetTx().GetMsgs() {
// there should only be a single vote under the given conditions
var vote *v1.Vote
if voteMsg, ok := msg.(*v1beta1.MsgVote); ok {
vote = &v1.Vote{
Voter: voteMsg.Voter,
ProposalId: params.ProposalID,
Options: v1.NewNonSplitVoteOption(v1.VoteOption(voteMsg.Option)),
}
}

if voteMsg, ok := msg.(*v1.MsgVote); ok {
vote = &v1.Vote{
Voter: voteMsg.Voter,
ProposalId: params.ProposalID,
Options: v1.NewNonSplitVoteOption(voteMsg.Option),
}
}

if voteWeightedMsg, ok := msg.(*v1beta1.MsgVoteWeighted); ok {
vote = convertVote(voteWeightedMsg)
}

if voteWeightedMsg, ok := msg.(*v1.MsgVoteWeighted); ok {
vote = &v1.Vote{
Voter: voteWeightedMsg.Voter,
ProposalId: params.ProposalID,
Options: voteWeightedMsg.Options,
}
}

if vote != nil {
bz, err := cctx.Codec.MarshalJSON(vote)
if err != nil {
return nil, err
}

return bz, nil
}
}
}

return nil, fmt.Errorf("address '%s' did not vote on proposalID %d", params.Voter, params.ProposalID)
}

// QueryProposerByTxQuery will query for a proposer of a governance proposal by ID.
func QueryProposerByTxQuery(ctx context.Context, cctx client.Context, proposalID uint64) (Proposer, error) {
q := fmt.Sprintf("%s.%s='%d'", types.EventTypeSubmitProposal, types.AttributeKeyProposalID, proposalID)
searchResult, err := QueryTxsByEvents(ctx, cctx, []string{q}, defaultPage, defaultLimit, "")
if err != nil {
return Proposer{}, err
}

for _, info := range searchResult.Txs {
for _, msg := range info.GetTx().GetMsgs() {
// there should only be a single proposal under the given conditions
if subMsg, ok := msg.(*v1beta1.MsgSubmitProposal); ok {
return NewProposer(proposalID, subMsg.Proposer), nil
}
if subMsg, ok := msg.(*v1.MsgSubmitProposal); ok {
return NewProposer(proposalID, subMsg.Proposer), nil
}
}
}

return Proposer{}, fmt.Errorf("failed to find the proposer for proposalID %d", proposalID)
}


// convertVote converts a MsgVoteWeighted into a *v1.Vote.
func convertVote(v *v1beta1.MsgVoteWeighted) *v1.Vote {
opts := make([]*v1.WeightedVoteOption, len(v.Options))
for i, o := range v.Options {
opts[i] = &v1.WeightedVoteOption{
Option: v1.VoteOption(o.Option),
Weight: o.Weight.String(),
}
}
return &v1.Vote{
Voter: v.Voter,
ProposalId: v.ProposalId,
Options: opts,
}
}

0 comments on commit dcf9143

Please sign in to comment.