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

Various Fixes & Features #24

Merged
merged 3 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions arrays/batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ import (
"github.com/tessellated-io/pickaxe/arrays"
)

func TestBatch_OneSizeBatch(t *testing.T) {
arr := []int{1, 2}
batched := arrays.Batch(arr, 1)

require.Equal(t, len(batched), 2)

require.Equal(t, len(batched[0]), 1)
require.Equal(t, len(batched[1]), 1)

require.Equal(t, batched[0][0], 1)
require.Equal(t, batched[1][0], 2)
}

func TestBatch_EvenBatches(t *testing.T) {
arr := []int{1, 2, 3, 4}
batched := arrays.Batch(arr, 2)
Expand Down
91 changes: 91 additions & 0 deletions cosmos/chain-registry/asset_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package registry

import (
"encoding/json"
"math"
"strings"

sdk "github.com/cosmos/cosmos-sdk/types"
)

type AssetList struct {
Schema string `json:"$schema"`
ChainName string `json:"chain_name"`
Assets []Asset `json:"assets"`
}

type Asset struct {
Description string `json:"description"`
DenomUnits []DenomUnit `json:"denom_units"`
Base string `json:"base"`
Name string `json:"name"`
Display string `json:"display"`
Symbol string `json:"symbol"`
LogoURIs LogoURIs `json:"logo_URIs"`
CoingeckoID string `json:"coingecko_id"`
Images []ImageLinks `json:"images"`
}

type DenomUnit struct {
Denom string `json:"denom"`
Exponent int `json:"exponent"`
}

type ImageLinks struct {
Png string `json:"png"`
Svg string `json:"svg"`
}

func parseAssetListResponse(assetListBytes []byte) (*AssetList, error) {
// Unmarshal JSON data
var assetList AssetList
if err := json.Unmarshal(assetListBytes, &assetList); err != nil {
return nil, err
}
return &assetList, nil
}

// Convenience methods

func (al *AssetList) ExtractAssetByBaseSymbol(baseAssetSymbol string) (*Asset, error) {
assets := al.Assets
for _, asset := range assets {
if strings.EqualFold(asset.Base, baseAssetSymbol) {
return &asset, nil
}
}
return nil, ErrNoMatchingAsset
}

func (a *Asset) ExtractDenomByUnit(needleDenomUnit string) (*DenomUnit, error) {
denomUnits := a.DenomUnits
for _, denomUnit := range denomUnits {
if strings.EqualFold(denomUnit.Denom, needleDenomUnit) {
return &denomUnit, nil
}
}
return nil, ErrNoMatchingDenom
}

// Get a single token, given a base symbol. Ex. 1 JUNO = sdk.Coin{ denom: "ujuno", amount: "1_000_000" }
func (al *AssetList) OneToken(baseAssetSymbol string) (*sdk.Coin, error) {
// Extract the asset
asset, err := al.ExtractAssetByBaseSymbol(baseAssetSymbol)
if err != nil {
return nil, err
}

// Extract the denom unit
denomUnit, err := asset.ExtractDenomByUnit(baseAssetSymbol)
if err != nil {
return nil, err
}

decimals := denomUnit.Exponent

// Create one coin.
oneAmount := sdk.NewInt(int64(math.Pow(10, float64(decimals))))
oneCoin := sdk.NewCoin(baseAssetSymbol, oneAmount)

return &oneCoin, nil
}
31 changes: 0 additions & 31 deletions cosmos/chain-registry/chain_info.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
package registry

import (
"context"
"encoding/json"
"fmt"
"math"

"github.com/tessellated-io/pickaxe/cosmos/rpc"

sdk "github.com/cosmos/cosmos-sdk/types"
)

func parseChainResponse(responseBytes []byte) (*ChainInfo, error) {
Expand All @@ -32,30 +25,6 @@ func (ci *ChainInfo) FeeDenom() (string, error) {
return feeToken.Denom, nil
}

func (ci *ChainInfo) OneFeeToken(ctx context.Context, rpcClient rpc.RpcClient) (*sdk.Coin, error) {
feeDenom, err := ci.FeeDenom()
if err != nil {
return nil, err
}

// Fetch denom metadata from the registry to get decimals. It's a bummer chain registry doesn't seem to include this field.
denomMetadata, err := rpcClient.GetDenomMetadata(ctx, feeDenom)
if err != nil {
return nil, err
}
denomUnits := denomMetadata.DenomUnits
if len(denomUnits) == 0 {
return nil, fmt.Errorf("no denom unit found for %s", feeDenom)
}
decimals := denomUnits[0].Exponent

// Create one coin.
oneAmount := sdk.NewInt(int64(math.Pow(10, float64(decimals))))
oneCoin := sdk.NewCoin(feeDenom, oneAmount)

return &oneCoin, nil
}

func (ci *ChainInfo) StakingDenom() (string, error) {
stakingTokens := ci.Staking.StakingTokens
if len(stakingTokens) == 0 {
Expand Down
1 change: 1 addition & 0 deletions cosmos/chain-registry/chain_registry_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type ChainRegistryClient interface {
AllChainNames(ctx context.Context) ([]string, error)
ChainNameForChainID(ctx context.Context, targetChainID string, refreshCache bool) (string, error)
ChainInfo(ctx context.Context, chainName string) (*ChainInfo, error)
AssetList(ctx context.Context, chainName string) (*AssetList, error)

// Restake Validator Registry
Validator(ctx context.Context, targetValidator string) (*Validator, error)
Expand Down
2 changes: 2 additions & 0 deletions cosmos/chain-registry/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ var (
ErrNoChainFoundForChainID = errors.New("no chain found for chain ID")
ErrNoStakingTokenFound = errors.New("no staking tokens found in registry")
ErrNoFeeTokenFound = errors.New("no fee tokens found in registry")
ErrNoMatchingAsset = errors.New("no matching asset found")
ErrNoMatchingDenom = errors.New("no matching denom found")
)
23 changes: 21 additions & 2 deletions cosmos/chain-registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func NewChainRegistryClient(log *log.Logger) *chainRegistryClient {

func (rc *chainRegistryClient) ChainInfo(ctx context.Context, chainName string) (*ChainInfo, error) {
url := fmt.Sprintf("https://proxy.atomscan.com/directory/%s/chain.json", chainName)

bytes, err := rc.makeRequest(ctx, url)
if err != nil {
return nil, err
Expand All @@ -51,6 +52,21 @@ func (rc *chainRegistryClient) ChainInfo(ctx context.Context, chainName string)
return chainInfo, nil
}

func (rc *chainRegistryClient) AssetList(ctx context.Context, chainName string) (*AssetList, error) {
url := fmt.Sprintf("https://proxy.atomscan.com/directory/%s/assetlist.json", chainName)

bytes, err := rc.makeRequest(ctx, url)
if err != nil {
return nil, err
}

chainInfo, err := parseAssetListResponse(bytes)
if err != nil {
return nil, err
}
return chainInfo, nil
}

func (rc *chainRegistryClient) ChainNameForChainID(ctx context.Context, targetChainID string, refreshCache bool) (string, error) {
// If refresh cache is requested, clear the local values
if refreshCache {
Expand Down Expand Up @@ -84,13 +100,14 @@ func (rc *chainRegistryClient) ChainNameForChainID(ctx context.Context, targetCh
// NOTE: No retries because GetChainInfo manages that for us.
chainInfo, err := rc.ChainInfo(ctx, chainName)
if err != nil {
rc.log.Error().Err(err).Str("chain_name", chainName).Msg("error fetching chain information during chain id refresh")
return "", err
}

chainID = chainInfo.ChainID
}

if strings.EqualFold(chainName, chainID) {
if strings.EqualFold(targetChainID, chainID) {
return chainName, nil
}
}
Expand All @@ -100,7 +117,7 @@ func (rc *chainRegistryClient) ChainNameForChainID(ctx context.Context, targetCh

func (rc *chainRegistryClient) AllChainNames(ctx context.Context) ([]string, error) {
// Get all chain names
url := "https://cosmos-chain.directory/chains"
url := "https://cosmoschains.thesilverfox.pro/api/v1/mainnet"
bytes, err := rc.makeRequest(ctx, url)
if err != nil {
return nil, err
Expand Down Expand Up @@ -143,6 +160,8 @@ func (rc *chainRegistryClient) Validators(ctx context.Context) ([]Validator, err
// Private helpers

func (rc *chainRegistryClient) makeRequest(ctx context.Context, url string) ([]byte, error) {
rc.log.Debug().Str("url", url).Msg("making GET request to url")

request, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
Expand Down
87 changes: 78 additions & 9 deletions cosmos/chain-registry/retryable_chain_registry_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

retry "github.com/avast/retry-go/v4"
"github.com/tessellated-io/pickaxe/log"
)

// Implements a retryable and returns the last error
Expand All @@ -14,18 +15,22 @@ type retryableChainRegistryClient struct {

attempts retry.Option
delay retry.Option

logger *log.Logger
}

// Ensure that retryableChainRegistryClient implements ChainRegistryClient
var _ ChainRegistryClient = (*retryableChainRegistryClient)(nil)

// NewRetryableChainRegistryClient returns a new retryableChainRegistryClient
func NewRetryableChainRegistryClient(attempts uint, delay time.Duration, chainRegistryClient ChainRegistryClient) (ChainRegistryClient, error) {
func NewRetryableChainRegistryClient(attempts uint, delay time.Duration, chainRegistryClient ChainRegistryClient, logger *log.Logger) (ChainRegistryClient, error) {
return &retryableChainRegistryClient{
wrappedClient: chainRegistryClient,

attempts: retry.Attempts(attempts),
delay: retry.Delay(delay),

logger: logger,
}, nil
}

Expand All @@ -37,13 +42,22 @@ func (r *retryableChainRegistryClient) AllChainNames(ctx context.Context) ([]str

err = retry.Do(func() error {
result, err = r.wrappedClient.AllChainNames(ctx)
if err != nil {
r.logger.Error().Err(err).Str("method", "all_chain_names").Msg("failed call in registry client, will retry")
}
return err
}, r.delay, r.attempts, retry.Context(ctx))
if err != nil {
err = errors.Unwrap(err)
// If err is an error from a context, unwrapping will write out nil
unwrappedErr := errors.Unwrap(err)
if unwrappedErr != nil {
return result, unwrappedErr
} else {
return result, err
}
}

return result, err
return result, nil
}

func (r *retryableChainRegistryClient) ChainNameForChainID(ctx context.Context, targetChainID string, refreshCache bool) (string, error) {
Expand All @@ -52,13 +66,23 @@ func (r *retryableChainRegistryClient) ChainNameForChainID(ctx context.Context,

err = retry.Do(func() error {
result, err = r.wrappedClient.ChainNameForChainID(ctx, targetChainID, refreshCache)
if err != nil {
r.logger.Error().Err(err).Str("method", "chain_name_for_id").Msg("failed call in rpc client, will retry")
}
return err
}, r.delay, r.attempts, retry.Context(ctx))

if err != nil {
err = errors.Unwrap(err)
// If err is an error from a context, unwrapping will write out nil
unwrappedErr := errors.Unwrap(err)
if unwrappedErr != nil {
return result, unwrappedErr
} else {
return result, err
}
}

return result, err
return result, nil
}

func (r *retryableChainRegistryClient) ChainInfo(ctx context.Context, chainName string) (*ChainInfo, error) {
Expand All @@ -67,13 +91,48 @@ func (r *retryableChainRegistryClient) ChainInfo(ctx context.Context, chainName

err = retry.Do(func() error {
result, err = r.wrappedClient.ChainInfo(ctx, chainName)
if err != nil {
r.logger.Error().Err(err).Str("method", "chain_info").Msg("failed call in rpc client, will retry")
}
return err
}, r.delay, r.attempts, retry.Context(ctx))

if err != nil {
// If err is an error from a context, unwrapping will write out nil
unwrappedErr := errors.Unwrap(err)
if unwrappedErr != nil {
return result, unwrappedErr
} else {
return result, err
}
}

return result, nil
}

func (r *retryableChainRegistryClient) AssetList(ctx context.Context, chainName string) (*AssetList, error) {
var result *AssetList
var err error

err = retry.Do(func() error {
result, err = r.wrappedClient.AssetList(ctx, chainName)
if err != nil {
r.logger.Error().Err(err).Str("method", "asset_list").Msg("failed call in rpc client, will retry")
}
return err
}, r.delay, r.attempts, retry.Context(ctx))

if err != nil {
err = errors.Unwrap(err)
// If err is an error from a context, unwrapping will write out nil
unwrappedErr := errors.Unwrap(err)
if unwrappedErr != nil {
return result, unwrappedErr
} else {
return result, err
}
}

return result, err
return result, nil
}

func (r *retryableChainRegistryClient) Validator(ctx context.Context, targetValidator string) (*Validator, error) {
Expand All @@ -82,11 +141,21 @@ func (r *retryableChainRegistryClient) Validator(ctx context.Context, targetVali

err = retry.Do(func() error {
result, err = r.wrappedClient.Validator(ctx, targetValidator)
if err != nil {
r.logger.Error().Err(err).Str("method", "validator").Msg("failed call in rpc client, will retry")
}
return err
}, r.delay, r.attempts, retry.Context(ctx))

if err != nil {
err = errors.Unwrap(err)
// If err is an error from a context, unwrapping will write out nil
unwrappedErr := errors.Unwrap(err)
if unwrappedErr != nil {
return result, unwrappedErr
} else {
return result, err
}
}

return result, err
return result, nil
}
Loading