From d5bdb2c28a9332c8cc54faddd39147cb9a0f824e Mon Sep 17 00:00:00 2001 From: Mohammed Hussan Date: Fri, 25 Oct 2024 15:31:17 +0100 Subject: [PATCH] feat(warpMonitor): Support collateral value monitoring (#4545) ### Description - Support continuous value monitoring by allowing promethues to scrape value metrics for native and collateral tokens - Uses the `CoinGeckoTokenPriceGetter` - Added `getTokenPriceByIds` helper method to the `CoinGeckoTokenPriceGetter`, to support fetching prices using the coin gecko id only - Add a warp_route_id label to metrics ### Testing Manual --- .../mainnet3/warp/EZETH-deployments.yaml | 1 + .../warp/ancient8-USDC-deployments.yaml | 1 + .../warp/bsc-lumia-LUMIA-deployments.yaml | 1 + .../warp/eclipse-SOL-deployments.yaml | 1 + .../warp/eclipse-USDC-deployments.yaml | 2 + .../warp/eclipse-WIF-deployments.yaml | 1 + .../warp/ethereumUSDC-inevm-deployments.yaml | 1 + .../warp/ethereumUSDT-inevm-deployments.yaml | 1 + .../warp/injective-inevm-deployments.yaml | 1 + .../neutron-mantapacific-deployments.yaml | 1 + .../warp/sei-FASTUSD-deployments.yaml | 1 + .../viction-ethereum-ETH-deployments.yaml | 1 + .../viction-ethereum-USDC-deployments.yaml | 1 + .../viction-ethereum-USDT-deployments.yaml | 1 + typescript/infra/package.json | 2 +- .../monitor-warp-routes-balances.ts | 283 ++++++++++++++---- typescript/sdk/src/gas/token-prices.ts | 93 +++--- .../sdk/src/metadata/warpRouteConfig.ts | 1 + yarn.lock | 12 +- 19 files changed, 316 insertions(+), 90 deletions(-) diff --git a/typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml index b83663645..26a0a5807 100644 --- a/typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml @@ -10,6 +10,7 @@ data: symbol: ezETH hypAddress: '0xC59336D8edDa9722B4f1Ec104007191Ec16f7087' tokenAddress: '0xbf5495Efe5DB9ce00f80364C8B423567e58d2110' + tokenCoinGeckoId: renzo-restaked-eth decimals: 18 bsc: protocolType: ethereum diff --git a/typescript/infra/config/environments/mainnet3/warp/ancient8-USDC-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/ancient8-USDC-deployments.yaml index 07be06027..d87b3fa0a 100644 --- a/typescript/infra/config/environments/mainnet3/warp/ancient8-USDC-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/ancient8-USDC-deployments.yaml @@ -10,6 +10,7 @@ data: type: collateral hypAddress: '0x8b4192B9Ad1fCa440A5808641261e5289e6de95D' tokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' # USDC + tokenCoinGeckoId: usd-coin name: USDC symbol: USDC decimals: 6 diff --git a/typescript/infra/config/environments/mainnet3/warp/bsc-lumia-LUMIA-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/bsc-lumia-LUMIA-deployments.yaml index 9db075248..d106ac884 100644 --- a/typescript/infra/config/environments/mainnet3/warp/bsc-lumia-LUMIA-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/bsc-lumia-LUMIA-deployments.yaml @@ -10,6 +10,7 @@ data: type: collateral hypAddress: '0xdD313D475f8A9d81CBE2eA953a357f52e10BA357' tokenAddress: '0xd9343a049d5dbd89cd19dc6bca8c48fb3a0a42a7' + tokenCoinGeckoId: lumia name: Lumia Token symbol: LUMIA decimals: 18 diff --git a/typescript/infra/config/environments/mainnet3/warp/eclipse-SOL-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/eclipse-SOL-deployments.yaml index 0469f45eb..711e6d428 100644 --- a/typescript/infra/config/environments/mainnet3/warp/eclipse-SOL-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/eclipse-SOL-deployments.yaml @@ -9,6 +9,7 @@ data: protocolType: sealevel type: native hypAddress: '8DtAGQpcMuD5sG3KdxDy49ydqXUggR1LQtebh2TECbAc' + tokenCoinGeckoId: solana name: Solana symbol: SOL decimals: 9 diff --git a/typescript/infra/config/environments/mainnet3/warp/eclipse-USDC-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/eclipse-USDC-deployments.yaml index 2955d2c84..7405cb94d 100644 --- a/typescript/infra/config/environments/mainnet3/warp/eclipse-USDC-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/eclipse-USDC-deployments.yaml @@ -10,6 +10,7 @@ data: type: collateral hypAddress: '0xe1De9910fe71cC216490AC7FCF019e13a34481D7' tokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' # USDC + tokenCoinGeckoId: usd-coin name: USDC symbol: USDC decimals: 6 @@ -18,6 +19,7 @@ data: type: collateral hypAddress: '3EpVCPUgyjq2MfGeCttyey6bs5zya5wjYZ2BE6yDg6bm' tokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' + tokenCoinGeckoId: usd-coin isSpl2022: false name: USDC symbol: USDC diff --git a/typescript/infra/config/environments/mainnet3/warp/eclipse-WIF-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/eclipse-WIF-deployments.yaml index 94e44a8b1..1b7245c91 100644 --- a/typescript/infra/config/environments/mainnet3/warp/eclipse-WIF-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/eclipse-WIF-deployments.yaml @@ -10,6 +10,7 @@ data: type: collateral hypAddress: 'CuQmsT4eSF4dYiiGUGYYQxJ7c58pUAD5ADE3BbFGzQKx' tokenAddress: 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm' + tokenCoinGeckoId: dogwifcoin name: dogwifhat symbol: WIF decimals: 9 diff --git a/typescript/infra/config/environments/mainnet3/warp/ethereumUSDC-inevm-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/ethereumUSDC-inevm-deployments.yaml index 0d8ff7755..90f22e0df 100644 --- a/typescript/infra/config/environments/mainnet3/warp/ethereumUSDC-inevm-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/ethereumUSDC-inevm-deployments.yaml @@ -10,6 +10,7 @@ data: type: collateral hypAddress: '0xED56728fb977b0bBdacf65bCdD5e17Bb7e84504f' tokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' # USDC + tokenCoinGeckoId: usd-coin name: USDC symbol: USDC decimals: 6 diff --git a/typescript/infra/config/environments/mainnet3/warp/ethereumUSDT-inevm-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/ethereumUSDT-inevm-deployments.yaml index 25ec599f7..91976ddd9 100644 --- a/typescript/infra/config/environments/mainnet3/warp/ethereumUSDT-inevm-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/ethereumUSDT-inevm-deployments.yaml @@ -10,6 +10,7 @@ data: type: collateral hypAddress: '0xab852e67bf03E74C89aF67C4BA97dd1088D3dA19' tokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7' # USDT + tokenCoinGeckoId: tether name: Tether USD symbol: USDT decimals: 6 diff --git a/typescript/infra/config/environments/mainnet3/warp/injective-inevm-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/injective-inevm-deployments.yaml index efabaf59d..cd39382ea 100644 --- a/typescript/infra/config/environments/mainnet3/warp/injective-inevm-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/injective-inevm-deployments.yaml @@ -9,6 +9,7 @@ data: protocolType: cosmos type: native hypAddress: inj1mv9tjvkaw7x8w8y9vds8pkfq46g2vcfkjehc6k + tokenCoinGeckoId: injective-protocol name: Injective Coin symbol: INJ decimals: 18 diff --git a/typescript/infra/config/environments/mainnet3/warp/neutron-mantapacific-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/neutron-mantapacific-deployments.yaml index ae13acf3d..bf4e1770a 100644 --- a/typescript/infra/config/environments/mainnet3/warp/neutron-mantapacific-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/neutron-mantapacific-deployments.yaml @@ -10,6 +10,7 @@ data: type: collateral hypAddress: neutron1ch7x3xgpnj62weyes8vfada35zff6z59kt2psqhnx9gjnt2ttqdqtva3pa tokenAddress: ibc/773B4D0A3CD667B2275D5A4A7A2F0909C0BA0F4059C0B9181E680DDF4965DCC7 + tokenCoinGeckoId: celestia name: Celestia symbol: TIA decimals: 6 diff --git a/typescript/infra/config/environments/mainnet3/warp/sei-FASTUSD-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/sei-FASTUSD-deployments.yaml index 885c1a4db..ed9672e41 100644 --- a/typescript/infra/config/environments/mainnet3/warp/sei-FASTUSD-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/sei-FASTUSD-deployments.yaml @@ -8,6 +8,7 @@ data: type: collateral hypAddress: '0x9AD81058c6C3Bf552C9014CB30E824717A0ee21b' tokenAddress: '0x15700B564Ca08D9439C58cA5053166E8317aa138' + tokenCoinGeckoId: elixir-deusd # unique setup where we want deUSD to be deposited as collateral and we want fastUSD to be minted as a synthetic on sei name: fastUSD symbol: fastUSD decimals: 18 diff --git a/typescript/infra/config/environments/mainnet3/warp/viction-ethereum-ETH-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/viction-ethereum-ETH-deployments.yaml index 8923facc0..c3e03b54d 100644 --- a/typescript/infra/config/environments/mainnet3/warp/viction-ethereum-ETH-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/viction-ethereum-ETH-deployments.yaml @@ -9,6 +9,7 @@ data: protocolType: ethereum type: native hypAddress: '0x15b5D6B614242B118AA404528A7f3E2Ad241e4A4' + tokenCoinGeckoId: ethereum name: Ether symbol: ETH decimals: 18 diff --git a/typescript/infra/config/environments/mainnet3/warp/viction-ethereum-USDC-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/viction-ethereum-USDC-deployments.yaml index a278a93e8..8a84e6a7c 100644 --- a/typescript/infra/config/environments/mainnet3/warp/viction-ethereum-USDC-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/viction-ethereum-USDC-deployments.yaml @@ -10,6 +10,7 @@ data: type: collateral hypAddress: '0x31Dca7762930f56D81292f85E65c9D67575804fE' tokenAddress: '0x31Dca7762930f56D81292f85E65c9D67575804fE' # USDC + tokenCoinGeckoId: usd-coin name: USD Coin symbol: USDC decimals: 6 diff --git a/typescript/infra/config/environments/mainnet3/warp/viction-ethereum-USDT-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/viction-ethereum-USDT-deployments.yaml index 8ddaa1e08..a22598042 100644 --- a/typescript/infra/config/environments/mainnet3/warp/viction-ethereum-USDT-deployments.yaml +++ b/typescript/infra/config/environments/mainnet3/warp/viction-ethereum-USDT-deployments.yaml @@ -10,6 +10,7 @@ data: type: collateral hypAddress: '0x4221a16A01F61c2b38A03C52d828a7041f6AAA49' tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7' # USDT + tokenCoinGeckoId: tether name: Tether USD symbol: USDT decimals: 6 diff --git a/typescript/infra/package.json b/typescript/infra/package.json index 375d06c59..81361ac85 100644 --- a/typescript/infra/package.json +++ b/typescript/infra/package.json @@ -14,7 +14,7 @@ "@ethersproject/providers": "^5.7.2", "@google-cloud/secret-manager": "^5.5.0", "@hyperlane-xyz/helloworld": "5.6.1", - "@hyperlane-xyz/registry": "4.7.0", + "@hyperlane-xyz/registry": "4.10.0", "@hyperlane-xyz/sdk": "5.6.1", "@hyperlane-xyz/utils": "5.6.1", "@inquirer/prompts": "^5.3.8", diff --git a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts index 3d946a9e6..2d588a89c 100644 --- a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts +++ b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts @@ -9,10 +9,12 @@ import { IXERC20__factory, } from '@hyperlane-xyz/core'; import { ERC20__factory } from '@hyperlane-xyz/core'; +import { createWarpRouteConfigId } from '@hyperlane-xyz/registry'; import { ChainMap, ChainMetadata, ChainName, + CoinGeckoTokenPriceGetter, CosmNativeTokenAdapter, CwNativeTokenAdapter, MultiProtocolProvider, @@ -38,17 +40,41 @@ import { getEnvironmentConfig } from '../core-utils.js'; const logger = rootLogger.child({ module: 'warp-balance-monitor' }); const metricsRegister = new Registry(); + +interface WarpRouteMetrics { + chain_name: ChainName; + token_address: string; + token_name: string; + wallet_address: string; + token_type: TokenType; + warp_route_id: string; + related_chain_names: string; +} + +type WarpRouteMetricLabels = keyof WarpRouteMetrics; + +const warpRouteMetricLabels: WarpRouteMetricLabels[] = [ + 'chain_name', + 'token_address', + 'token_name', + 'wallet_address', + 'token_type', + 'warp_route_id', + 'related_chain_names', +]; + const warpRouteTokenBalance = new Gauge({ name: 'hyperlane_warp_route_token_balance', help: 'HypERC20 token balance of a Warp Route', registers: [metricsRegister], - labelNames: [ - 'chain_name', - 'token_address', - 'token_name', - 'wallet_address', - 'token_type', - ], + labelNames: warpRouteMetricLabels, +}); + +const warpRouteCollateralValue = new Gauge({ + name: 'hyperlane_warp_route_collateral_value', + help: 'Total value of collateral held in a HypERC20Collateral or HypNative contract of a Warp Route', + registers: [metricsRegister], + labelNames: warpRouteMetricLabels, }); const xERC20LimitsGauge = new Gauge({ @@ -66,6 +92,11 @@ interface xERC20Limit { burnMax: number; } +interface WarpRouteInfo { + balance: number; + valueUSD?: number; +} + export function readWarpRouteConfig(filePath: string) { const config = readYaml(filePath); if (!config) throw new Error(`No warp config found at ${filePath}`); @@ -112,7 +143,8 @@ async function main(): Promise { async function checkBalance( tokenConfig: WarpRouteConfig, multiProtocolProvider: MultiProtocolProvider, -): Promise> { + tokenPriceGetter: CoinGeckoTokenPriceGetter, +): Promise> { const output = objMap( tokenConfig, async (chain: ChainName, token: WarpRouteConfig[ChainName]) => { @@ -122,8 +154,12 @@ async function checkBalance( case ProtocolType.Ethereum: { const provider = multiProtocolProvider.getEthersV5Provider(chain); const nativeBalance = await provider.getBalance(token.hypAddress); - return parseFloat( - ethers.utils.formatUnits(nativeBalance, token.decimals), + + return getNativeTokenWarpInfo( + nativeBalance, + token.decimals, + tokenPriceGetter, + chain, ); } case ProtocolType.Sealevel: { @@ -142,8 +178,12 @@ async function checkBalance( const balance = ethers.BigNumber.from( await adapter.getBalance(token.hypAddress), ); - return parseFloat( - ethers.utils.formatUnits(balance, token.decimals), + + return getNativeTokenWarpInfo( + balance, + token.decimals, + tokenPriceGetter, + chain, ); } case ProtocolType.Cosmos: { @@ -156,8 +196,12 @@ async function checkBalance( { ibcDenom: token.ibcDenom }, ); const tokenBalance = await adapter.getBalance(token.hypAddress); - return parseFloat( - ethers.utils.formatUnits(tokenBalance, token.decimals), + + return getNativeTokenWarpInfo( + tokenBalance, + token.decimals, + tokenPriceGetter, + chain, ); } } @@ -177,8 +221,11 @@ async function checkBalance( token.hypAddress, ); - return parseFloat( - ethers.utils.formatUnits(collateralBalance, token.decimals), + return getCollateralTokenWarpInfo( + collateralBalance, + token.decimals, + tokenPriceGetter, + token.tokenCoinGeckoId, ); } case ProtocolType.Sealevel: { @@ -198,8 +245,12 @@ async function checkBalance( const collateralBalance = ethers.BigNumber.from( await adapter.getBalance(token.hypAddress), ); - return parseFloat( - ethers.utils.formatUnits(collateralBalance, token.decimals), + + return getCollateralTokenWarpInfo( + collateralBalance, + token.decimals, + tokenPriceGetter, + token.tokenCoinGeckoId, ); } case ProtocolType.Cosmos: { @@ -216,8 +267,12 @@ async function checkBalance( const collateralBalance = ethers.BigNumber.from( await adapter.getBalance(token.hypAddress), ); - return parseFloat( - ethers.utils.formatUnits(collateralBalance, token.decimals), + + return getCollateralTokenWarpInfo( + collateralBalance, + token.decimals, + tokenPriceGetter, + token.tokenCoinGeckoId, ); } } @@ -232,9 +287,11 @@ async function checkBalance( provider, ); const syntheticBalance = await tokenContract.totalSupply(); - return parseFloat( - ethers.utils.formatUnits(syntheticBalance, token.decimals), - ); + return { + balance: parseFloat( + ethers.utils.formatUnits(syntheticBalance, token.decimals), + ), + }; } case ProtocolType.Sealevel: { if (!token.tokenAddress) @@ -253,13 +310,15 @@ async function checkBalance( const syntheticBalance = ethers.BigNumber.from( await adapter.getTotalSupply(), ); - return parseFloat( - ethers.utils.formatUnits(syntheticBalance, token.decimals), - ); + return { + balance: parseFloat( + ethers.utils.formatUnits(syntheticBalance, token.decimals), + ), + }; } case ProtocolType.Cosmos: // TODO - cosmos synthetic - return 0; + return { balance: 0 }; } break; } @@ -275,9 +334,11 @@ async function checkBalance( const xerc20 = IXERC20__factory.connect(xerc20Address, provider); const syntheticBalance = await xerc20.totalSupply(); - return parseFloat( - ethers.utils.formatUnits(syntheticBalance, token.decimals), - ); + return { + balance: parseFloat( + ethers.utils.formatUnits(syntheticBalance, token.decimals), + ), + }; } default: throw new Error( @@ -307,8 +368,11 @@ async function checkBalance( xerc20LockboxAddress, ); - return parseFloat( - ethers.utils.formatUnits(collateralBalance, token.decimals), + return getCollateralTokenWarpInfo( + collateralBalance, + token.decimals, + tokenPriceGetter, + token.tokenCoinGeckoId, ); } default: @@ -318,7 +382,7 @@ async function checkBalance( } } } - return 0; + return { balance: 0 }; }, ); @@ -327,22 +391,44 @@ async function checkBalance( export function updateTokenBalanceMetrics( tokenConfig: WarpRouteConfig, - balances: ChainMap, + balances: ChainMap, ) { objMap(tokenConfig, (chain: ChainName, token: WarpRouteConfig[ChainName]) => { - warpRouteTokenBalance - .labels({ - chain_name: chain, - token_address: token.tokenAddress ?? ethers.constants.AddressZero, - token_name: token.name, - wallet_address: token.hypAddress, - token_type: token.type, - }) - .set(balances[chain]); + const metrics: WarpRouteMetrics = { + chain_name: chain, + token_address: token.tokenAddress ?? ethers.constants.AddressZero, + token_name: token.name, + wallet_address: token.hypAddress, + token_type: token.type, + warp_route_id: createWarpRouteConfigId( + token.symbol, + Object.keys(tokenConfig) as ChainName[], + ), + related_chain_names: Object.keys(tokenConfig) + .filter((chainName) => chainName !== chain) + .sort() + .join(','), + }; + + warpRouteTokenBalance.labels(metrics).set(balances[chain].balance); + if (balances[chain].valueUSD) { + warpRouteCollateralValue + .labels(metrics) + .set(balances[chain].valueUSD as number); + logger.debug('Collateral value updated for chain', { + chain, + related_chain_names: metrics.related_chain_names, + warp_route_id: metrics.warp_route_id, + token: metrics.token_name, + value: balances[chain].valueUSD, + }); + } logger.debug('Wallet balance updated for chain', { chain, - token: token.name, - balance: balances[chain], + related_chain_names: metrics.related_chain_names, + warp_route_id: metrics.warp_route_id, + token: metrics.token_name, + balance: balances[chain].balance, }); }); } @@ -468,27 +554,120 @@ const getXERC20Limit = async ( }; }; +async function getTokenPriceByChain( + chain: ChainName, + tokenPriceGetter: CoinGeckoTokenPriceGetter, +): Promise { + try { + return await tokenPriceGetter.getTokenPrice(chain); + } catch (e) { + logger.warn('Error getting token price', e); + return undefined; + } +} + +async function getNativeTokenValue( + chain: ChainName, + balanceFloat: number, + tokenPriceGetter: CoinGeckoTokenPriceGetter, +): Promise { + const price = await getTokenPriceByChain(chain, tokenPriceGetter); + logger.debug(`${chain} native token price ${price}`); + if (!price) return undefined; + return balanceFloat * price; +} + +async function getNativeTokenWarpInfo( + balance: ethers.BigNumber | bigint, + decimal: number, + tokenPriceGetter: CoinGeckoTokenPriceGetter, + chain: ChainName, +): Promise { + const balanceFloat = parseFloat(ethers.utils.formatUnits(balance, decimal)); + const value = await getNativeTokenValue( + chain, + balanceFloat, + tokenPriceGetter, + ); + return { balance: balanceFloat, valueUSD: value }; +} + +async function getCollateralTokenPrice( + tokenCoinGeckoId: string | undefined, + tokenPriceGetter: CoinGeckoTokenPriceGetter, +): Promise { + if (!tokenCoinGeckoId) return undefined; + const prices = await tokenPriceGetter.getTokenPriceByIds([tokenCoinGeckoId]); + if (!prices) return undefined; + return prices[0]; +} + +async function getCollateralTokenValue( + tokenCoinGeckoId: string | undefined, + balanceFloat: number, + tokenPriceGetter: CoinGeckoTokenPriceGetter, +): Promise { + const price = await getCollateralTokenPrice( + tokenCoinGeckoId, + tokenPriceGetter, + ); + logger.debug(`${tokenCoinGeckoId} token price ${price}`); + if (!price) return undefined; + return balanceFloat * price; +} + +async function getCollateralTokenWarpInfo( + balance: ethers.BigNumber | bigint, + decimal: number, + tokenPriceGetter: CoinGeckoTokenPriceGetter, + tokenCoinGeckoId?: string, +): Promise { + const balanceFloat = parseFloat(ethers.utils.formatUnits(balance, decimal)); + const value = await getCollateralTokenValue( + tokenCoinGeckoId, + balanceFloat, + tokenPriceGetter, + ); + return { balance: balanceFloat, valueUSD: value }; +} + async function checkWarpRouteMetrics( checkFrequency: number, tokenConfig: WarpRouteConfig, chainMetadata: ChainMap, ) { + const tokenPriceGetter = + CoinGeckoTokenPriceGetter.withDefaultCoinGecko(chainMetadata); + setInterval(async () => { try { const multiProtocolProvider = new MultiProtocolProvider(chainMetadata); - const balances = await checkBalance(tokenConfig, multiProtocolProvider); + const balances = await checkBalance( + tokenConfig, + multiProtocolProvider, + tokenPriceGetter, + ); logger.info('Token Balances:', balances); updateTokenBalanceMetrics(tokenConfig, balances); } catch (e) { logger.error('Error checking balances', e); } - try { - const xERC20Limits = await getXERC20Limits(tokenConfig, chainMetadata); - logger.info('xERC20 Limits:', xERC20Limits); - updateXERC20LimitsMetrics(xERC20Limits); - } catch (e) { - logger.error('Error checking xERC20 limits', e); + // only check xERC20 limits if there are xERC20 tokens in the config + if ( + Object.keys(tokenConfig).some( + (chain) => + tokenConfig[chain].type === TokenType.XERC20 || + tokenConfig[chain].type === TokenType.XERC20Lockbox, + ) + ) { + try { + const xERC20Limits = await getXERC20Limits(tokenConfig, chainMetadata); + logger.info('xERC20 Limits:', xERC20Limits); + updateXERC20LimitsMetrics(xERC20Limits); + } catch (e) { + logger.error('Error checking xERC20 limits', e); + } } }, checkFrequency); } diff --git a/typescript/sdk/src/gas/token-prices.ts b/typescript/sdk/src/gas/token-prices.ts index 26d17cd8b..d60949630 100644 --- a/typescript/sdk/src/gas/token-prices.ts +++ b/typescript/sdk/src/gas/token-prices.ts @@ -23,23 +23,23 @@ type TokenPriceCacheEntry = { }; class TokenPriceCache { - protected cache: Map; + protected cache: Map; protected freshSeconds: number; protected evictionSeconds: number; constructor(freshSeconds = 60, evictionSeconds = 3 * 60 * 60) { - this.cache = new Map(); + this.cache = new Map(); this.freshSeconds = freshSeconds; this.evictionSeconds = evictionSeconds; } - put(chain: ChainName, price: number): void { + put(id: string, price: number): void { const now = new Date(); - this.cache.set(chain, { timestamp: now, price }); + this.cache.set(id, { timestamp: now, price }); } - isFresh(chain: ChainName): boolean { - const entry = this.cache.get(chain); + isFresh(id: string): boolean { + const entry = this.cache.get(id); if (!entry) return false; const expiryTime = new Date( @@ -49,17 +49,17 @@ class TokenPriceCache { return now < expiryTime; } - fetch(chain: ChainName): number { - const entry = this.cache.get(chain); + fetch(id: string): number { + const entry = this.cache.get(id); if (!entry) { - throw new Error(`no entry found for ${chain} in token price cache`); + throw new Error(`no entry found for ${id} in token price cache`); } const evictionTime = new Date( entry.timestamp.getTime() + 1000 * this.evictionSeconds, ); const now = new Date(); if (now > evictionTime) { - throw new Error(`evicted entry found for ${chain} in token price cache`); + throw new Error(`evicted entry found for ${id} in token price cache`); } return entry.price; } @@ -97,20 +97,30 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter { ); } - async getTokenPrice(chain: ChainName): Promise { - const [price] = await this.getTokenPrices([chain]); + async getTokenPrice( + chain: ChainName, + currency: string = 'usd', + ): Promise { + const [price] = await this.getTokenPrices([chain], currency); return price; } async getTokenExchangeRate( base: ChainName, quote: ChainName, + currency: string = 'usd', ): Promise { - const [basePrice, quotePrice] = await this.getTokenPrices([base, quote]); + const [basePrice, quotePrice] = await this.getTokenPrices( + [base, quote], + currency, + ); return basePrice / quotePrice; } - private async getTokenPrices(chains: ChainName[]): Promise { + private async getTokenPrices( + chains: ChainName[], + currency: string = 'usd', + ): Promise { const isMainnet = chains.map((c) => !this.metadata[c].isTestnet); const allMainnets = isMainnet.every((v) => v === true); const allTestnets = isMainnet.every((v) => v === false); @@ -125,32 +135,43 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter { ); } - const toQuery = chains.filter((c) => !this.cache.isFresh(c)); + const ids = chains.map( + (chain) => this.metadata[chain].gasCurrencyCoinGeckoId || chain, + ); + + await this.getTokenPriceByIds(ids, currency); + return chains.map((chain) => + this.cache.fetch(this.metadata[chain].gasCurrencyCoinGeckoId || chain), + ); + } + + public async getTokenPriceByIds( + ids: string[], + currency: string = 'usd', + ): Promise { + const toQuery = ids.filter((id) => !this.cache.isFresh(id)); + await sleep(this.sleepMsBetweenRequests); + if (toQuery.length > 0) { + let response: any; try { - await this.queryTokenPrices(toQuery); + response = await this.coinGecko.simple.price({ + ids: toQuery, + vs_currencies: [currency], + }); + + if (response.success === true) { + const prices = toQuery.map((id) => response.data[id][currency]); + toQuery.map((id, i) => this.cache.put(id, prices[i])); + } else { + rootLogger.warn('Failed to query token prices', response.message); + return undefined; + } } catch (e) { - rootLogger.warn('Failed to query token prices', e); + rootLogger.warn('Error when querying token prices', e); + return undefined; } } - return chains.map((chain) => this.cache.fetch(chain)); - } - - private async queryTokenPrices(chains: ChainName[]): Promise { - const currency = 'usd'; - // The CoinGecko API expects, in some cases, IDs that do not match - // ChainNames. - const ids = chains.map( - (chain) => this.metadata[chain].gasCurrencyCoinGeckoId || chain, - ); - // Coingecko rate limits, so we are adding this sleep - await sleep(this.sleepMsBetweenRequests); - const response = await this.coinGecko.simple.price({ - ids, - vs_currencies: [currency], - }); - const prices = ids.map((id) => response.data[id][currency]); - // Update the cache with the newly fetched prices - chains.map((chain, i) => this.cache.put(chain, prices[i])); + return ids.map((id) => this.cache.fetch(id)); } } diff --git a/typescript/sdk/src/metadata/warpRouteConfig.ts b/typescript/sdk/src/metadata/warpRouteConfig.ts index 2fd69ce03..8c1ee346c 100644 --- a/typescript/sdk/src/metadata/warpRouteConfig.ts +++ b/typescript/sdk/src/metadata/warpRouteConfig.ts @@ -10,6 +10,7 @@ const TokenConfigSchema = z.object({ type: z.nativeEnum(TokenType), hypAddress: z.string(), // HypERC20Collateral, HypERC20Synthetic, HypNativeToken address tokenAddress: z.string().optional(), // external token address needed for collateral type eg tokenAddress.balanceOf(hypAddress) + tokenCoinGeckoId: z.string().optional(), // CoinGecko id for token name: z.string(), symbol: z.string(), decimals: z.number(), diff --git a/yarn.lock b/yarn.lock index 279fe75e6..161abbd88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7971,7 +7971,7 @@ __metadata: "@ethersproject/providers": "npm:^5.7.2" "@google-cloud/secret-manager": "npm:^5.5.0" "@hyperlane-xyz/helloworld": "npm:5.6.1" - "@hyperlane-xyz/registry": "npm:4.7.0" + "@hyperlane-xyz/registry": "npm:4.10.0" "@hyperlane-xyz/sdk": "npm:5.6.1" "@hyperlane-xyz/utils": "npm:5.6.1" "@inquirer/prompts": "npm:^5.3.8" @@ -8029,6 +8029,16 @@ __metadata: languageName: unknown linkType: soft +"@hyperlane-xyz/registry@npm:4.10.0": + version: 4.10.0 + resolution: "@hyperlane-xyz/registry@npm:4.10.0" + dependencies: + yaml: "npm:2.4.5" + zod: "npm:^3.21.2" + checksum: 22bb18f426cbada8b97db0894fe5d0980dfc08ecbd5174c978b7aeb6d8df9706f93d7e9cf0630644d2455ad05feee714dc2a38ec515a717b0b257184637902fb + languageName: node + linkType: hard + "@hyperlane-xyz/registry@npm:4.7.0": version: 4.7.0 resolution: "@hyperlane-xyz/registry@npm:4.7.0"