diff --git a/td2/alert.go b/td2/alert.go index 3b18846..74e23ee 100644 --- a/td2/alert.go +++ b/td2/alert.go @@ -950,6 +950,19 @@ func evaluateUnclaimedRewardsAlert(cc *ChainConfig) (bool, bool) { } } + // fallback to cosmos.directory chain data if available + if cc.denomMetadata == nil && cc.hasCosmosDirectoryData() { + if bankMeta := cc.getBankMetadataFromCosmosDirectory(targetDenom); bankMeta != nil { + cc.denomMetadata = bankMeta + addDenom(cc.denomMetadata.Base) + addDenom(cc.denomMetadata.Display) + targetDenom = cc.denomMetadata.Display + if targetDenom == "" { + targetDenom = cc.denomMetadata.Base + } + } + } + // when `cc.denomMetadata` is nil, we try to infer the denom from the rewards if len(targetDenoms) == 0 { if selfRewardsLen > 0 { @@ -1011,6 +1024,21 @@ func evaluateUnclaimedRewardsAlert(cc *ChainConfig) (bool, bool) { return alert, resolved } + if err := github_com_cosmos_cosmos_sdk_types.ValidateDenom(targetDenom); err != nil { + fallback := "" + if len(nativeCoins) > 0 { + if errFallback := github_com_cosmos_cosmos_sdk_types.ValidateDenom(nativeCoins[0].Denom); errFallback == nil { + fallback = nativeCoins[0].Denom + } + } + if fallback == "" { + l(fmt.Errorf("invalid target denom %q for %s: %w", targetDenom, cc.name, err)) + return alert, resolved + } + l(fmt.Errorf("invalid target denom %q for %s: %w; falling back to %q", targetDenom, cc.name, err, fallback)) + targetDenom = fallback + } + totalRewards := github_com_cosmos_cosmos_sdk_types.NewDecCoinFromDec(targetDenom, totalAmount) coinPrice, err := td.coinMarketCapClient.GetPrice(td.ctx, cc.Slug) diff --git a/td2/chain-details.go b/td2/chain-details.go index da9bd5f..bfd8196 100644 --- a/td2/chain-details.go +++ b/td2/chain-details.go @@ -177,7 +177,8 @@ type CosmosDirectoryChainData struct { Path string `json:"path"` ChainName string `json:"chain_name"` Symbol string `json:"symbol"` - Decimals int `json:"decimals"` + Display string `json:"display"` + Decimals uint32 `json:"decimals"` Denom string `json:"denom"` Params CDParams `json:"params"` Assets []CDAsset `json:"assets"` @@ -387,7 +388,35 @@ func (cc *ChainConfig) getChainInfoFromCosmosDirectory() (communityTax float64, func (cc *ChainConfig) getBankMetadataFromCosmosDirectory(denom string) *bank.Metadata { cdAsset := cc.getDenomMetadataFromCosmosDirectory(denom) if cdAsset == nil { - return nil + if cc.cosmosDirectoryData == nil || cc.cosmosDirectoryData.Denom == "" { + return nil + } + display := cc.cosmosDirectoryData.Display + if display == "" { + display = cc.cosmosDirectoryData.Symbol + } + + denomUnits := []*bank.DenomUnit{ + { + Denom: cc.cosmosDirectoryData.Denom, + Exponent: 0, + }, + } + if display != "" && display != cc.cosmosDirectoryData.Denom { + denomUnits = append(denomUnits, &bank.DenomUnit{ + Denom: display, + Exponent: cc.cosmosDirectoryData.Decimals, + }) + } + + return &bank.Metadata{ + Description: "", + DenomUnits: denomUnits, + Base: cc.cosmosDirectoryData.Denom, + Display: display, + Name: cc.cosmosDirectoryData.ChainName, + Symbol: cc.cosmosDirectoryData.Symbol, + } } // Convert CDDenomUnit to bank.DenomUnit diff --git a/td2/provider-default.go b/td2/provider-default.go index bbc60b1..dc7f55a 100644 --- a/td2/provider-default.go +++ b/td2/provider-default.go @@ -362,6 +362,27 @@ func (d *DefaultProvider) QuerySlashingParams(ctx context.Context) (*slashing.Pa return ¶ms.Params, nil } +func (d *DefaultProvider) QueryStakingParams(ctx context.Context) (*staking.Params, error) { + qParams := &staking.QueryParamsRequest{} + b, err := qParams.Marshal() + if err != nil { + return nil, fmt.Errorf("marshal staking params: %w", err) + } + resp, err := d.ChainConfig.client.ABCIQuery(ctx, "/cosmos.staking.v1beta1.Query/Params", b) + if err != nil { + return nil, fmt.Errorf("query staking params: %w", err) + } + if resp.Response.Value == nil { + return nil, errors.New("🛑 could not query staking params, got empty response") + } + params := &staking.QueryParamsResponse{} + err = params.Unmarshal(resp.Response.Value) + if err != nil { + return nil, fmt.Errorf("unmarshal staking params: %w", err) + } + return ¶ms.Params, nil +} + func (d *DefaultProvider) QueryChainInfo(ctx context.Context) (totalSupply float64, communityTax float64, inflationRate float64, err error) { // Query total supply using bank module supplyQueryParams := bank.QuerySupplyOfRequest{ diff --git a/td2/provider-namada.go b/td2/provider-namada.go index b869a8a..12132cb 100644 --- a/td2/provider-namada.go +++ b/td2/provider-namada.go @@ -295,6 +295,10 @@ func (d *NamadaProvider) QuerySlashingParams(ctx context.Context) (*slashing.Par return &slashing.Params{SignedBlocksWindow: int64(livenessInfo.LivenessWindowLen), MinSignedPerWindow: cosmos_sdk_types.MustNewDecFromStr(livenessInfo.LivenessThreshold.String())}, nil } +func (d *NamadaProvider) QueryStakingParams(ctx context.Context) (*staking.Params, error) { + return nil, errors.New("QueryStakingParams with ABCIQuery not implemented for Namada") +} + func (d *NamadaProvider) QueryDenomMetadata(ctx context.Context, denom string) (medatada *bank.Metadata, err error) { return nil, errors.New("QueryDenomMetadata with ABCIQuery not implemented for Namada") } diff --git a/td2/types.go b/td2/types.go index d81aa38..270d985 100644 --- a/td2/types.go +++ b/td2/types.go @@ -754,6 +754,7 @@ type ChainProvider interface { QueryValidatorInfo(ctx context.Context) (pub []byte, moniker string, jailed bool, bonded bool, delegatedTokens float64, commissionRate float64, err error) QuerySigningInfo(ctx context.Context) (*slashing.ValidatorSigningInfo, error) QuerySlashingParams(ctx context.Context) (*slashing.Params, error) + QueryStakingParams(ctx context.Context) (*staking.Params, error) QueryValidatorVotingPool(ctx context.Context) (votingPool *staking.Pool, err error) QueryValidatorSelfDelegationRewardsAndCommission(ctx context.Context) (rewards *github_com_cosmos_cosmos_sdk_types.DecCoins, commission *github_com_cosmos_cosmos_sdk_types.DecCoins, err error) QueryDenomMetadata(ctx context.Context, denom string) (medatada *bank.Metadata, err error) diff --git a/td2/utils/price-conversion.go b/td2/utils/price-conversion.go index 3f5b309..8149b00 100644 --- a/td2/utils/price-conversion.go +++ b/td2/utils/price-conversion.go @@ -239,6 +239,9 @@ func ConvertDecCoinToDisplayUnit(coins []github_com_cosmos_cosmos_sdk_types.DecC } else { displayDenom = metadata.Display } + if err := github_com_cosmos_cosmos_sdk_types.ValidateDenom(displayDenom); err != nil { + return nil, fmt.Errorf("invalid display denom %q for base %q: %w", displayDenom, metadata.Base, err) + } // Find the exponent for the display denom foundDisplayDenom := false @@ -325,11 +328,17 @@ func ConvertFloatInBaseUnitToDisplayUnit(value float64, metadata bank.Metadata) // If no display is set, default to base if metadata.Display == "" { displayDenom = metadata.Base + if err := github_com_cosmos_cosmos_sdk_types.ValidateDenom(displayDenom); err != nil { + return 0, "", fmt.Errorf("invalid display denom %q for base %q: %w", displayDenom, metadata.Base, err) + } // If display is base, no conversion needed return value, displayDenom, nil } else { displayDenom = metadata.Display } + if err := github_com_cosmos_cosmos_sdk_types.ValidateDenom(displayDenom); err != nil { + return 0, "", fmt.Errorf("invalid display denom %q for base %q: %w", displayDenom, metadata.Base, err) + } // Find the exponent for the display denom foundDisplayDenom := false diff --git a/td2/validator.go b/td2/validator.go index c5a4752..9ebde59 100644 --- a/td2/validator.go +++ b/td2/validator.go @@ -196,26 +196,48 @@ func (cc *ChainConfig) GetValInfo(first bool) (err error) { rewards, commission, err := provider.QueryValidatorSelfDelegationRewardsAndCommission(ctx) if err == nil { // query the chain's denom metadata, only query once since this does not change - if first && rewards != nil && len(*rewards) > 0 { - bankMeta, err := provider.QueryDenomMetadata(ctx, (*rewards)[0].Denom) + if first { + bondDenom := "" + stakingParams, err := provider.QueryStakingParams(ctx) if err == nil { - cc.denomMetadata = bankMeta + bondDenom = stakingParams.BondDenom } else { - l(fmt.Errorf("cannot query bank metadata for chain %s via ABCI, err: %w, trying cosmos.directory fallback", cc.name, err)) - // Try cosmos.directory fallback first - bankMeta = cc.getBankMetadataFromCosmosDirectory((*rewards)[0].Denom) - if bankMeta != nil { + l(fmt.Errorf("cannot query staking params for chain %s via ABCI, err: %w", cc.name, err)) + } + if bondDenom == "" && cc.hasCosmosDirectoryData() { + if cc.cosmosDirectoryData.Params.Staking.BondDenom != "" { + bondDenom = cc.cosmosDirectoryData.Params.Staking.BondDenom + } else if cc.cosmosDirectoryData.Denom != "" { + bondDenom = cc.cosmosDirectoryData.Denom + } + } + if bondDenom == "" && rewards != nil && len(*rewards) > 0 { + bondDenom = (*rewards)[0].Denom + } + + if bondDenom != "" { + bankMeta, err := provider.QueryDenomMetadata(ctx, bondDenom) + if err == nil { cc.denomMetadata = bankMeta - l(fmt.Sprintf("✅ loaded bank metadata for chain %s from cosmos.directory", cc.name)) } else { - l(fmt.Sprintf("â„šī¸ cosmos.directory bank metadata not available for chain %s, trying GitHub fallback", cc.name)) - bankMeta, err = cc.fetchBankMetadataFromGitHub() - if err == nil { + l(fmt.Errorf("cannot query bank metadata for chain %s via ABCI, err: %w, trying cosmos.directory fallback", cc.name, err)) + // Try cosmos.directory fallback (assets first, then chain data) + bankMeta = cc.getBankMetadataFromCosmosDirectory(bondDenom) + if bankMeta != nil { cc.denomMetadata = bankMeta + l(fmt.Sprintf("✅ loaded bank metadata for chain %s from cosmos.directory", cc.name)) } else { - l(fmt.Errorf("cannot find bank metadata for chain %s in the GitHub JSON file, err: %w", cc.name, err)) + l(fmt.Sprintf("â„šī¸ cosmos.directory bank metadata not available for chain %s, trying GitHub fallback", cc.name)) + bankMeta, err = cc.fetchBankMetadataFromGitHub() + if err == nil { + cc.denomMetadata = bankMeta + } else { + l(fmt.Errorf("cannot find bank metadata for chain %s in the GitHub JSON file, err: %w", cc.name, err)) + } } } + } else { + l(fmt.Sprintf("âš ī¸ could not determine bond denom for chain %s, skipping denom metadata query", cc.name)) } }