-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Artur Troian <[email protected]>
- Loading branch information
Showing
2 changed files
with
354 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |