Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: re-add support for gov v1beta1 messages #725

Merged
merged 5 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 42 additions & 17 deletions cmd/parse/gov/proposal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -124,7 +125,11 @@ func refreshProposalDetails(parseCtx *parser.Context, proposalID uint64, govModu

// Handle the MsgSubmitProposal messages
for index, msg := range tx.GetMsgs() {
if _, ok := msg.(*govtypesv1.MsgSubmitProposal); !ok {
_, isMsgSubmitProposalV1 := msg.(*govtypesv1.MsgSubmitProposal)
_, isMsgSubmitProposalV1Beta1 := msg.(*govtypesv1beta1.MsgSubmitProposal)

// Skip if the message is not a submit proposal message
if !isMsgSubmitProposalV1 && !isMsgSubmitProposalV1Beta1 {
continue
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about

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

		}

Copy link
Contributor Author

@dadamu dadamu Mar 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, it is cleaner, thanks! 3936cc6

Expand Down Expand Up @@ -155,7 +160,11 @@ func refreshProposalDeposits(parseCtx *parser.Context, proposalID uint64, govMod

// Handle the MsgDeposit messages
for index, msg := range junoTx.GetMsgs() {
if _, ok := msg.(*govtypesv1.MsgDeposit); !ok {
_, isMsgDepositV1beta1 := msg.(*govtypesv1beta1.MsgDeposit)
_, isMsgDepositV1 := msg.(*govtypesv1.MsgDeposit)

// Skip if the message is not a deposit message
if !isMsgDepositV1 && !isMsgDepositV1beta1 {
continue
}

Expand Down Expand Up @@ -187,23 +196,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
}
}
}
Expand Down
111 changes: 53 additions & 58 deletions modules/gov/handle_msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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)
Copy link
Contributor Author

@dadamu dadamu Mar 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need put proposer from message since Cosmos-SDK does not support proposer attribute inside submit_proposal event, it maybe better we post a request to Cosmos-SDK team or just remove this field in the feature. From my side, I would remove it as I would see depositors more than proposer, in addition, proposer must be one of depositors.

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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Voter event is supported by 0.47.5, we currently better to put Voter from message to make it backward compatible.
https://github.com/cosmos/cosmos-sdk/blob/main/CHANGELOG.md#v0475---2023-09-01

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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

V1beta1 and V1 messages are sharing the same event, so we can parse event rather than push message directly so that we don't support many version messages later.

https://docs.cosmos.network/main/build/modules/gov#events

// 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
Expand Down Expand Up @@ -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)
}
Comment on lines +108 to +111
Copy link
Contributor Author

@dadamu dadamu Mar 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using codec to unpack Any type messages to the underlying structure, then we can get proposal message by proposal.Messages without error.


// 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)
}
Expand All @@ -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 {
Copy link
Contributor Author

@dadamu dadamu Mar 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shares the same vote event as MsgVote, so it can be removed safely.

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)
}
9 changes: 5 additions & 4 deletions modules/gov/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 66 additions & 0 deletions modules/gov/utils_events.go
Original file line number Diff line number Diff line change
@@ -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\""
Comment on lines +41 to +42
Copy link
Contributor Author

@dadamu dadamu Mar 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VoteOption inside event is represented in these two type format, v1 and v1beta1 currently.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still trying to understand cosmos SDK.

We are not able to use provided methods like:

func (m *Vote) GetOptions() []*WeightedVoteOption {
	if m != nil {
		return m.Options
	}
	return nil
}

func parseWeightVoteOption(optionValue string) (govtypesv1.WeightedVoteOption, error) {
// try parse option value as json
var weightedVoteOption govtypesv1.WeightedVoteOption
err := json.Unmarshal([]byte(optionValue), &weightedVoteOption)
if err == nil {
return weightedVoteOption, nil
}

// try parse option value as string
// 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
}
25 changes: 25 additions & 0 deletions utils/events/events.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading