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 +}