From b2a76dfb94cc66c0a7da02583e21923b6a51db2e Mon Sep 17 00:00:00 2001 From: Sergey <83376337+freak12techno@users.noreply.github.com> Date: Sat, 17 Feb 2024 04:27:41 +0300 Subject: [PATCH] feat: add LCD API support (#13) * feat: add LCD API support * chore: refactor chain type setting * chore: fixed linting --- README.md | 10 ++- cmd/tmtop.go | 3 +- pkg/config/config.go | 47 ++++++++++-- pkg/fetcher/cosmos_lcd.go | 96 ++++++++++++++++++++++++ pkg/fetcher/{cosmos.go => cosmos_rpc.go} | 16 ++-- pkg/fetcher/data_fetcher.go | 8 +- pkg/http/http.go | 42 +++++++++-- 7 files changed, 198 insertions(+), 24 deletions(-) create mode 100644 pkg/fetcher/cosmos_lcd.go rename pkg/fetcher/{cosmos.go => cosmos_rpc.go} (90%) diff --git a/README.md b/README.md index 43872aa..3889da6 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,19 @@ To run it for a sovereign chain that is not Cosmos-based (for example, Nomic), s (this will limit the app possibilities, as in, it won't display validators monikers, upgrades status etc.): ``` -/tmtop --rpc-host --chain-type tendermint +./tmtop --rpc-host --chain-type tendermint +``` + +If a chain is not Cosmos-based, but exposes a webserver that is compatible with LCD REST API of cosmos-sdk, +you can try running it this way to fetch data from LCD (the `--lcd-host` parameter is not used in other cases): +``` +./tmtop --rpc-host --chain-type cosmos-lcd --lcd-host ``` To run it for a Cosmos-based consumer chains (like Stride or Neutron), something like this should be enough: ``` -/tmtop --rpc-host --provider-rpc-host --consumer-chain-id +./tmtop --rpc-host --provider-rpc-host --consumer-chain-id ``` There are more parameters to tweak, for all the possible arguments, see `./tmtop --help`. diff --git a/cmd/tmtop.go b/cmd/tmtop.go index ff68145..5cb3fb2 100644 --- a/cmd/tmtop.go +++ b/cmd/tmtop.go @@ -39,11 +39,12 @@ func main() { rootCmd.PersistentFlags().DurationVar(&config.RefreshRate, "refresh-rate", time.Second, "Refresh rate") rootCmd.PersistentFlags().BoolVar(&config.Verbose, "verbose", false, "Display more debug logs") rootCmd.PersistentFlags().BoolVar(&config.DisableEmojis, "disable-emojis", false, "Disable emojis in output") - rootCmd.PersistentFlags().StringVar(&config.ChainType, "chain-type", "cosmos", "Chain type. Allowed values are: 'cosmos', 'tendermint'") + rootCmd.PersistentFlags().Var(&config.ChainType, "chain-type", "Chain type. Allowed values are: 'cosmos-rpc', 'cosmos-lcd', 'tendermint'") rootCmd.PersistentFlags().DurationVar(&config.ValidatorsRefreshRate, "validators-refresh-rate", time.Minute, "Validators refresh rate") rootCmd.PersistentFlags().DurationVar(&config.ChainInfoRefreshRate, "chain-info-refresh-rate", 5*time.Minute, "Chain info refresh rate") rootCmd.PersistentFlags().DurationVar(&config.UpgradeRefreshRate, "upgrade-refresh-rate", 30*time.Minute, "Upgrades refresh rate") rootCmd.PersistentFlags().DurationVar(&config.BlockTimeRefreshRate, "block-time-refresh-rate", 30*time.Second, "Block time refresh rate") + rootCmd.PersistentFlags().StringVar(&config.LCDHost, "lcd-host", "", "LCD API host URL") if err := rootCmd.Execute(); err != nil { logger.GetDefaultLogger().Fatal().Err(err).Msg("Could not start application") diff --git a/pkg/config/config.go b/pkg/config/config.go index 9832ea9..9377eab 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -3,10 +3,43 @@ package config import ( "errors" "fmt" - "slices" "time" ) +type ChainType string + +const ( + ChainTypeCosmosRPC ChainType = "cosmos-rpc" + ChainTypeCosmosLCD ChainType = "cosmos-lcd" + ChainTypeTendermint ChainType = "tendermint" +) + +func (t *ChainType) String() string { + return string(*t) +} + +func (t *ChainType) Set(v string) error { + switch v { + case "cosmos-rpc", "": + *t = ChainTypeCosmosRPC + return nil + case "cosmos-lcd": + *t = ChainTypeCosmosLCD + return nil + case "tendermint": + *t = ChainTypeTendermint + return nil + } + + return fmt.Errorf( + "expected chain-type to be one of 'cosmos-rpc', 'cosmos-lcd', 'tendermint', but got '%s'", + v, + ) +} +func (t *ChainType) Type() string { + return "ChainType" +} + type Config struct { RPCHost string ProviderRPCHost string @@ -16,9 +49,11 @@ type Config struct { ChainInfoRefreshRate time.Duration UpgradeRefreshRate time.Duration BlockTimeRefreshRate time.Duration - ChainType string + ChainType ChainType Verbose bool DisableEmojis bool + + LCDHost string } func (c Config) GetProviderOrConsumerHost() string { @@ -34,13 +69,13 @@ func (c Config) IsConsumer() bool { } func (c Config) Validate() error { - if !slices.Contains([]string{"cosmos", "tendermint"}, c.ChainType) { - return fmt.Errorf("expected chain-type to be one of 'cosmos', 'tendermint', but got '%s'", c.ChainType) - } - if c.IsConsumer() && c.ConsumerChainID == "" { return errors.New("chain is consumer, but consumer-chain-id is not set") } + if c.ChainType == ChainTypeCosmosLCD && c.LCDHost == "" { + return errors.New("chain-type is 'cosmos-lcd', but lcd-node is not set") + } + return nil } diff --git a/pkg/fetcher/cosmos_lcd.go b/pkg/fetcher/cosmos_lcd.go new file mode 100644 index 0000000..6b2a78a --- /dev/null +++ b/pkg/fetcher/cosmos_lcd.go @@ -0,0 +1,96 @@ +package fetcher + +import ( + "errors" + "fmt" + configPkg "main/pkg/config" + "main/pkg/http" + "main/pkg/types" + + "github.com/cosmos/cosmos-sdk/codec" + codecTypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/std" + stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" + upgradeTypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + "github.com/rs/zerolog" +) + +type CosmosLcdDataFetcher struct { + Config configPkg.Config + Logger zerolog.Logger + Client *http.Client + + Registry codecTypes.InterfaceRegistry + ParseCodec *codec.ProtoCodec + LegacyAmino *codec.LegacyAmino +} + +func NewCosmosLcdDataFetcher(config configPkg.Config, logger zerolog.Logger) *CosmosLcdDataFetcher { + interfaceRegistry := codecTypes.NewInterfaceRegistry() + std.RegisterInterfaces(interfaceRegistry) + parseCodec := codec.NewProtoCodec(interfaceRegistry) + + return &CosmosLcdDataFetcher{ + Config: config, + Logger: logger.With().Str("component", "cosmos_lcd_data_fetcher").Logger(), + Client: http.NewClient(logger, "cosmos_lcd_data_fetcher", config.LCDHost), + Registry: interfaceRegistry, + ParseCodec: parseCodec, + } +} + +func (f *CosmosLcdDataFetcher) GetValidators() (*types.ChainValidators, error) { + bytes, err := f.Client.GetPlain( + "/cosmos/staking/v1beta1/validators?status=BOND_STATUS_BONDED&pagination.limit=1000", + ) + + if err != nil { + return nil, err + } + + var validatorsResponse stakingTypes.QueryValidatorsResponse + + if err := f.ParseCodec.UnmarshalJSON(bytes, &validatorsResponse); err != nil { + return nil, err + } + + validators := make(types.ChainValidators, len(validatorsResponse.Validators)) + + for index, validator := range validatorsResponse.Validators { + if err := validator.UnpackInterfaces(f.ParseCodec); err != nil { + return nil, err + } + + addr, err := validator.GetConsAddr() + if err != nil { + return nil, err + } + + validators[index] = types.ChainValidator{ + Moniker: validator.Description.Moniker, + Address: fmt.Sprintf("%X", addr), + RawAddress: addr.String(), + } + } + + return &validators, nil +} + +func (f *CosmosLcdDataFetcher) GetUpgradePlan() (*types.Upgrade, error) { + var response upgradeTypes.QueryCurrentPlanResponse + if err := f.Client.Get( + "/cosmos/upgrade/v1beta1/current_plan", + &response, + ); err != nil { + return nil, err + } + + if response.Plan == nil { + return nil, errors.New("upgrade plan is not present") + } + + return &types.Upgrade{ + Name: response.Plan.Name, + Height: response.Plan.Height, + }, nil +} diff --git a/pkg/fetcher/cosmos.go b/pkg/fetcher/cosmos_rpc.go similarity index 90% rename from pkg/fetcher/cosmos.go rename to pkg/fetcher/cosmos_rpc.go index af4afc7..f0d1567 100644 --- a/pkg/fetcher/cosmos.go +++ b/pkg/fetcher/cosmos_rpc.go @@ -22,7 +22,7 @@ import ( providerTypes "github.com/cosmos/interchain-security/x/ccv/provider/types" ) -type CosmosDataFetcher struct { +type CosmosRPCDataFetcher struct { Config configPkg.Config Logger zerolog.Logger Client *http.Client @@ -32,12 +32,12 @@ type CosmosDataFetcher struct { ParseCodec *codec.ProtoCodec } -func NewCosmosDataFetcher(config configPkg.Config, logger zerolog.Logger) *CosmosDataFetcher { +func NewCosmosRPCDataFetcher(config configPkg.Config, logger zerolog.Logger) *CosmosRPCDataFetcher { interfaceRegistry := codecTypes.NewInterfaceRegistry() std.RegisterInterfaces(interfaceRegistry) parseCodec := codec.NewProtoCodec(interfaceRegistry) - return &CosmosDataFetcher{ + return &CosmosRPCDataFetcher{ Config: config, Logger: logger.With().Str("component", "cosmos_data_fetcher").Logger(), ProviderClient: http.NewClient(logger, "cosmos_data_fetcher", config.ProviderRPCHost), @@ -47,7 +47,7 @@ func NewCosmosDataFetcher(config configPkg.Config, logger zerolog.Logger) *Cosmo } } -func (f *CosmosDataFetcher) GetProviderOrConsumerClient() *http.Client { +func (f *CosmosRPCDataFetcher) GetProviderOrConsumerClient() *http.Client { if f.Config.ProviderRPCHost != "" { return f.ProviderClient } @@ -55,7 +55,7 @@ func (f *CosmosDataFetcher) GetProviderOrConsumerClient() *http.Client { return f.Client } -func (f *CosmosDataFetcher) GetValidatorAssignedConsumerKey( +func (f *CosmosRPCDataFetcher) GetValidatorAssignedConsumerKey( providerValcons string, ) (*providerTypes.QueryValidatorConsumerAddrResponse, error) { query := providerTypes.QueryValidatorConsumerAddrRequest{ @@ -76,7 +76,7 @@ func (f *CosmosDataFetcher) GetValidatorAssignedConsumerKey( return &response, nil } -func (f *CosmosDataFetcher) AbciQuery( +func (f *CosmosRPCDataFetcher) AbciQuery( method string, message codec.ProtoMarshaler, output codec.ProtoMarshaler, @@ -110,7 +110,7 @@ func (f *CosmosDataFetcher) AbciQuery( return output.Unmarshal(response.Result.Response.Value) } -func (f *CosmosDataFetcher) GetValidators() (*types.ChainValidators, error) { +func (f *CosmosRPCDataFetcher) GetValidators() (*types.ChainValidators, error) { query := stakingTypes.QueryValidatorsRequest{ Pagination: &queryTypes.PageRequest{ Limit: 1000, @@ -182,7 +182,7 @@ func (f *CosmosDataFetcher) GetValidators() (*types.ChainValidators, error) { return &validators, nil } -func (f *CosmosDataFetcher) GetUpgradePlan() (*types.Upgrade, error) { +func (f *CosmosRPCDataFetcher) GetUpgradePlan() (*types.Upgrade, error) { query := upgradeTypes.QueryCurrentPlanRequest{} var response upgradeTypes.QueryCurrentPlanResponse diff --git a/pkg/fetcher/data_fetcher.go b/pkg/fetcher/data_fetcher.go index 807b884..3f5bff4 100644 --- a/pkg/fetcher/data_fetcher.go +++ b/pkg/fetcher/data_fetcher.go @@ -13,8 +13,12 @@ type DataFetcher interface { } func GetDataFetcher(config configPkg.Config, logger zerolog.Logger) DataFetcher { - if config.ChainType == "cosmos" { - return NewCosmosDataFetcher(config, logger) + if config.ChainType == "cosmos-rpc" { + return NewCosmosRPCDataFetcher(config, logger) + } + + if config.ChainType == "cosmos-lcd" { + return NewCosmosLcdDataFetcher(config, logger) } return NewNoopDataFetcher() diff --git a/pkg/http/http.go b/pkg/http/http.go index ede39df..5ddc944 100644 --- a/pkg/http/http.go +++ b/pkg/http/http.go @@ -3,6 +3,7 @@ package http import ( "encoding/json" "fmt" + "io" "net/http" "time" @@ -24,7 +25,7 @@ func NewClient(logger zerolog.Logger, invoker, host string) *Client { } } -func (c *Client) Get(relativeURL string, target interface{}) error { +func (c *Client) GetInternal(relativeURL string) (io.ReadCloser, error) { client := &http.Client{Timeout: 300 * time.Second} start := time.Now() @@ -32,7 +33,7 @@ func (c *Client) Get(relativeURL string, target interface{}) error { req, err := http.NewRequest(http.MethodGet, fullURL, nil) if err != nil { - return err + return nil, err } req.Header.Set("User-Agent", "tmtop") @@ -42,11 +43,42 @@ func (c *Client) Get(relativeURL string, target interface{}) error { res, err := client.Do(req) if err != nil { c.Logger.Warn().Str("url", fullURL).Err(err).Msg("Query failed") - return err + return nil, err } - defer res.Body.Close() c.Logger.Debug().Str("url", fullURL).Dur("duration", time.Since(start)).Msg("Query is finished") - return json.NewDecoder(res.Body).Decode(target) + return res.Body, nil +} + +func (c *Client) Get(relativeURL string, target interface{}) error { + body, err := c.GetInternal(relativeURL) + if err != nil { + return err + } + + if err := json.NewDecoder(body).Decode(target); err != nil { + return err + } + + return body.Close() +} + +func (c *Client) GetPlain(relativeURL string) ([]byte, error) { + body, err := c.GetInternal(relativeURL) + + if err != nil { + return nil, err + } + + bytes, err := io.ReadAll(body) + if err != nil { + return nil, err + } + + if err := body.Close(); err != nil { + return nil, err + } + + return bytes, nil }