From cf052368a37e5265d79bf72e2af2730a0478009b Mon Sep 17 00:00:00 2001 From: Paul Chen Date: Wed, 17 Apr 2024 14:12:04 +0800 Subject: [PATCH] fix: re-add support for gov v1beta1 messages (#725) ## Description Closes: #XXXX Currently, gov module is updated to gov v1, however, gov v1beta1 is still alive to chains, Cosmos-SDK doesn't completely remove v1beta1 support. As a result, we should re-add gov v1beta1 support to make sure our proposals data are also updated when gov v1beta1 messages are performed. Say, some of users on Cheqd is using gov.v1beta1.MsgVote to vote on the proposal. https://bigdipper.live/cheqd/transactions/19E4F068FBFBC46DA9BEF3E0F7CA908317D5582CAB2A17683138A5248ED34E3C --- ### Author Checklist *All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues.* I have... - [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] added `!` to the type prefix if API or client breaking change - [ ] targeted the correct branch - [ ] provided a link to the relevant issue or specification - [ ] added a changelog entry to `CHANGELOG.md` - [ ] included comments for [documenting Go code](https://blog.golang.org/godoc) - [ ] updated the relevant documentation or specification - [ ] reviewed "Files changed" and left comments if necessary - [ ] confirmed all CI checks have passed ### Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed `!` in the type prefix if API or client breaking change - [ ] confirmed all author checklist items have been addressed - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable) --- cmd/parse/gov/proposal.go | 72 ++++++++++++-------- modules/gov/handle_msg.go | 111 +++++++++++++++---------------- modules/gov/module.go | 9 +-- modules/gov/utils_events.go | 66 ++++++++++++++++++ modules/gov/utils_events_test.go | 71 ++++++++++++++++++++ utils/events/events.go | 25 +++++++ 6 files changed, 264 insertions(+), 90 deletions(-) create mode 100644 modules/gov/utils_events.go create mode 100644 modules/gov/utils_events_test.go create mode 100644 utils/events/events.go diff --git a/cmd/parse/gov/proposal.go b/cmd/parse/gov/proposal.go index e0aaeb9dc..899455972 100644 --- a/cmd/parse/gov/proposal.go +++ b/cmd/parse/gov/proposal.go @@ -10,6 +10,7 @@ import ( modulestypes "github.com/forbole/callisto/v4/modules/types" govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + govtypesv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" parsecmdtypes "github.com/forbole/juno/v5/cmd/parse/types" "github.com/forbole/juno/v5/parser" "github.com/forbole/juno/v5/types/config" @@ -124,13 +125,13 @@ func refreshProposalDetails(parseCtx *parser.Context, proposalID uint64, govModu // Handle the MsgSubmitProposal messages for index, msg := range tx.GetMsgs() { - if _, ok := msg.(*govtypesv1.MsgSubmitProposal); !ok { - continue - } - err = govModule.HandleMsg(index, msg, tx) - if err != nil { - return fmt.Errorf("error while handling MsgSubmitProposal: %s", err) + switch msg.(type) { + case *govtypesv1.MsgSubmitProposal, *govtypesv1beta1.MsgSubmitProposal: + err = govModule.HandleMsg(index, msg, tx) + if err != nil { + return fmt.Errorf("error while handling MsgSubmitProposal: %s", err) + } } } @@ -155,13 +156,12 @@ func refreshProposalDeposits(parseCtx *parser.Context, proposalID uint64, govMod // Handle the MsgDeposit messages for index, msg := range junoTx.GetMsgs() { - if _, ok := msg.(*govtypesv1.MsgDeposit); !ok { - continue - } - - err = govModule.HandleMsg(index, msg, junoTx) - if err != nil { - return fmt.Errorf("error while handling MsgDeposit: %s", err) + switch msg.(type) { + case *govtypesv1.MsgDeposit, *govtypesv1beta1.MsgDeposit: + err = govModule.HandleMsg(index, msg, junoTx) + if err != nil { + return fmt.Errorf("error while handling MsgDeposit: %s", err) + } } } } @@ -187,23 +187,39 @@ func refreshProposalVotes(parseCtx *parser.Context, proposalID uint64, govModule // Handle the MsgVote messages for index, msg := range junoTx.GetMsgs() { - if msgVote, ok := msg.(*govtypesv1.MsgVote); !ok { + var msgProposalID uint64 + + switch cosmosMsg := msg.(type) { + case *govtypesv1.MsgVote: + msgProposalID = cosmosMsg.ProposalId + + case *govtypesv1beta1.MsgVote: + msgProposalID = cosmosMsg.ProposalId + + case *govtypesv1.MsgVoteWeighted: + msgProposalID = cosmosMsg.ProposalId + + case *govtypesv1beta1.MsgVoteWeighted: + msgProposalID = cosmosMsg.ProposalId + + // Skip if the message is not a vote message + default: continue - } else { - // check if requested proposal ID is the same as proposal ID returned - // from the msg as some txs may contain multiple MsgVote msgs - // for different proposals which can cause error if one of the proposals - // info is not stored in database - if proposalID == msgVote.ProposalId { - err = govModule.HandleMsg(index, msg, junoTx) - if err != nil { - return fmt.Errorf("error while handling MsgVote: %s", err) - } - } else { - // skip votes for proposals with IDs - // different than requested in the query - continue + } + + // check if requested proposal ID is the same as proposal ID returned + // from the msg as some txs may contain multiple MsgVote msgs + // for different proposals which can cause error if one of the proposals + // info is not stored in database + if proposalID == msgProposalID { + err = govModule.HandleMsg(index, msg, junoTx) + if err != nil { + return fmt.Errorf("error while handling MsgVote: %s", err) } + } else { + // skip votes for proposals with IDs + // different than requested in the query + continue } } } diff --git a/modules/gov/handle_msg.go b/modules/gov/handle_msg.go index 8e8573b82..d8f0de4b2 100644 --- a/modules/gov/handle_msg.go +++ b/modules/gov/handle_msg.go @@ -2,20 +2,18 @@ package gov import ( "fmt" - "strconv" "strings" "time" - "github.com/cosmos/cosmos-sdk/x/authz" - "github.com/forbole/callisto/v4/types" "google.golang.org/grpc/codes" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/authz" distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + govtypesv1beta1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1beta1" - gov "github.com/cosmos/cosmos-sdk/x/gov/types" juno "github.com/forbole/juno/v5/types" ) @@ -32,37 +30,35 @@ func (m *Module) HandleMsg(index int, msg sdk.Msg, tx *juno.Tx) error { switch cosmosMsg := msg.(type) { case *govtypesv1.MsgSubmitProposal: - return m.handleMsgSubmitProposal(tx, index, cosmosMsg) + return m.handleSubmitProposalEvent(tx, cosmosMsg.Proposer, tx.Logs[index].Events) + case *govtypesv1beta1.MsgSubmitProposal: + return m.handleSubmitProposalEvent(tx, cosmosMsg.Proposer, tx.Logs[index].Events) case *govtypesv1.MsgDeposit: - return m.handleMsgDeposit(tx, cosmosMsg) + return m.handleDepositEvent(tx, cosmosMsg.Depositor, tx.Logs[index].Events) + case *govtypesv1beta1.MsgDeposit: + return m.handleDepositEvent(tx, cosmosMsg.Depositor, tx.Logs[index].Events) case *govtypesv1.MsgVote: - return m.handleMsgVote(tx, cosmosMsg) + return m.handleVoteEvent(tx, cosmosMsg.Voter, tx.Logs[index].Events) + case *govtypesv1beta1.MsgVote: + return m.handleVoteEvent(tx, cosmosMsg.Voter, tx.Logs[index].Events) case *govtypesv1.MsgVoteWeighted: - return m.handleMsgVoteWeighted(tx, cosmosMsg) + return m.handleVoteEvent(tx, cosmosMsg.Voter, tx.Logs[index].Events) + case *govtypesv1beta1.MsgVoteWeighted: + return m.handleVoteEvent(tx, cosmosMsg.Voter, tx.Logs[index].Events) } return nil } -// handleMsgSubmitProposal allows to properly handle a MsgSubmitProposal -func (m *Module) handleMsgSubmitProposal(tx *juno.Tx, index int, msg *govtypesv1.MsgSubmitProposal) error { +// handleSubmitProposalEvent allows to properly handle a handleSubmitProposalEvent +func (m *Module) handleSubmitProposalEvent(tx *juno.Tx, proposer string, events sdk.StringEvents) error { // Get the proposal id - event, err := tx.FindEventByType(index, gov.EventTypeSubmitProposal) - if err != nil { - return fmt.Errorf("error while searching for EventTypeSubmitProposal: %s", err) - } - - id, err := tx.FindAttributeByKey(event, gov.AttributeKeyProposalID) - if err != nil { - return fmt.Errorf("error while searching for AttributeKeyProposalID: %s", err) - } - - proposalID, err := strconv.ParseUint(id, 10, 64) + proposalID, err := ProposalIDFromEvents(events) if err != nil { - return fmt.Errorf("error while parsing proposal id: %s", err) + return fmt.Errorf("error while getting proposal id: %s", err) } // Get the proposal @@ -108,39 +104,45 @@ func (m *Module) handleMsgSubmitProposal(tx *juno.Tx, index int, msg *govtypesv1 return fmt.Errorf("error while storing proposal recipient: %s", err) } + // Unpack the proposal interfaces + err = proposal.UnpackInterfaces(m.cdc) + if err != nil { + return fmt.Errorf("error while unpacking proposal interfaces: %s", err) + } + // Store the proposal proposalObj := types.NewProposal( proposal.Id, proposal.Title, proposal.Summary, proposal.Metadata, - msg.Messages, + proposal.Messages, proposal.Status.String(), *proposal.SubmitTime, *proposal.DepositEndTime, proposal.VotingStartTime, proposal.VotingEndTime, - msg.Proposer, + proposer, ) err = m.db.SaveProposals([]types.Proposal{proposalObj}) if err != nil { - return err + return fmt.Errorf("error while saving proposal: %s", err) } - txTimestamp, err := time.Parse(time.RFC3339, tx.Timestamp) + // Submit proposal must have a deposit event with depositor equal to the proposer + return m.handleDepositEvent(tx, proposer, events) +} + +// handleDepositEvent allows to properly handle a handleDepositEvent +func (m *Module) handleDepositEvent(tx *juno.Tx, depositor string, events sdk.StringEvents) error { + // Get the proposal id + proposalID, err := ProposalIDFromEvents(events) if err != nil { - return fmt.Errorf("error while parsing time: %s", err) + return fmt.Errorf("error while getting proposal id: %s", err) } - // Store the deposit - deposit := types.NewDeposit(proposal.Id, msg.Proposer, msg.InitialDeposit, txTimestamp, tx.TxHash, tx.Height) - return m.db.SaveDeposits([]types.Deposit{deposit}) -} - -// handleMsgDeposit allows to properly handle a MsgDeposit -func (m *Module) handleMsgDeposit(tx *juno.Tx, msg *govtypesv1.MsgDeposit) error { - deposit, err := m.source.ProposalDeposit(tx.Height, msg.ProposalId, msg.Depositor) + deposit, err := m.source.ProposalDeposit(tx.Height, proposalID, depositor) if err != nil { return fmt.Errorf("error while getting proposal deposit: %s", err) } @@ -150,43 +152,36 @@ func (m *Module) handleMsgDeposit(tx *juno.Tx, msg *govtypesv1.MsgDeposit) error } return m.db.SaveDeposits([]types.Deposit{ - types.NewDeposit(msg.ProposalId, msg.Depositor, deposit.Amount, txTimestamp, tx.TxHash, tx.Height), + types.NewDeposit(proposalID, depositor, deposit.Amount, txTimestamp, tx.TxHash, tx.Height), }) } -// handleMsgVote allows to properly handle a MsgVote -func (m *Module) handleMsgVote(tx *juno.Tx, msg *govtypesv1.MsgVote) error { +// handleVoteEvent allows to properly handle a handleVoteEvent +func (m *Module) handleVoteEvent(tx *juno.Tx, voter string, events sdk.StringEvents) error { + // Get the proposal id + proposalID, err := ProposalIDFromEvents(events) + if err != nil { + return fmt.Errorf("error while getting proposal id: %s", err) + } + txTimestamp, err := time.Parse(time.RFC3339, tx.Timestamp) if err != nil { return fmt.Errorf("error while parsing time: %s", err) } - vote := types.NewVote(msg.ProposalId, msg.Voter, msg.Option, "1.0", txTimestamp, tx.Height) - - err = m.db.SaveVote(vote) + // Get the vote option + weightVoteOption, err := WeightVoteOptionFromEvents(events) if err != nil { - return fmt.Errorf("error while saving vote: %s", err) + return fmt.Errorf("error while getting vote option: %s", err) } - // update tally result for given proposal - return m.UpdateProposalTallyResult(msg.ProposalId, tx.Height) -} + vote := types.NewVote(proposalID, voter, weightVoteOption.Option, weightVoteOption.Weight, txTimestamp, tx.Height) -// handleMsgVoteWeighted allows to properly handle a MsgVoteWeighted -func (m *Module) handleMsgVoteWeighted(tx *juno.Tx, msg *govtypesv1.MsgVoteWeighted) error { - txTimestamp, err := time.Parse(time.RFC3339, tx.Timestamp) + err = m.db.SaveVote(vote) if err != nil { - return fmt.Errorf("error while parsing time: %s", err) - } - - for _, option := range msg.Options { - vote := types.NewVote(msg.ProposalId, msg.Voter, option.Option, option.Weight, txTimestamp, tx.Height) - err = m.db.SaveVote(vote) - if err != nil { - return fmt.Errorf("error while saving weighted vote for address %s: %s", msg.Voter, err) - } + return fmt.Errorf("error while saving vote: %s", err) } // update tally result for given proposal - return m.UpdateProposalTallyResult(msg.ProposalId, tx.Height) + return m.UpdateProposalTallyResult(proposalID, tx.Height) } diff --git a/modules/gov/module.go b/modules/gov/module.go index 5db42c0ad..34ca290f3 100644 --- a/modules/gov/module.go +++ b/modules/gov/module.go @@ -11,10 +11,11 @@ import ( ) var ( - _ modules.Module = &Module{} - _ modules.GenesisModule = &Module{} - _ modules.BlockModule = &Module{} - _ modules.MessageModule = &Module{} + _ modules.Module = &Module{} + _ modules.GenesisModule = &Module{} + _ modules.BlockModule = &Module{} + _ modules.MessageModule = &Module{} + _ modules.AuthzMessageModule = &Module{} ) // Module represent x/gov module diff --git a/modules/gov/utils_events.go b/modules/gov/utils_events.go new file mode 100644 index 000000000..772d9e802 --- /dev/null +++ b/modules/gov/utils_events.go @@ -0,0 +1,66 @@ +package gov + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + eventsutil "github.com/forbole/callisto/v4/utils/events" +) + +// ProposalIDFromEvent returns the proposal id from the given events +func ProposalIDFromEvents(events sdk.StringEvents) (uint64, error) { + for _, event := range events { + attribute, ok := eventsutil.FindAttributeByKey(event, govtypes.AttributeKeyProposalID) + if ok { + return strconv.ParseUint(attribute.Value, 10, 64) + } + } + + return 0, fmt.Errorf("no proposal id found") +} + +// WeightVoteOptionFromEvents returns the vote option from the given events +func WeightVoteOptionFromEvents(events sdk.StringEvents) (govtypesv1.WeightedVoteOption, error) { + for _, event := range events { + attribute, ok := eventsutil.FindAttributeByKey(event, govtypes.AttributeKeyOption) + if ok { + return parseWeightVoteOption(attribute.Value) + } + } + + return govtypesv1.WeightedVoteOption{}, fmt.Errorf("no vote option found") +} + +// parseWeightVoteOption returns the vote option from the given string +// option value in string has 2 cases, for example: +// 1. "{\"option\":1,\"weight\":\"1.000000000000000000\"}" +// 2. "option:VOTE_OPTION_NO weight:\"1.000000000000000000\"" +func parseWeightVoteOption(optionValue string) (govtypesv1.WeightedVoteOption, error) { + // try parse json option value + var weightedVoteOption govtypesv1.WeightedVoteOption + err := json.Unmarshal([]byte(optionValue), &weightedVoteOption) + if err == nil { + return weightedVoteOption, nil + } + + // try parse string option value + // option:VOTE_OPTION_NO weight:"1.000000000000000000" + voteOptionParsed := strings.Split(optionValue, " ") + if len(voteOptionParsed) != 2 { + return govtypesv1.WeightedVoteOption{}, fmt.Errorf("failed to parse vote option %s", optionValue) + } + + voteOption, err := govtypesv1.VoteOptionFromString(strings.ReplaceAll(voteOptionParsed[0], "option:", "")) + if err != nil { + return govtypesv1.WeightedVoteOption{}, fmt.Errorf("failed to parse vote option %s: %s", optionValue, err) + } + weight := strings.ReplaceAll(voteOptionParsed[1], "weight:", "") + weight = strings.ReplaceAll(weight, "\"", "") + + return govtypesv1.WeightedVoteOption{Option: voteOption, Weight: weight}, nil +} diff --git a/modules/gov/utils_events_test.go b/modules/gov/utils_events_test.go new file mode 100644 index 000000000..8605e653b --- /dev/null +++ b/modules/gov/utils_events_test.go @@ -0,0 +1,71 @@ +package gov_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + govtypesv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" + "github.com/forbole/callisto/v4/modules/gov" + "github.com/stretchr/testify/require" +) + +func TestWeightVoteOptionFromEvents(t *testing.T) { + tests := []struct { + name string + events sdk.StringEvents + expected govtypesv1.WeightedVoteOption + shouldErr bool + }{ + { + "json option from vote event returns properly", + sdk.StringEvents{ + sdk.StringEvent{ + Type: "vote", + Attributes: []sdk.Attribute{ + sdk.NewAttribute(govtypes.AttributeKeyOption, "{\"option\":1,\"weight\":\"1.000000000000000000\"}"), + }, + }, + }, + govtypesv1.WeightedVoteOption{Option: govtypesv1.OptionYes, Weight: "1.000000000000000000"}, + false, + }, + { + "string option from vote event returns properly", + sdk.StringEvents{ + sdk.StringEvent{ + Type: "vote", + Attributes: []sdk.Attribute{ + sdk.NewAttribute(govtypes.AttributeKeyOption, "option:VOTE_OPTION_NO weight:\"1.000000000000000000\""), + }, + }, + }, + govtypesv1.WeightedVoteOption{Option: govtypesv1.OptionNo, Weight: "1.000000000000000000"}, + false, + }, + { + "invalid option from vote event returns error", + sdk.StringEvents{ + sdk.StringEvent{ + Type: "vote", + Attributes: []sdk.Attribute{ + sdk.NewAttribute("other", "value"), + }, + }, + }, + govtypesv1.WeightedVoteOption{}, + true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := gov.WeightVoteOptionFromEvents(test.events) + if test.shouldErr { + require.Error(t, err) + } else { + require.Equal(t, test.expected, result) + } + }) + } +} diff --git a/utils/events/events.go b/utils/events/events.go new file mode 100644 index 000000000..5b175dbcd --- /dev/null +++ b/utils/events/events.go @@ -0,0 +1,25 @@ +package events + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// FindEventByType returns the event with the given type +func FindEventByType(events sdk.StringEvents, eventType string) (sdk.StringEvent, bool) { + for _, event := range events { + if event.Type == eventType { + return event, true + } + } + return sdk.StringEvent{}, false +} + +// FindAttributeByKey returns the attribute with the given key +func FindAttributeByKey(event sdk.StringEvent, key string) (sdk.Attribute, bool) { + for _, attribute := range event.Attributes { + if attribute.Key == key { + return attribute, true + } + } + return sdk.Attribute{}, false +}