diff --git a/go/node/utils/auth_query.go b/go/node/utils/auth_query.go new file mode 100644 index 00000000..66bc08ea --- /dev/null +++ b/go/node/utils/auth_query.go @@ -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 +} diff --git a/go/node/utils/gov_query.go b/go/node/utils/gov_query.go new file mode 100644 index 00000000..5dc64a72 --- /dev/null +++ b/go/node/utils/gov_query.go @@ -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, + } +}