diff --git a/.gitignore b/.gitignore index 64da65e..d964413 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ main cover.out cosmos-proposals-checker dist +**/.DS_Store \ No newline at end of file diff --git a/pkg/events/proposals_query_error.go b/pkg/events/proposals_query_error.go index 5597c75..81ba897 100644 --- a/pkg/events/proposals_query_error.go +++ b/pkg/events/proposals_query_error.go @@ -6,7 +6,7 @@ import ( type ProposalsQueryErrorEvent struct { Chain *types.Chain - Error error + Error *types.QueryError } func (e ProposalsQueryErrorEvent) Name() string { diff --git a/pkg/events/vote_query_error.go b/pkg/events/vote_query_error.go index ec7a059..b623631 100644 --- a/pkg/events/vote_query_error.go +++ b/pkg/events/vote_query_error.go @@ -7,7 +7,7 @@ import ( type VoteQueryError struct { Chain *types.Chain Proposal types.Proposal - Error error + Error *types.QueryError } func (e VoteQueryError) Name() string { diff --git a/pkg/report/generator_test.go b/pkg/report/generator_test.go index cbc65c9..e381cd6 100644 --- a/pkg/report/generator_test.go +++ b/pkg/report/generator_test.go @@ -1,6 +1,7 @@ package report import ( + "errors" "main/pkg/events" "testing" @@ -20,7 +21,9 @@ func TestReportGeneratorWithProposalError(t *testing.T) { newState := state.State{ ChainInfos: map[string]*state.ChainInfo{ "chain": { - ProposalsError: types.NewJSONError("test error"), + ProposalsError: &types.QueryError{ + QueryError: errors.New("test error"), + }, }, }, } @@ -34,7 +37,7 @@ func TestReportGeneratorWithProposalError(t *testing.T) { entry, ok := report.Entries[0].(events.ProposalsQueryErrorEvent) assert.True(t, ok, "Expected to have a proposal query error!") - assert.Equal(t, "test error", entry.Error.Error(), "Error text mismatch!") + assert.Equal(t, "test error", entry.Error.QueryError.Error(), "Error text mismatch!") } func TestReportGeneratorWithVoteError(t *testing.T) { @@ -53,7 +56,9 @@ func TestReportGeneratorWithVoteError(t *testing.T) { }, Votes: map[string]state.ProposalVote{ "wallet": { - Error: types.NewJSONError("test error"), + Error: &types.QueryError{ + QueryError: errors.New("test error"), + }, }, }, }, diff --git a/pkg/state/generator.go b/pkg/state/generator.go index 154ff1e..bcc5d2b 100644 --- a/pkg/state/generator.go +++ b/pkg/state/generator.go @@ -124,7 +124,7 @@ func (g *Generator) ProcessProposalAndWallet( } if err != nil { - proposalVote.Error = types.NewJSONError(err.Error()) + proposalVote.Error = err } else { proposalVote.Vote = voteResponse.Vote } diff --git a/pkg/state/state.go b/pkg/state/state.go index 0e96bfd..91ba3c4 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -7,7 +7,7 @@ import ( type ProposalVote struct { Wallet *types.Wallet Vote *types.Vote - Error *types.JSONError + Error *types.QueryError } func (v ProposalVote) HasVoted() bool { @@ -26,7 +26,7 @@ type WalletVotes struct { type ChainInfo struct { Chain *types.Chain ProposalVotes map[string]WalletVotes - ProposalsError *types.JSONError + ProposalsError *types.QueryError } func (c ChainInfo) HasProposalsError() bool { @@ -61,10 +61,10 @@ func (s *State) SetVote(chain *types.Chain, proposal types.Proposal, wallet *typ s.ChainInfos[chain.Name].ProposalVotes[proposal.ID].Votes[wallet.Address] = vote } -func (s *State) SetChainProposalsError(chain *types.Chain, err error) { +func (s *State) SetChainProposalsError(chain *types.Chain, err *types.QueryError) { s.ChainInfos[chain.Name] = &ChainInfo{ Chain: chain, - ProposalsError: types.NewJSONError(err.Error()), + ProposalsError: err, } } diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index 4affc5f..9a4f0e4 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -37,13 +37,15 @@ func TestSetProposalErrorWithoutChainInfo(t *testing.T) { t.Parallel() state := NewState() - state.SetChainProposalsError(&types.Chain{Name: "test"}, errors.New("test error")) + state.SetChainProposalsError(&types.Chain{Name: "test"}, &types.QueryError{ + QueryError: errors.New("test error"), + }) hasError2 := state.ChainInfos["test"].HasProposalsError() assert.True(t, hasError2, "Chain info should have a proposal error!") err := state.ChainInfos["test"].ProposalsError - assert.Equal(t, "test error", err.Error(), "Errors text should match!") + assert.Equal(t, "test error", err.QueryError.Error(), "Errors text should match!") } func TestSetVotes(t *testing.T) { @@ -83,13 +85,15 @@ func TestSetProposalErrorWithChainInfo(t *testing.T) { hasError := state.ChainInfos["test"].HasProposalsError() assert.False(t, hasError, "Chain info should not have a proposal error!") - state.SetChainProposalsError(&types.Chain{Name: "test"}, errors.New("test error")) + state.SetChainProposalsError(&types.Chain{Name: "test"}, &types.QueryError{ + QueryError: errors.New("test error"), + }) hasError2 := state.ChainInfos["test"].HasProposalsError() assert.True(t, hasError2, "Chain info should have a proposal error!") err := state.ChainInfos["test"].ProposalsError - assert.Equal(t, "test error", err.Error(), "Errors text should match!") + assert.Equal(t, "test error", err.QueryError.Error(), "Errors text should match!") } func TestGetVoteWithoutChainInfo(t *testing.T) { diff --git a/pkg/tendermint/tendermint.go b/pkg/tendermint/tendermint.go index 9a09cc5..9c9341c 100644 --- a/pkg/tendermint/tendermint.go +++ b/pkg/tendermint/tendermint.go @@ -30,7 +30,7 @@ func NewRPC(chainConfig *types.Chain, logger zerolog.Logger) *RPC { } } -func (rpc *RPC) GetAllProposals() ([]types.Proposal, error) { +func (rpc *RPC) GetAllProposals() ([]types.Proposal, *types.QueryError) { if rpc.ProposalsType == "v1" { return rpc.GetAllV1Proposals() } @@ -38,7 +38,7 @@ func (rpc *RPC) GetAllProposals() ([]types.Proposal, error) { return rpc.GetAllV1beta1Proposals() } -func (rpc *RPC) GetAllV1beta1Proposals() ([]types.Proposal, error) { +func (rpc *RPC) GetAllV1beta1Proposals() ([]types.Proposal, *types.QueryError) { proposals := []types.Proposal{} offset := 0 @@ -51,12 +51,17 @@ func (rpc *RPC) GetAllV1beta1Proposals() ([]types.Proposal, error) { ) var batchProposals types.V1Beta1ProposalsRPCResponse - if err := rpc.Get(url, &batchProposals); err != nil { - return nil, err + if errs := rpc.Get(url, &batchProposals); len(errs) > 0 { + return nil, &types.QueryError{ + QueryError: nil, + NodeErrors: errs, + } } if batchProposals.Message != "" { - return nil, errors.New(batchProposals.Message) + return nil, &types.QueryError{ + QueryError: errors.New(batchProposals.Message), + } } parsedProposals := utils.Map(batchProposals.Proposals, func(p types.V1beta1Proposal) types.Proposal { @@ -73,7 +78,7 @@ func (rpc *RPC) GetAllV1beta1Proposals() ([]types.Proposal, error) { return proposals, nil } -func (rpc *RPC) GetAllV1Proposals() ([]types.Proposal, error) { +func (rpc *RPC) GetAllV1Proposals() ([]types.Proposal, *types.QueryError) { proposals := []types.Proposal{} offset := 0 @@ -86,12 +91,17 @@ func (rpc *RPC) GetAllV1Proposals() ([]types.Proposal, error) { ) var batchProposals types.V1ProposalsRPCResponse - if err := rpc.Get(url, &batchProposals); err != nil { - return nil, err + if errs := rpc.Get(url, &batchProposals); len(errs) > 0 { + return nil, &types.QueryError{ + QueryError: nil, + NodeErrors: errs, + } } if batchProposals.Message != "" { - return nil, errors.New(batchProposals.Message) + return nil, &types.QueryError{ + QueryError: errors.New(batchProposals.Message), + } } parsedProposals := utils.Map(batchProposals.Proposals, func(p types.V1Proposal) types.Proposal { @@ -108,7 +118,7 @@ func (rpc *RPC) GetAllV1Proposals() ([]types.Proposal, error) { return proposals, nil } -func (rpc *RPC) GetVote(proposal, voter string) (*types.VoteRPCResponse, error) { +func (rpc *RPC) GetVote(proposal, voter string) (*types.VoteRPCResponse, *types.QueryError) { url := fmt.Sprintf( "/cosmos/gov/v1beta1/proposals/%s/votes/%s", proposal, @@ -116,55 +126,69 @@ func (rpc *RPC) GetVote(proposal, voter string) (*types.VoteRPCResponse, error) ) var vote types.VoteRPCResponse - if err := rpc.Get(url, &vote); err != nil { - return nil, err + if errs := rpc.Get(url, &vote); len(errs) > 0 { + return nil, &types.QueryError{ + QueryError: nil, + NodeErrors: errs, + } } if vote.IsError() && !strings.Contains(vote.Message, "not found") { - return nil, errors.New(vote.Message) + return nil, &types.QueryError{ + QueryError: errors.New(vote.Message), + } } return &vote, nil } -func (rpc *RPC) GetTally(proposal string) (*types.TallyRPCResponse, error) { +func (rpc *RPC) GetTally(proposal string) (*types.TallyRPCResponse, *types.QueryError) { url := fmt.Sprintf( "/cosmos/gov/v1beta1/proposals/%s/tally", proposal, ) var tally types.TallyRPCResponse - if err := rpc.Get(url, &tally); err != nil { - return nil, err + if errs := rpc.Get(url, &tally); len(errs) > 0 { + return nil, &types.QueryError{ + QueryError: nil, + NodeErrors: errs, + } } return &tally, nil } -func (rpc *RPC) GetStakingPool() (*types.PoolRPCResponse, error) { +func (rpc *RPC) GetStakingPool() (*types.PoolRPCResponse, *types.QueryError) { url := "/cosmos/staking/v1beta1/pool" var pool types.PoolRPCResponse - if err := rpc.Get(url, &pool); err != nil { - return nil, err + if errs := rpc.Get(url, &pool); len(errs) > 0 { + return nil, &types.QueryError{ + QueryError: nil, + NodeErrors: errs, + } } return &pool, nil } -func (rpc *RPC) GetGovParams(paramsType string) (*types.ParamsResponse, error) { +func (rpc *RPC) GetGovParams(paramsType string) (*types.ParamsResponse, *types.QueryError) { url := fmt.Sprintf("/cosmos/gov/v1beta1/params/%s", paramsType) var pool types.ParamsResponse - if err := rpc.Get(url, &pool); err != nil { - return nil, err + if errs := rpc.Get(url, &pool); len(errs) > 0 { + return nil, &types.QueryError{ + QueryError: nil, + NodeErrors: errs, + } } return &pool, nil } -func (rpc *RPC) Get(url string, target interface{}) error { - nodeErrors := make([]error, len(rpc.URLs)) +func (rpc *RPC) Get(url string, target interface{}) []types.NodeError { + nodeErrors := make([]types.NodeError, len(rpc.URLs)) for index, lcd := range rpc.URLs { fullURL := lcd + url @@ -180,19 +204,14 @@ func (rpc *RPC) Get(url string, target interface{}) error { } rpc.Logger.Warn().Str("url", fullURL).Err(err).Msg("LCD request failed") - nodeErrors[index] = err + nodeErrors[index] = types.NodeError{ + Node: lcd, + Error: types.NewJSONError(err), + } } rpc.Logger.Warn().Str("url", url).Msg("All LCD requests failed") - - var sb strings.Builder - - sb.WriteString("All LCD requests failed:\n") - for index, url := range rpc.URLs { - sb.WriteString(fmt.Sprintf("#%d: %s -> %s\n", index+1, url, nodeErrors[index])) - } - - return fmt.Errorf(sb.String()) + return nodeErrors } func (rpc *RPC) GetFull(url string, target interface{}) error { diff --git a/pkg/types/chain.go b/pkg/types/chain.go index 226451f..9a3b73c 100644 --- a/pkg/types/chain.go +++ b/pkg/types/chain.go @@ -34,7 +34,7 @@ type Chain struct { Explorer *Explorer `toml:"explorer"` } -func (c *Chain) Validate() error { +func (c Chain) Validate() error { if c.Name == "" { return fmt.Errorf("empty chain name") } diff --git a/pkg/types/error.go b/pkg/types/error.go index 2196b49..beb2c8b 100644 --- a/pkg/types/error.go +++ b/pkg/types/error.go @@ -1,13 +1,17 @@ package types -import "encoding/json" +import ( + "encoding/json" + "fmt" + "strings" +) type JSONError struct { error string } -func NewJSONError(err string) *JSONError { - return &JSONError{error: err} +func NewJSONError(err error) JSONError { + return JSONError{error: err.Error()} } func (e *JSONError) Error() string { @@ -22,3 +26,28 @@ func (e *JSONError) UnmarshalJSON(data []byte) error { e.error = string(data) return nil } + +type NodeError struct { + Node string + Error JSONError +} + +type QueryError struct { + QueryError error + NodeErrors []NodeError +} + +func (q QueryError) Error() string { + if q.QueryError != nil { + return q.QueryError.Error() + } + + var sb strings.Builder + + sb.WriteString("All LCD requests failed:\n") + for index, nodeError := range q.NodeErrors { + sb.WriteString(fmt.Sprintf("#%d: %s -> %s\n", index+1, nodeError.Node, nodeError.Error.error)) + } + + return sb.String() +} diff --git a/pkg/types/error_test.go b/pkg/types/error_test.go new file mode 100644 index 0000000..5cedd30 --- /dev/null +++ b/pkg/types/error_test.go @@ -0,0 +1,38 @@ +package types + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestQueryErrorSerializeWithQueryError(t *testing.T) { + t.Parallel() + + queryError := QueryError{ + QueryError: errors.New("test error"), + } + + serializedError := queryError.Error() + assert.Equal(t, "test error", serializedError, "Error mismatch!") +} + +func TestQueryErrorSerializeWithoutQueryError(t *testing.T) { + t.Parallel() + + queryError := QueryError{ + NodeErrors: []NodeError{ + {Node: "test", Error: NewJSONError(errors.New("test error"))}, + {Node: "test2", Error: NewJSONError(errors.New("test error2"))}, + }, + } + + serializedError := queryError.Error() + assert.Equal( + t, + "All LCD requests failed:\n#1: test -> test error\n#2: test2 -> test error2\n", + serializedError, + "Error mismatch!", + ) +}