Skip to content

Commit

Permalink
feat: add LCD API support (#13)
Browse files Browse the repository at this point in the history
* feat: add LCD API support

* chore: refactor chain type setting

* chore: fixed linting
  • Loading branch information
freak12techno authored Feb 17, 2024
1 parent f3465cd commit b2a76df
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 24 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <RPC host address> --chain-type tendermint
./tmtop --rpc-host <RPC host address> --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 <RPC host address> --chain-type cosmos-lcd --lcd-host <LCD host address>
```

To run it for a Cosmos-based consumer chains (like Stride or Neutron),
something like this should be enough:
```
/tmtop --rpc-host <RPC host address> --provider-rpc-host <provider RPC host> --consumer-chain-id <consumer chain ID>
./tmtop --rpc-host <RPC host address> --provider-rpc-host <provider RPC host> --consumer-chain-id <consumer chain ID>
```

There are more parameters to tweak, for all the possible arguments, see `./tmtop --help`.
Expand Down
3 changes: 2 additions & 1 deletion cmd/tmtop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
47 changes: 41 additions & 6 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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
}
96 changes: 96 additions & 0 deletions pkg/fetcher/cosmos_lcd.go
Original file line number Diff line number Diff line change
@@ -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
}
16 changes: 8 additions & 8 deletions pkg/fetcher/cosmos.go → pkg/fetcher/cosmos_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand All @@ -47,15 +47,15 @@ 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
}

return f.Client
}

func (f *CosmosDataFetcher) GetValidatorAssignedConsumerKey(
func (f *CosmosRPCDataFetcher) GetValidatorAssignedConsumerKey(
providerValcons string,
) (*providerTypes.QueryValidatorConsumerAddrResponse, error) {
query := providerTypes.QueryValidatorConsumerAddrRequest{
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions pkg/fetcher/data_fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
42 changes: 37 additions & 5 deletions pkg/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package http
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"

Expand All @@ -24,15 +25,15 @@ 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()

fullURL := fmt.Sprintf("%s%s", c.Host, relativeURL)

req, err := http.NewRequest(http.MethodGet, fullURL, nil)
if err != nil {
return err
return nil, err
}

req.Header.Set("User-Agent", "tmtop")
Expand All @@ -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
}

0 comments on commit b2a76df

Please sign in to comment.