From b27cf2b03fb0b430924df2dd1ffaa17c73f04fd0 Mon Sep 17 00:00:00 2001 From: Sergey <83376337+freak12techno@users.noreply.github.com> Date: Sun, 4 Jun 2023 12:04:34 +0300 Subject: [PATCH] feat: parse v1 proposals (#48) * feat: parse v1 proposals * feat: fix defaults --- config.example.toml | 10 ++++ go.mod | 4 +- go.sum | 4 +- pkg/config/config.go | 30 ++++++----- pkg/config/config_test.go | 31 +++++++++--- pkg/config/types/types.go | 17 +++++-- pkg/logger/logger.go | 5 +- pkg/report/generator.go | 4 +- pkg/report/generator_test.go | 26 ++++------ pkg/reporters/pagerduty/pagerduty.go | 14 +++--- pkg/reporters/telegram/telegram.go | 4 +- pkg/state/generator.go | 12 ++--- pkg/state/state.go | 6 +-- pkg/state/state_test.go | 2 +- pkg/tendermint/tendermint.go | 64 +++++++++++++++++++++--- pkg/types/responses.go | 63 +++++++++++++++++++---- pkg/types/types.go | 17 +++++++ pkg/utils/utils.go | 10 ++++ templates/telegram/not_voted.html | 6 +-- templates/telegram/proposals.html | 2 +- templates/telegram/revoted.html | 6 +-- templates/telegram/vote_query_error.html | 2 +- templates/telegram/voted.html | 6 +-- 23 files changed, 248 insertions(+), 97 deletions(-) diff --git a/config.example.toml b/config.example.toml index 3b79220..3dd0632 100644 --- a/config.example.toml +++ b/config.example.toml @@ -37,6 +37,16 @@ wallets = [ { address = "bitsong14rvn7anf22e00vj5x3al4w50ns78s7n4t8yxcy", alias = "Validator wallet" }, { address = "bitsong125hdkukw4pu2urhj4nv366q0avdqv24t0vprxs" }, ] +# Some chains have a new proposals structure (v1) compared to an older one (v1beta1), +# when there are 2 or more actual proposals inside a single one (namely, Quicksilver). +# On such chains, querying proposals with an older endpoint when there are proposals +# with 2 or more proposals inside causes an error like this: +# "codespace sdk code 29: invalid type: can't convert a gov/v1 Proposal to gov/v1beta1 Proposal +# when amount of proposal messages is more than one". If you see this error, consider switching to +# a newer endpoint. Keep in mind that some chains do not implement the newer format. +# The possible values are: "v1" (newer format), "v1beta1" (older format). +# Defaults to "v1beta1" +proposals-type = "v1beta1" # Custom explorer links patterns. They are overridden if mintscan-prefix is specified. [chains.explorer] # A pattern for proposal link for explorer, if there's no Mintscan support diff --git a/go.mod b/go.mod index 78ff6fa..5ad2fff 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/BurntSushi/toml v1.1.0 - github.com/mcuadros/go-defaults v1.2.0 + github.com/creasty/defaults v1.7.0 github.com/robfig/cron/v3 v3.0.1 github.com/rs/zerolog v1.26.1 github.com/spf13/cobra v1.4.0 @@ -15,7 +15,9 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index a8d32b4..634b7d3 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= +github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -24,8 +26,6 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc= -github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/pkg/config/config.go b/pkg/config/config.go index 22cdcae..daaadf3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -2,22 +2,23 @@ package config import ( "fmt" + "main/pkg/logger" "os" - "main/pkg/config/types" + configTypes "main/pkg/config/types" "github.com/BurntSushi/toml" - "github.com/mcuadros/go-defaults" + "github.com/creasty/defaults" ) type Config struct { - PagerDutyConfig PagerDutyConfig `toml:"pagerduty"` - TelegramConfig TelegramConfig `toml:"telegram"` - LogConfig LogConfig `toml:"log"` - StatePath string `toml:"state-path"` - MutesPath string `toml:"mutes-path"` - Chains types.Chains `toml:"chains"` - Interval string `toml:"interval" default:"* * * * *"` + PagerDutyConfig PagerDutyConfig `toml:"pagerduty"` + TelegramConfig TelegramConfig `toml:"telegram"` + LogConfig configTypes.LogConfig `toml:"log"` + StatePath string `toml:"state-path"` + MutesPath string `toml:"mutes-path"` + Chains configTypes.Chains `toml:"chains"` + Interval string `toml:"interval" default:"* * * * *"` } type PagerDutyConfig struct { @@ -30,11 +31,6 @@ type TelegramConfig struct { TelegramToken string `toml:"token"` } -type LogConfig struct { - LogLevel string `toml:"level" default:"info"` - JSONOutput bool `toml:"json" default:"false"` -} - func (c *Config) Validate() error { if len(c.Chains) == 0 { return fmt.Errorf("no chains provided") @@ -62,11 +58,13 @@ func GetConfig(path string) (*Config, error) { return nil, err } - defaults.SetDefaults(configStruct) + if err := defaults.Set(configStruct); err != nil { + logger.GetDefaultLogger().Fatal().Err(err).Msg("Error setting default config values") + } for _, chain := range configStruct.Chains { if chain.MintscanPrefix != "" { - chain.Explorer = &types.Explorer{ + chain.Explorer = &configTypes.Explorer{ ProposalLinkPattern: fmt.Sprintf("https://mintscan.io/%s/proposals/%%s", chain.MintscanPrefix), WalletLinkPattern: fmt.Sprintf("https://mintscan.io/%s/account/%%s", chain.MintscanPrefix), } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 15b8cde..adab1aa 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -48,9 +48,10 @@ func TestValidateChainWithValidConfig(t *testing.T) { t.Parallel() chain := configTypes.Chain{ - Name: "chain", - LCDEndpoints: []string{"endpoint"}, - Wallets: []*configTypes.Wallet{{Address: "wallet"}}, + Name: "chain", + LCDEndpoints: []string{"endpoint"}, + Wallets: []*configTypes.Wallet{{Address: "wallet"}}, + ProposalsType: "v1", } err := chain.Validate() @@ -105,15 +106,33 @@ func TestValidateConfigInvalidChain(t *testing.T) { assert.NotEqual(t, err, nil, "Error should be presented!") } +func TestValidateConfigWrongProposalType(t *testing.T) { + t.Parallel() + + config := Config{ + Chains: []*configTypes.Chain{ + { + Name: "chain", + LCDEndpoints: []string{"endpoint"}, + Wallets: []*configTypes.Wallet{{Address: "wallet"}}, + ProposalsType: "test", + }, + }, + } + err := config.Validate() + assert.NotEqual(t, err, nil, "Error should be presented!") +} + func TestValidateConfigValidChain(t *testing.T) { t.Parallel() config := Config{ Chains: []*configTypes.Chain{ { - Name: "chain", - LCDEndpoints: []string{"endpoint"}, - Wallets: []*configTypes.Wallet{{Address: "wallet"}}, + Name: "chain", + LCDEndpoints: []string{"endpoint"}, + Wallets: []*configTypes.Wallet{{Address: "wallet"}}, + ProposalsType: "v1", }, }, } diff --git a/pkg/config/types/types.go b/pkg/config/types/types.go index 7fe4451..b6d6da9 100644 --- a/pkg/config/types/types.go +++ b/pkg/config/types/types.go @@ -3,6 +3,7 @@ package types import ( "fmt" "main/pkg/types" + "main/pkg/utils" ) type Explorer struct { @@ -28,6 +29,7 @@ type Chain struct { PrettyName string `toml:"pretty-name"` KeplrName string `toml:"keplr-name"` LCDEndpoints []string `toml:"lcd-endpoints"` + ProposalsType string `toml:"proposals-type" default:"v1beta1"` Wallets []*Wallet `toml:"wallets"` MintscanPrefix string `toml:"mintscan-prefix"` Explorer *Explorer `toml:"explorer"` @@ -46,6 +48,10 @@ func (c *Chain) Validate() error { return fmt.Errorf("no wallets provided") } + if !utils.Contains([]string{"v1beta1", "v1"}, c.ProposalsType) { + return fmt.Errorf("wrong proposals type: expected one of 'v1beta1', 'v1', but got %s", c.ProposalsType) + } + for index, wallet := range c.Wallets { if wallet.Address == "" { return fmt.Errorf("wallet #%d: address is empty", index) @@ -85,12 +91,12 @@ func (c Chain) GetExplorerProposalsLinks(proposalID string) []types.Link { func (c Chain) GetProposalLink(proposal types.Proposal) types.Link { if c.Explorer == nil || c.Explorer.ProposalLinkPattern == "" { - return types.Link{Name: proposal.Content.Title} + return types.Link{Name: proposal.Title} } return types.Link{ - Name: proposal.Content.Title, - Href: fmt.Sprintf(c.Explorer.ProposalLinkPattern, proposal.ProposalID), + Name: proposal.Title, + Href: fmt.Sprintf(c.Explorer.ProposalLinkPattern, proposal.ID), } } @@ -122,3 +128,8 @@ func (c Chains) FindByName(name string) *Chain { return nil } + +type LogConfig struct { + LogLevel string `toml:"level" default:"info"` + JSONOutput bool `toml:"json" default:"false"` +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 3a4dfcc..9c487dd 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -1,10 +1,9 @@ package logger import ( + configTypes "main/pkg/config/types" "os" - "main/pkg/config" - "github.com/rs/zerolog" ) @@ -13,7 +12,7 @@ func GetDefaultLogger() *zerolog.Logger { return &log } -func GetLogger(config config.LogConfig) *zerolog.Logger { +func GetLogger(config configTypes.LogConfig) *zerolog.Logger { log := zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout}).With().Timestamp().Logger() if config.JSONOutput { diff --git a/pkg/report/generator.go b/pkg/report/generator.go index e16d14b..830e613 100644 --- a/pkg/report/generator.go +++ b/pkg/report/generator.go @@ -91,7 +91,7 @@ func (g *Generator) GenerateReport(oldState, newState state.State) reporters.Rep if newVote.HasVoted() && !oldVote.HasVoted() { g.Logger.Debug(). Str("chain", chainName). - Str("proposal", proposal.ProposalID). + Str("proposal", proposal.ID). Str("wallet", wallet). Msg("Wallet hasn't voted before but voted now - closing an alert") @@ -108,7 +108,7 @@ func (g *Generator) GenerateReport(oldState, newState state.State) reporters.Rep if newVote.HasVoted() && oldVote.HasVoted() && newVote.Vote.Option != oldVote.Vote.Option { g.Logger.Debug(). Str("chain", chainName). - Str("proposal", proposal.ProposalID). + Str("proposal", proposal.ID). Str("wallet", wallet). Msg("Wallet changed its vote - sending an alert") diff --git a/pkg/report/generator_test.go b/pkg/report/generator_test.go index 0d5c408..bb79634 100644 --- a/pkg/report/generator_test.go +++ b/pkg/report/generator_test.go @@ -50,8 +50,7 @@ func TestReportGeneratorWithVoteError(t *testing.T) { ProposalVotes: map[string]state.WalletVotes{ "proposal": { Proposal: types.Proposal{ - ProposalID: "proposal", - Content: &types.ProposalContent{}, + ID: "proposal", }, Votes: map[string]state.ProposalVote{ "wallet": { @@ -73,7 +72,7 @@ func TestReportGeneratorWithVoteError(t *testing.T) { entry, ok := report.Entries[0].(events.VoteQueryError) assert.True(t, ok, "Expected to have a vote query error!") - assert.Equal(t, entry.Proposal.ProposalID, "proposal", "Proposal ID mismatch!") + assert.Equal(t, entry.Proposal.ID, "proposal", "Proposal ID mismatch!") } func TestReportGeneratorWithNotVoted(t *testing.T) { @@ -88,8 +87,7 @@ func TestReportGeneratorWithNotVoted(t *testing.T) { ProposalVotes: map[string]state.WalletVotes{ "proposal": { Proposal: types.Proposal{ - ProposalID: "proposal", - Content: &types.ProposalContent{}, + ID: "proposal", }, Votes: map[string]state.ProposalVote{ "wallet": {}, @@ -109,7 +107,7 @@ func TestReportGeneratorWithNotVoted(t *testing.T) { entry, ok := report.Entries[0].(events.NotVotedEvent) assert.True(t, ok, "Expected to have not voted type!") - assert.Equal(t, entry.Proposal.ProposalID, "proposal", "Proposal ID mismatch!") + assert.Equal(t, entry.Proposal.ID, "proposal", "Proposal ID mismatch!") } func TestReportGeneratorWithVoted(t *testing.T) { @@ -123,8 +121,7 @@ func TestReportGeneratorWithVoted(t *testing.T) { ProposalVotes: map[string]state.WalletVotes{ "proposal": { Proposal: types.Proposal{ - ProposalID: "proposal", - Content: &types.ProposalContent{}, + ID: "proposal", }, Votes: map[string]state.ProposalVote{ "wallet": {}, @@ -140,8 +137,7 @@ func TestReportGeneratorWithVoted(t *testing.T) { ProposalVotes: map[string]state.WalletVotes{ "proposal": { Proposal: types.Proposal{ - ProposalID: "proposal", - Content: &types.ProposalContent{}, + ID: "proposal", }, Votes: map[string]state.ProposalVote{ "wallet": { @@ -165,7 +161,7 @@ func TestReportGeneratorWithVoted(t *testing.T) { entry, ok := report.Entries[0].(events.VotedEvent) assert.True(t, ok, "Expected to have voted type!") - assert.Equal(t, entry.Proposal.ProposalID, "proposal", "Proposal ID mismatch!") + assert.Equal(t, entry.Proposal.ID, "proposal", "Proposal ID mismatch!") } func TestReportGeneratorWithRevoted(t *testing.T) { @@ -179,8 +175,7 @@ func TestReportGeneratorWithRevoted(t *testing.T) { ProposalVotes: map[string]state.WalletVotes{ "proposal": { Proposal: types.Proposal{ - ProposalID: "proposal", - Content: &types.ProposalContent{}, + ID: "proposal", }, Votes: map[string]state.ProposalVote{ "wallet": { @@ -200,8 +195,7 @@ func TestReportGeneratorWithRevoted(t *testing.T) { ProposalVotes: map[string]state.WalletVotes{ "proposal": { Proposal: types.Proposal{ - ProposalID: "proposal", - Content: &types.ProposalContent{}, + ID: "proposal", }, Votes: map[string]state.ProposalVote{ "wallet": { @@ -225,5 +219,5 @@ func TestReportGeneratorWithRevoted(t *testing.T) { entry, ok := report.Entries[0].(events.RevotedEvent) assert.True(t, ok, "Expected to have revoted type!") - assert.Equal(t, entry.Proposal.ProposalID, "proposal", "Proposal ID mismatch!") + assert.Equal(t, entry.Proposal.ID, "proposal", "Proposal ID mismatch!") } diff --git a/pkg/reporters/pagerduty/pagerduty.go b/pkg/reporters/pagerduty/pagerduty.go index 0b64a5f..240af53 100644 --- a/pkg/reporters/pagerduty/pagerduty.go +++ b/pkg/reporters/pagerduty/pagerduty.go @@ -65,7 +65,7 @@ func (r *Reporter) NewAlertFromReportEntry(eventRaw entry.ReportEntry) (Alert, e dedupKey := fmt.Sprintf( "cosmos-proposals-checker alert chain=%s proposal=%s wallet=%s", event.GetChain().Name, - event.GetProposal().ProposalID, + event.GetProposal().ID, event.GetWallet().AddressOrAlias(), ) @@ -75,7 +75,7 @@ func (r *Reporter) NewAlertFromReportEntry(eventRaw entry.ReportEntry) (Alert, e } links := []Link{} - explorerLinks := event.GetChain().GetExplorerProposalsLinks(event.GetProposal().ProposalID) + explorerLinks := event.GetChain().GetExplorerProposalsLinks(event.GetProposal().ID) for _, link := range explorerLinks { links = append(links, Link{ Href: link.Href, @@ -88,9 +88,9 @@ func (r *Reporter) NewAlertFromReportEntry(eventRaw entry.ReportEntry) (Alert, e Summary: fmt.Sprintf( "Wallet %s hasn't voted on proposal %s on %s: %s", event.GetWallet().AddressOrAlias(), - event.GetProposal().ProposalID, + event.GetProposal().ID, event.GetChain().GetName(), - event.GetProposal().Content.Title, + event.GetProposal().Title, ), Timestamp: time.Now().Format(time.RFC3339), Severity: "error", @@ -98,9 +98,9 @@ func (r *Reporter) NewAlertFromReportEntry(eventRaw entry.ReportEntry) (Alert, e CustomDetails: map[string]string{ "Wallet": event.GetWallet().AddressOrAlias(), "Chain": event.GetChain().GetName(), - "Proposal ID": event.GetProposal().ProposalID, - "Proposal title": event.GetProposal().Content.Title, - "Proposal description": event.GetProposal().Content.Description, + "Proposal ID": event.GetProposal().ID, + "Proposal title": event.GetProposal().Title, + "Proposal description": event.GetProposal().Description, }, }, Links: links, diff --git a/pkg/reporters/telegram/telegram.go b/pkg/reporters/telegram/telegram.go index b9fce88..ae0e621 100644 --- a/pkg/reporters/telegram/telegram.go +++ b/pkg/reporters/telegram/telegram.go @@ -124,10 +124,10 @@ func (reporter *Reporter) SendReport(report reporters.Report) error { if entryConverted, ok := reportEntry.(entry.ReportEntryNotError); ok { chain := entryConverted.GetChain() proposal := entryConverted.GetProposal() - if reporter.MutesManager.IsMuted(chain.Name, proposal.ProposalID) { + if reporter.MutesManager.IsMuted(chain.Name, proposal.ID) { reporter.Logger.Debug(). Str("chain", chain.Name). - Str("proposal", proposal.ProposalID). + Str("proposal", proposal.ID). Msg("Notifications are muted, not sending.") continue } diff --git a/pkg/state/generator.go b/pkg/state/generator.go index 6e0f62c..46c4003 100644 --- a/pkg/state/generator.go +++ b/pkg/state/generator.go @@ -45,7 +45,7 @@ func (g *Generator) ProcessChain( state State, oldState State, ) { - rpc := tendermint.NewRPC(chain.LCDEndpoints, g.Logger) + rpc := tendermint.NewRPC(chain, g.Logger) proposals, err := rpc.GetAllProposals() if err != nil { @@ -71,13 +71,13 @@ func (g *Generator) ProcessChain( for _, proposal := range proposals { g.Logger.Trace(). Str("name", chain.Name). - Str("proposal", proposal.ProposalID). + Str("proposal", proposal.ID). Msg("Processing a proposal") for _, wallet := range chain.Wallets { g.Logger.Trace(). Str("name", chain.Name). - Str("proposal", proposal.ProposalID). + Str("proposal", proposal.ID). Str("wallet", wallet.Address). Msg("Processing wallet vote") wg.Add(1) @@ -100,13 +100,13 @@ func (g *Generator) ProcessProposalAndWallet( state State, oldState State, ) { - oldVote, _, found := oldState.GetVoteAndProposal(chain.Name, proposal.ProposalID, wallet.Address) - voteResponse, err := rpc.GetVote(proposal.ProposalID, wallet.Address) + oldVote, _, found := oldState.GetVoteAndProposal(chain.Name, proposal.ID, wallet.Address) + voteResponse, err := rpc.GetVote(proposal.ID, wallet.Address) if found && oldVote.HasVoted() && voteResponse.Vote == nil { g.Logger.Trace(). Str("chain", chain.Name). - Str("proposal", proposal.ProposalID). + Str("proposal", proposal.ID). Str("wallet", wallet.Address). Msg("Wallet has voted and there's no vote in the new state - using old vote") diff --git a/pkg/state/state.go b/pkg/state/state.go index 35a3c48..aca921b 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -52,14 +52,14 @@ func (s *State) SetVote(chain *configTypes.Chain, proposal types.Proposal, walle } } - if _, ok := s.ChainInfos[chain.Name].ProposalVotes[proposal.ProposalID]; !ok { - s.ChainInfos[chain.Name].ProposalVotes[proposal.ProposalID] = WalletVotes{ + if _, ok := s.ChainInfos[chain.Name].ProposalVotes[proposal.ID]; !ok { + s.ChainInfos[chain.Name].ProposalVotes[proposal.ID] = WalletVotes{ Proposal: proposal, Votes: make(map[string]ProposalVote), } } - s.ChainInfos[chain.Name].ProposalVotes[proposal.ProposalID].Votes[wallet.Address] = vote + s.ChainInfos[chain.Name].ProposalVotes[proposal.ID].Votes[wallet.Address] = vote } func (s *State) SetChainProposalsError(chain *configTypes.Chain, err error) { diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index 46370f0..f88fedc 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -19,7 +19,7 @@ func TestSetVoteWithoutChainInfo(t *testing.T) { state.SetVote( &configTypes.Chain{Name: "chain"}, - types.Proposal{ProposalID: "proposal"}, + types.Proposal{ID: "proposal"}, &configTypes.Wallet{Address: "wallet"}, ProposalVote{ Vote: &types.Vote{ diff --git a/pkg/tendermint/tendermint.go b/pkg/tendermint/tendermint.go index cd8bf31..07b83dd 100644 --- a/pkg/tendermint/tendermint.go +++ b/pkg/tendermint/tendermint.go @@ -4,6 +4,8 @@ import ( "encoding/json" "errors" "fmt" + configTypes "main/pkg/config/types" + "main/pkg/utils" "net/http" "strings" "time" @@ -16,18 +18,28 @@ import ( const PaginationLimit = 1000 type RPC struct { - URLs []string - Logger zerolog.Logger + URLs []string + ProposalsType string + Logger zerolog.Logger } -func NewRPC(urls []string, logger zerolog.Logger) *RPC { +func NewRPC(chainConfig *configTypes.Chain, logger zerolog.Logger) *RPC { return &RPC{ - URLs: urls, - Logger: logger.With().Str("component", "rpc").Logger(), + URLs: chainConfig.LCDEndpoints, + ProposalsType: chainConfig.ProposalsType, + Logger: logger.With().Str("component", "rpc").Logger(), } } func (rpc *RPC) GetAllProposals() ([]types.Proposal, error) { + if rpc.ProposalsType == "v1" { + return rpc.GetAllV1Proposals() + } + + return rpc.GetAllV1beta1Proposals() +} + +func (rpc *RPC) GetAllV1beta1Proposals() ([]types.Proposal, error) { proposals := []types.Proposal{} offset := 0 @@ -39,7 +51,42 @@ func (rpc *RPC) GetAllProposals() ([]types.Proposal, error) { offset, ) - var batchProposals types.ProposalsRPCResponse + var batchProposals types.V1Beta1ProposalsRPCResponse + if err := rpc.Get(url, &batchProposals); err != nil { + return nil, err + } + + if batchProposals.Message != "" { + return nil, errors.New(batchProposals.Message) + } + + parsedProposals := utils.Map(batchProposals.Proposals, func(p types.V1beta1Proposal) types.Proposal { + return p.ToProposal() + }) + proposals = append(proposals, parsedProposals...) + if len(batchProposals.Proposals) < PaginationLimit { + break + } + + offset += PaginationLimit + } + + return proposals, nil +} + +func (rpc *RPC) GetAllV1Proposals() ([]types.Proposal, error) { + proposals := []types.Proposal{} + offset := 0 + + for { + url := fmt.Sprintf( + // 2 is for PROPOSAL_STATUS_VOTING_PERIOD + "/cosmos/gov/v1/proposals?pagination.limit=%d&pagination.offset=%d&proposal_status=2", + PaginationLimit, + offset, + ) + + var batchProposals types.V1ProposalsRPCResponse if err := rpc.Get(url, &batchProposals); err != nil { return nil, err } @@ -48,7 +95,10 @@ func (rpc *RPC) GetAllProposals() ([]types.Proposal, error) { return nil, errors.New(batchProposals.Message) } - proposals = append(proposals, batchProposals.Proposals...) + parsedProposals := utils.Map(batchProposals.Proposals, func(p types.V1Proposal) types.Proposal { + return p.ToProposal() + }) + proposals = append(proposals, parsedProposals...) if len(batchProposals.Proposals) < PaginationLimit { break } diff --git a/pkg/types/responses.go b/pkg/types/responses.go index 701710a..60b44da 100644 --- a/pkg/types/responses.go +++ b/pkg/types/responses.go @@ -7,19 +7,22 @@ import ( "main/pkg/utils" ) -type Proposal struct { +// cosmos/gov/v1beta1/proposals?pagination.limit=1000&pagination.offset=0 + +type V1beta1Proposal struct { ProposalID string `json:"proposal_id"` Status string `json:"status"` Content *ProposalContent `json:"content"` VotingEndTime time.Time `json:"voting_end_time"` } -func (p Proposal) GetTimeLeft() string { - return utils.FormatDuration(time.Until(p.VotingEndTime).Round(time.Second)) -} - -func (p Proposal) GetProposalTime() string { - return p.VotingEndTime.Format(time.RFC1123) +func (p V1beta1Proposal) ToProposal() Proposal { + return Proposal{ + ID: p.ProposalID, + Title: p.Content.Title, + Description: p.Content.Description, + EndTime: p.VotingEndTime, + } } type ProposalContent struct { @@ -27,12 +30,50 @@ type ProposalContent struct { Description string `json:"description"` } -type ProposalsRPCResponse struct { - Code int64 `json:"code"` - Message string `json:"message"` - Proposals []Proposal `json:"proposals"` +type V1Beta1ProposalsRPCResponse struct { + Code int64 `json:"code"` + Message string `json:"message"` + Proposals []V1beta1Proposal `json:"proposals"` +} + +// cosmos/gov/v1beta1/proposals?pagination.limit=1000&pagination.offset=0 + +type V1ProposalMessage struct { + Content ProposalContent `json:"content"` +} + +type V1Proposal struct { + ProposalID string `json:"id"` + Status string `json:"status"` + VotingEndTime time.Time `json:"voting_end_time"` + Messages []V1ProposalMessage `json:"messages"` } +func (p V1Proposal) ToProposal() Proposal { + titles := utils.Map(p.Messages, func(m V1ProposalMessage) string { + return m.Content.Title + }) + + descriptions := utils.Map(p.Messages, func(m V1ProposalMessage) string { + return m.Content.Description + }) + + return Proposal{ + ID: p.ProposalID, + Title: strings.Join(titles, ", "), + Description: strings.Join(descriptions, ", "), + EndTime: p.VotingEndTime, + } +} + +type V1ProposalsRPCResponse struct { + Code int64 `json:"code"` + Message string `json:"message"` + Proposals []V1Proposal `json:"proposals"` +} + +// cosmos/gov/v1beta1/proposals/:id/votes/:wallet + type Vote struct { ProposalID string `json:"proposal_id"` Voter string `json:"voter"` diff --git a/pkg/types/types.go b/pkg/types/types.go index 9627c33..b99a5cb 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -2,6 +2,8 @@ package types import ( "fmt" + "main/pkg/utils" + "time" ) type Link struct { @@ -16,3 +18,18 @@ func (l Link) Serialize() string { return fmt.Sprintf("%s", l.Href, l.Name) } + +type Proposal struct { + ID string + Title string + Description string + EndTime time.Time +} + +func (p Proposal) GetTimeLeft() string { + return utils.FormatDuration(time.Until(p.EndTime).Round(time.Second)) +} + +func (p Proposal) GetProposalTime() string { + return p.EndTime.Format(time.RFC1123) +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index d85429b..8926ed1 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -27,6 +27,16 @@ func Map[T, V any](slice []T, f func(T) V) []V { return result } +func Contains[T comparable](slice []T, elt T) bool { + for _, value := range slice { + if value == elt { + return true + } + } + + return false +} + func ResolveVote(value string) string { votes := map[string]string{ "VOTE_OPTION_YES": "Yes", diff --git a/templates/telegram/not_voted.html b/templates/telegram/not_voted.html index 822f7e1..be3b08b 100644 --- a/templates/telegram/not_voted.html +++ b/templates/telegram/not_voted.html @@ -1,9 +1,9 @@ {{- $walletLink := .Chain.GetWalletLink .Wallet -}} -🔴 Wallet {{ SerializeLink $walletLink }} hasn't voted on proposal {{ .Proposal.ProposalID }} on {{ .Chain.GetName }} -{{ .Proposal.Content.Title }} +🔴 Wallet {{ SerializeLink $walletLink }} hasn't voted on proposal {{ .Proposal.ID }} on {{ .Chain.GetName }} +{{ .Proposal.Title }} Voting ends at: {{ .Proposal.GetProposalTime }} (in {{ .Proposal.GetTimeLeft }}) -{{ range .Chain.GetExplorerProposalsLinks .Proposal.ProposalID }}{{ SerializeLink .}} +{{ range .Chain.GetExplorerProposalsLinks .Proposal.ID }}{{ SerializeLink .}} {{ end }} Sent by cosmos-proposals-checker. \ No newline at end of file diff --git a/templates/telegram/proposals.html b/templates/telegram/proposals.html index f01f07d..9ea5edf 100644 --- a/templates/telegram/proposals.html +++ b/templates/telegram/proposals.html @@ -9,7 +9,7 @@ {{- end }} {{- range .ProposalVotes }} {{- $proposalLink := $chain.GetProposalLink .Proposal }} -Proposal #{{ .Proposal.ProposalID }}: {{ SerializeLink $proposalLink }} (voting ends in {{ .Proposal.GetTimeLeft }}) +Proposal #{{ .Proposal.ID }}: {{ SerializeLink $proposalLink }} (voting ends in {{ .Proposal.GetTimeLeft }}) {{- range $wallet, $vote := .Votes }} {{- $walletLink := $chain.GetWalletLink $vote.Wallet -}} {{- if $vote.IsError }} diff --git a/templates/telegram/revoted.html b/templates/telegram/revoted.html index 3e18eb3..fe12b39 100644 --- a/templates/telegram/revoted.html +++ b/templates/telegram/revoted.html @@ -1,11 +1,11 @@ {{- $walletLink := .Chain.GetWalletLink .Wallet -}} -↔️ Wallet {{ SerializeLink $walletLink }} has changed its vote on proposal {{ .Proposal.ProposalID }} on {{ .Chain.GetName }} -{{ .Proposal.Content.Title }} +↔️ Wallet {{ SerializeLink $walletLink }} has changed its vote on proposal {{ .Proposal.ID }} on {{ .Chain.GetName }} +{{ .Proposal.Title }} Vote: {{ .Vote.ResolveVote }} Old vote: {{ .OldVote.ResolveVote }} Voting ends at: {{ .Proposal.GetProposalTime }} (in {{ .Proposal.GetTimeLeft }}) -{{ range .Chain.GetExplorerProposalsLinks .Proposal.ProposalID }}{{ SerializeLink .}} +{{ range .Chain.GetExplorerProposalsLinks .Proposal.ID }}{{ SerializeLink .}} {{ end }} Sent by cosmos-proposals-checker. \ No newline at end of file diff --git a/templates/telegram/vote_query_error.html b/templates/telegram/vote_query_error.html index ffd6dc5..3d5927a 100644 --- a/templates/telegram/vote_query_error.html +++ b/templates/telegram/vote_query_error.html @@ -1,5 +1,5 @@ ❌ There was an error querying proposal on {{ .Chain.GetName }} -Proposal ID: {{ .Proposal.ProposalID }} +Proposal ID: {{ .Proposal.ID }} Error text: {{ .Error }} Sent by cosmos-proposals-checker. \ No newline at end of file diff --git a/templates/telegram/voted.html b/templates/telegram/voted.html index 3f2d1cf..9c3cfd7 100644 --- a/templates/telegram/voted.html +++ b/templates/telegram/voted.html @@ -1,10 +1,10 @@ {{- $walletLink := .Chain.GetWalletLink .Wallet -}} -✅ Wallet {{ SerializeLink $walletLink }} has voted on proposal {{ .Proposal.ProposalID }} on {{ .Chain.GetName }} -{{ .Proposal.Content.Title }} +✅ Wallet {{ SerializeLink $walletLink }} has voted on proposal {{ .Proposal.ID }} on {{ .Chain.GetName }} +{{ .Proposal.Title }} Vote: {{ .Vote.ResolveVote }} Voting ends at: {{ .Proposal.GetProposalTime }} (in {{ .Proposal.GetTimeLeft }}) -{{ range .Chain.GetExplorerProposalsLinks .Proposal.ProposalID }}{{ SerializeLink .}} +{{ range .Chain.GetExplorerProposalsLinks .Proposal.ID }}{{ SerializeLink .}} {{ end }} Sent by cosmos-proposals-checker. \ No newline at end of file