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
pull/4751/head
Mohammed Hussan 1 month ago committed by GitHub
parent 1bd8e3e384
commit d5bdb2c28a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml
  2. 1
      typescript/infra/config/environments/mainnet3/warp/ancient8-USDC-deployments.yaml
  3. 1
      typescript/infra/config/environments/mainnet3/warp/bsc-lumia-LUMIA-deployments.yaml
  4. 1
      typescript/infra/config/environments/mainnet3/warp/eclipse-SOL-deployments.yaml
  5. 2
      typescript/infra/config/environments/mainnet3/warp/eclipse-USDC-deployments.yaml
  6. 1
      typescript/infra/config/environments/mainnet3/warp/eclipse-WIF-deployments.yaml
  7. 1
      typescript/infra/config/environments/mainnet3/warp/ethereumUSDC-inevm-deployments.yaml
  8. 1
      typescript/infra/config/environments/mainnet3/warp/ethereumUSDT-inevm-deployments.yaml
  9. 1
      typescript/infra/config/environments/mainnet3/warp/injective-inevm-deployments.yaml
  10. 1
      typescript/infra/config/environments/mainnet3/warp/neutron-mantapacific-deployments.yaml
  11. 1
      typescript/infra/config/environments/mainnet3/warp/sei-FASTUSD-deployments.yaml
  12. 1
      typescript/infra/config/environments/mainnet3/warp/viction-ethereum-ETH-deployments.yaml
  13. 1
      typescript/infra/config/environments/mainnet3/warp/viction-ethereum-USDC-deployments.yaml
  14. 1
      typescript/infra/config/environments/mainnet3/warp/viction-ethereum-USDT-deployments.yaml
  15. 2
      typescript/infra/package.json
  16. 253
      typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts
  17. 91
      typescript/sdk/src/gas/token-prices.ts
  18. 1
      typescript/sdk/src/metadata/warpRouteConfig.ts
  19. 12
      yarn.lock

@ -10,6 +10,7 @@ data:
symbol: ezETH symbol: ezETH
hypAddress: '0xC59336D8edDa9722B4f1Ec104007191Ec16f7087' hypAddress: '0xC59336D8edDa9722B4f1Ec104007191Ec16f7087'
tokenAddress: '0xbf5495Efe5DB9ce00f80364C8B423567e58d2110' tokenAddress: '0xbf5495Efe5DB9ce00f80364C8B423567e58d2110'
tokenCoinGeckoId: renzo-restaked-eth
decimals: 18 decimals: 18
bsc: bsc:
protocolType: ethereum protocolType: ethereum

@ -10,6 +10,7 @@ data:
type: collateral type: collateral
hypAddress: '0x8b4192B9Ad1fCa440A5808641261e5289e6de95D' hypAddress: '0x8b4192B9Ad1fCa440A5808641261e5289e6de95D'
tokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' # USDC tokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' # USDC
tokenCoinGeckoId: usd-coin
name: USDC name: USDC
symbol: USDC symbol: USDC
decimals: 6 decimals: 6

@ -10,6 +10,7 @@ data:
type: collateral type: collateral
hypAddress: '0xdD313D475f8A9d81CBE2eA953a357f52e10BA357' hypAddress: '0xdD313D475f8A9d81CBE2eA953a357f52e10BA357'
tokenAddress: '0xd9343a049d5dbd89cd19dc6bca8c48fb3a0a42a7' tokenAddress: '0xd9343a049d5dbd89cd19dc6bca8c48fb3a0a42a7'
tokenCoinGeckoId: lumia
name: Lumia Token name: Lumia Token
symbol: LUMIA symbol: LUMIA
decimals: 18 decimals: 18

@ -9,6 +9,7 @@ data:
protocolType: sealevel protocolType: sealevel
type: native type: native
hypAddress: '8DtAGQpcMuD5sG3KdxDy49ydqXUggR1LQtebh2TECbAc' hypAddress: '8DtAGQpcMuD5sG3KdxDy49ydqXUggR1LQtebh2TECbAc'
tokenCoinGeckoId: solana
name: Solana name: Solana
symbol: SOL symbol: SOL
decimals: 9 decimals: 9

@ -10,6 +10,7 @@ data:
type: collateral type: collateral
hypAddress: '0xe1De9910fe71cC216490AC7FCF019e13a34481D7' hypAddress: '0xe1De9910fe71cC216490AC7FCF019e13a34481D7'
tokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' # USDC tokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' # USDC
tokenCoinGeckoId: usd-coin
name: USDC name: USDC
symbol: USDC symbol: USDC
decimals: 6 decimals: 6
@ -18,6 +19,7 @@ data:
type: collateral type: collateral
hypAddress: '3EpVCPUgyjq2MfGeCttyey6bs5zya5wjYZ2BE6yDg6bm' hypAddress: '3EpVCPUgyjq2MfGeCttyey6bs5zya5wjYZ2BE6yDg6bm'
tokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' tokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
tokenCoinGeckoId: usd-coin
isSpl2022: false isSpl2022: false
name: USDC name: USDC
symbol: USDC symbol: USDC

@ -10,6 +10,7 @@ data:
type: collateral type: collateral
hypAddress: 'CuQmsT4eSF4dYiiGUGYYQxJ7c58pUAD5ADE3BbFGzQKx' hypAddress: 'CuQmsT4eSF4dYiiGUGYYQxJ7c58pUAD5ADE3BbFGzQKx'
tokenAddress: 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm' tokenAddress: 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm'
tokenCoinGeckoId: dogwifcoin
name: dogwifhat name: dogwifhat
symbol: WIF symbol: WIF
decimals: 9 decimals: 9

@ -10,6 +10,7 @@ data:
type: collateral type: collateral
hypAddress: '0xED56728fb977b0bBdacf65bCdD5e17Bb7e84504f' hypAddress: '0xED56728fb977b0bBdacf65bCdD5e17Bb7e84504f'
tokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' # USDC tokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' # USDC
tokenCoinGeckoId: usd-coin
name: USDC name: USDC
symbol: USDC symbol: USDC
decimals: 6 decimals: 6

@ -10,6 +10,7 @@ data:
type: collateral type: collateral
hypAddress: '0xab852e67bf03E74C89aF67C4BA97dd1088D3dA19' hypAddress: '0xab852e67bf03E74C89aF67C4BA97dd1088D3dA19'
tokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7' # USDT tokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7' # USDT
tokenCoinGeckoId: tether
name: Tether USD name: Tether USD
symbol: USDT symbol: USDT
decimals: 6 decimals: 6

@ -9,6 +9,7 @@ data:
protocolType: cosmos protocolType: cosmos
type: native type: native
hypAddress: inj1mv9tjvkaw7x8w8y9vds8pkfq46g2vcfkjehc6k hypAddress: inj1mv9tjvkaw7x8w8y9vds8pkfq46g2vcfkjehc6k
tokenCoinGeckoId: injective-protocol
name: Injective Coin name: Injective Coin
symbol: INJ symbol: INJ
decimals: 18 decimals: 18

@ -10,6 +10,7 @@ data:
type: collateral type: collateral
hypAddress: neutron1ch7x3xgpnj62weyes8vfada35zff6z59kt2psqhnx9gjnt2ttqdqtva3pa hypAddress: neutron1ch7x3xgpnj62weyes8vfada35zff6z59kt2psqhnx9gjnt2ttqdqtva3pa
tokenAddress: ibc/773B4D0A3CD667B2275D5A4A7A2F0909C0BA0F4059C0B9181E680DDF4965DCC7 tokenAddress: ibc/773B4D0A3CD667B2275D5A4A7A2F0909C0BA0F4059C0B9181E680DDF4965DCC7
tokenCoinGeckoId: celestia
name: Celestia name: Celestia
symbol: TIA symbol: TIA
decimals: 6 decimals: 6

@ -8,6 +8,7 @@ data:
type: collateral type: collateral
hypAddress: '0x9AD81058c6C3Bf552C9014CB30E824717A0ee21b' hypAddress: '0x9AD81058c6C3Bf552C9014CB30E824717A0ee21b'
tokenAddress: '0x15700B564Ca08D9439C58cA5053166E8317aa138' 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 name: fastUSD
symbol: fastUSD symbol: fastUSD
decimals: 18 decimals: 18

@ -9,6 +9,7 @@ data:
protocolType: ethereum protocolType: ethereum
type: native type: native
hypAddress: '0x15b5D6B614242B118AA404528A7f3E2Ad241e4A4' hypAddress: '0x15b5D6B614242B118AA404528A7f3E2Ad241e4A4'
tokenCoinGeckoId: ethereum
name: Ether name: Ether
symbol: ETH symbol: ETH
decimals: 18 decimals: 18

@ -10,6 +10,7 @@ data:
type: collateral type: collateral
hypAddress: '0x31Dca7762930f56D81292f85E65c9D67575804fE' hypAddress: '0x31Dca7762930f56D81292f85E65c9D67575804fE'
tokenAddress: '0x31Dca7762930f56D81292f85E65c9D67575804fE' # USDC tokenAddress: '0x31Dca7762930f56D81292f85E65c9D67575804fE' # USDC
tokenCoinGeckoId: usd-coin
name: USD Coin name: USD Coin
symbol: USDC symbol: USDC
decimals: 6 decimals: 6

@ -10,6 +10,7 @@ data:
type: collateral type: collateral
hypAddress: '0x4221a16A01F61c2b38A03C52d828a7041f6AAA49' hypAddress: '0x4221a16A01F61c2b38A03C52d828a7041f6AAA49'
tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7' # USDT tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7' # USDT
tokenCoinGeckoId: tether
name: Tether USD name: Tether USD
symbol: USDT symbol: USDT
decimals: 6 decimals: 6

@ -14,7 +14,7 @@
"@ethersproject/providers": "^5.7.2", "@ethersproject/providers": "^5.7.2",
"@google-cloud/secret-manager": "^5.5.0", "@google-cloud/secret-manager": "^5.5.0",
"@hyperlane-xyz/helloworld": "5.6.1", "@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/sdk": "5.6.1",
"@hyperlane-xyz/utils": "5.6.1", "@hyperlane-xyz/utils": "5.6.1",
"@inquirer/prompts": "^5.3.8", "@inquirer/prompts": "^5.3.8",

@ -9,10 +9,12 @@ import {
IXERC20__factory, IXERC20__factory,
} from '@hyperlane-xyz/core'; } from '@hyperlane-xyz/core';
import { ERC20__factory } from '@hyperlane-xyz/core'; import { ERC20__factory } from '@hyperlane-xyz/core';
import { createWarpRouteConfigId } from '@hyperlane-xyz/registry';
import { import {
ChainMap, ChainMap,
ChainMetadata, ChainMetadata,
ChainName, ChainName,
CoinGeckoTokenPriceGetter,
CosmNativeTokenAdapter, CosmNativeTokenAdapter,
CwNativeTokenAdapter, CwNativeTokenAdapter,
MultiProtocolProvider, MultiProtocolProvider,
@ -38,17 +40,41 @@ import { getEnvironmentConfig } from '../core-utils.js';
const logger = rootLogger.child({ module: 'warp-balance-monitor' }); const logger = rootLogger.child({ module: 'warp-balance-monitor' });
const metricsRegister = new Registry(); const metricsRegister = new Registry();
const warpRouteTokenBalance = new Gauge({
name: 'hyperlane_warp_route_token_balance', interface WarpRouteMetrics {
help: 'HypERC20 token balance of a Warp Route', chain_name: ChainName;
registers: [metricsRegister], token_address: string;
labelNames: [ 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', 'chain_name',
'token_address', 'token_address',
'token_name', 'token_name',
'wallet_address', 'wallet_address',
'token_type', '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: 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({ const xERC20LimitsGauge = new Gauge({
@ -66,6 +92,11 @@ interface xERC20Limit {
burnMax: number; burnMax: number;
} }
interface WarpRouteInfo {
balance: number;
valueUSD?: number;
}
export function readWarpRouteConfig(filePath: string) { export function readWarpRouteConfig(filePath: string) {
const config = readYaml(filePath); const config = readYaml(filePath);
if (!config) throw new Error(`No warp config found at ${filePath}`); if (!config) throw new Error(`No warp config found at ${filePath}`);
@ -112,7 +143,8 @@ async function main(): Promise<boolean> {
async function checkBalance( async function checkBalance(
tokenConfig: WarpRouteConfig, tokenConfig: WarpRouteConfig,
multiProtocolProvider: MultiProtocolProvider, multiProtocolProvider: MultiProtocolProvider,
): Promise<ChainMap<number>> { tokenPriceGetter: CoinGeckoTokenPriceGetter,
): Promise<ChainMap<WarpRouteInfo>> {
const output = objMap( const output = objMap(
tokenConfig, tokenConfig,
async (chain: ChainName, token: WarpRouteConfig[ChainName]) => { async (chain: ChainName, token: WarpRouteConfig[ChainName]) => {
@ -122,8 +154,12 @@ async function checkBalance(
case ProtocolType.Ethereum: { case ProtocolType.Ethereum: {
const provider = multiProtocolProvider.getEthersV5Provider(chain); const provider = multiProtocolProvider.getEthersV5Provider(chain);
const nativeBalance = await provider.getBalance(token.hypAddress); 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: { case ProtocolType.Sealevel: {
@ -142,8 +178,12 @@ async function checkBalance(
const balance = ethers.BigNumber.from( const balance = ethers.BigNumber.from(
await adapter.getBalance(token.hypAddress), await adapter.getBalance(token.hypAddress),
); );
return parseFloat(
ethers.utils.formatUnits(balance, token.decimals), return getNativeTokenWarpInfo(
balance,
token.decimals,
tokenPriceGetter,
chain,
); );
} }
case ProtocolType.Cosmos: { case ProtocolType.Cosmos: {
@ -156,8 +196,12 @@ async function checkBalance(
{ ibcDenom: token.ibcDenom }, { ibcDenom: token.ibcDenom },
); );
const tokenBalance = await adapter.getBalance(token.hypAddress); 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, token.hypAddress,
); );
return parseFloat( return getCollateralTokenWarpInfo(
ethers.utils.formatUnits(collateralBalance, token.decimals), collateralBalance,
token.decimals,
tokenPriceGetter,
token.tokenCoinGeckoId,
); );
} }
case ProtocolType.Sealevel: { case ProtocolType.Sealevel: {
@ -198,8 +245,12 @@ async function checkBalance(
const collateralBalance = ethers.BigNumber.from( const collateralBalance = ethers.BigNumber.from(
await adapter.getBalance(token.hypAddress), await adapter.getBalance(token.hypAddress),
); );
return parseFloat(
ethers.utils.formatUnits(collateralBalance, token.decimals), return getCollateralTokenWarpInfo(
collateralBalance,
token.decimals,
tokenPriceGetter,
token.tokenCoinGeckoId,
); );
} }
case ProtocolType.Cosmos: { case ProtocolType.Cosmos: {
@ -216,8 +267,12 @@ async function checkBalance(
const collateralBalance = ethers.BigNumber.from( const collateralBalance = ethers.BigNumber.from(
await adapter.getBalance(token.hypAddress), 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, provider,
); );
const syntheticBalance = await tokenContract.totalSupply(); const syntheticBalance = await tokenContract.totalSupply();
return parseFloat( return {
balance: parseFloat(
ethers.utils.formatUnits(syntheticBalance, token.decimals), ethers.utils.formatUnits(syntheticBalance, token.decimals),
); ),
};
} }
case ProtocolType.Sealevel: { case ProtocolType.Sealevel: {
if (!token.tokenAddress) if (!token.tokenAddress)
@ -253,13 +310,15 @@ async function checkBalance(
const syntheticBalance = ethers.BigNumber.from( const syntheticBalance = ethers.BigNumber.from(
await adapter.getTotalSupply(), await adapter.getTotalSupply(),
); );
return parseFloat( return {
balance: parseFloat(
ethers.utils.formatUnits(syntheticBalance, token.decimals), ethers.utils.formatUnits(syntheticBalance, token.decimals),
); ),
};
} }
case ProtocolType.Cosmos: case ProtocolType.Cosmos:
// TODO - cosmos synthetic // TODO - cosmos synthetic
return 0; return { balance: 0 };
} }
break; break;
} }
@ -275,9 +334,11 @@ async function checkBalance(
const xerc20 = IXERC20__factory.connect(xerc20Address, provider); const xerc20 = IXERC20__factory.connect(xerc20Address, provider);
const syntheticBalance = await xerc20.totalSupply(); const syntheticBalance = await xerc20.totalSupply();
return parseFloat( return {
balance: parseFloat(
ethers.utils.formatUnits(syntheticBalance, token.decimals), ethers.utils.formatUnits(syntheticBalance, token.decimals),
); ),
};
} }
default: default:
throw new Error( throw new Error(
@ -307,8 +368,11 @@ async function checkBalance(
xerc20LockboxAddress, xerc20LockboxAddress,
); );
return parseFloat( return getCollateralTokenWarpInfo(
ethers.utils.formatUnits(collateralBalance, token.decimals), collateralBalance,
token.decimals,
tokenPriceGetter,
token.tokenCoinGeckoId,
); );
} }
default: default:
@ -318,7 +382,7 @@ async function checkBalance(
} }
} }
} }
return 0; return { balance: 0 };
}, },
); );
@ -327,22 +391,44 @@ async function checkBalance(
export function updateTokenBalanceMetrics( export function updateTokenBalanceMetrics(
tokenConfig: WarpRouteConfig, tokenConfig: WarpRouteConfig,
balances: ChainMap<number>, balances: ChainMap<WarpRouteInfo>,
) { ) {
objMap(tokenConfig, (chain: ChainName, token: WarpRouteConfig[ChainName]) => { objMap(tokenConfig, (chain: ChainName, token: WarpRouteConfig[ChainName]) => {
warpRouteTokenBalance const metrics: WarpRouteMetrics = {
.labels({
chain_name: chain, chain_name: chain,
token_address: token.tokenAddress ?? ethers.constants.AddressZero, token_address: token.tokenAddress ?? ethers.constants.AddressZero,
token_name: token.name, token_name: token.name,
wallet_address: token.hypAddress, wallet_address: token.hypAddress,
token_type: token.type, token_type: token.type,
}) warp_route_id: createWarpRouteConfigId(
.set(balances[chain]); 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', { logger.debug('Wallet balance updated for chain', {
chain, chain,
token: token.name, related_chain_names: metrics.related_chain_names,
balance: balances[chain], warp_route_id: metrics.warp_route_id,
token: metrics.token_name,
balance: balances[chain].balance,
}); });
}); });
} }
@ -468,21 +554,113 @@ const getXERC20Limit = async (
}; };
}; };
async function getTokenPriceByChain(
chain: ChainName,
tokenPriceGetter: CoinGeckoTokenPriceGetter,
): Promise<number | undefined> {
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<number | undefined> {
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<WarpRouteInfo> {
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<number | undefined> {
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<number | undefined> {
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<WarpRouteInfo> {
const balanceFloat = parseFloat(ethers.utils.formatUnits(balance, decimal));
const value = await getCollateralTokenValue(
tokenCoinGeckoId,
balanceFloat,
tokenPriceGetter,
);
return { balance: balanceFloat, valueUSD: value };
}
async function checkWarpRouteMetrics( async function checkWarpRouteMetrics(
checkFrequency: number, checkFrequency: number,
tokenConfig: WarpRouteConfig, tokenConfig: WarpRouteConfig,
chainMetadata: ChainMap<ChainMetadata>, chainMetadata: ChainMap<ChainMetadata>,
) { ) {
const tokenPriceGetter =
CoinGeckoTokenPriceGetter.withDefaultCoinGecko(chainMetadata);
setInterval(async () => { setInterval(async () => {
try { try {
const multiProtocolProvider = new MultiProtocolProvider(chainMetadata); const multiProtocolProvider = new MultiProtocolProvider(chainMetadata);
const balances = await checkBalance(tokenConfig, multiProtocolProvider); const balances = await checkBalance(
tokenConfig,
multiProtocolProvider,
tokenPriceGetter,
);
logger.info('Token Balances:', balances); logger.info('Token Balances:', balances);
updateTokenBalanceMetrics(tokenConfig, balances); updateTokenBalanceMetrics(tokenConfig, balances);
} catch (e) { } catch (e) {
logger.error('Error checking balances', e); logger.error('Error checking balances', 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 { try {
const xERC20Limits = await getXERC20Limits(tokenConfig, chainMetadata); const xERC20Limits = await getXERC20Limits(tokenConfig, chainMetadata);
logger.info('xERC20 Limits:', xERC20Limits); logger.info('xERC20 Limits:', xERC20Limits);
@ -490,6 +668,7 @@ async function checkWarpRouteMetrics(
} catch (e) { } catch (e) {
logger.error('Error checking xERC20 limits', e); logger.error('Error checking xERC20 limits', e);
} }
}
}, checkFrequency); }, checkFrequency);
} }

@ -23,23 +23,23 @@ type TokenPriceCacheEntry = {
}; };
class TokenPriceCache { class TokenPriceCache {
protected cache: Map<ChainName, TokenPriceCacheEntry>; protected cache: Map<string, TokenPriceCacheEntry>;
protected freshSeconds: number; protected freshSeconds: number;
protected evictionSeconds: number; protected evictionSeconds: number;
constructor(freshSeconds = 60, evictionSeconds = 3 * 60 * 60) { constructor(freshSeconds = 60, evictionSeconds = 3 * 60 * 60) {
this.cache = new Map<ChainName, TokenPriceCacheEntry>(); this.cache = new Map<string, TokenPriceCacheEntry>();
this.freshSeconds = freshSeconds; this.freshSeconds = freshSeconds;
this.evictionSeconds = evictionSeconds; this.evictionSeconds = evictionSeconds;
} }
put(chain: ChainName, price: number): void { put(id: string, price: number): void {
const now = new Date(); const now = new Date();
this.cache.set(chain, { timestamp: now, price }); this.cache.set(id, { timestamp: now, price });
} }
isFresh(chain: ChainName): boolean { isFresh(id: string): boolean {
const entry = this.cache.get(chain); const entry = this.cache.get(id);
if (!entry) return false; if (!entry) return false;
const expiryTime = new Date( const expiryTime = new Date(
@ -49,17 +49,17 @@ class TokenPriceCache {
return now < expiryTime; return now < expiryTime;
} }
fetch(chain: ChainName): number { fetch(id: string): number {
const entry = this.cache.get(chain); const entry = this.cache.get(id);
if (!entry) { 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( const evictionTime = new Date(
entry.timestamp.getTime() + 1000 * this.evictionSeconds, entry.timestamp.getTime() + 1000 * this.evictionSeconds,
); );
const now = new Date(); const now = new Date();
if (now > evictionTime) { 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; return entry.price;
} }
@ -97,20 +97,30 @@ export class CoinGeckoTokenPriceGetter implements TokenPriceGetter {
); );
} }
async getTokenPrice(chain: ChainName): Promise<number> { async getTokenPrice(
const [price] = await this.getTokenPrices([chain]); chain: ChainName,
currency: string = 'usd',
): Promise<number> {
const [price] = await this.getTokenPrices([chain], currency);
return price; return price;
} }
async getTokenExchangeRate( async getTokenExchangeRate(
base: ChainName, base: ChainName,
quote: ChainName, quote: ChainName,
currency: string = 'usd',
): Promise<number> { ): Promise<number> {
const [basePrice, quotePrice] = await this.getTokenPrices([base, quote]); const [basePrice, quotePrice] = await this.getTokenPrices(
[base, quote],
currency,
);
return basePrice / quotePrice; return basePrice / quotePrice;
} }
private async getTokenPrices(chains: ChainName[]): Promise<number[]> { private async getTokenPrices(
chains: ChainName[],
currency: string = 'usd',
): Promise<number[]> {
const isMainnet = chains.map((c) => !this.metadata[c].isTestnet); const isMainnet = chains.map((c) => !this.metadata[c].isTestnet);
const allMainnets = isMainnet.every((v) => v === true); const allMainnets = isMainnet.every((v) => v === true);
const allTestnets = isMainnet.every((v) => v === false); 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));
if (toQuery.length > 0) {
try {
await this.queryTokenPrices(toQuery);
} catch (e) {
rootLogger.warn('Failed to query token prices', e);
}
}
return chains.map((chain) => this.cache.fetch(chain));
}
private async queryTokenPrices(chains: ChainName[]): Promise<void> {
const currency = 'usd';
// The CoinGecko API expects, in some cases, IDs that do not match
// ChainNames.
const ids = chains.map( const ids = chains.map(
(chain) => this.metadata[chain].gasCurrencyCoinGeckoId || chain, (chain) => this.metadata[chain].gasCurrencyCoinGeckoId || chain,
); );
// Coingecko rate limits, so we are adding this sleep
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<number[] | undefined> {
const toQuery = ids.filter((id) => !this.cache.isFresh(id));
await sleep(this.sleepMsBetweenRequests); await sleep(this.sleepMsBetweenRequests);
const response = await this.coinGecko.simple.price({
ids, if (toQuery.length > 0) {
let response: any;
try {
response = await this.coinGecko.simple.price({
ids: toQuery,
vs_currencies: [currency], vs_currencies: [currency],
}); });
const prices = ids.map((id) => response.data[id][currency]);
// Update the cache with the newly fetched prices if (response.success === true) {
chains.map((chain, i) => this.cache.put(chain, prices[i])); 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('Error when querying token prices', e);
return undefined;
}
}
return ids.map((id) => this.cache.fetch(id));
} }
} }

@ -10,6 +10,7 @@ const TokenConfigSchema = z.object({
type: z.nativeEnum(TokenType), type: z.nativeEnum(TokenType),
hypAddress: z.string(), // HypERC20Collateral, HypERC20Synthetic, HypNativeToken address hypAddress: z.string(), // HypERC20Collateral, HypERC20Synthetic, HypNativeToken address
tokenAddress: z.string().optional(), // external token address needed for collateral type eg tokenAddress.balanceOf(hypAddress) 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(), name: z.string(),
symbol: z.string(), symbol: z.string(),
decimals: z.number(), decimals: z.number(),

@ -7971,7 +7971,7 @@ __metadata:
"@ethersproject/providers": "npm:^5.7.2" "@ethersproject/providers": "npm:^5.7.2"
"@google-cloud/secret-manager": "npm:^5.5.0" "@google-cloud/secret-manager": "npm:^5.5.0"
"@hyperlane-xyz/helloworld": "npm:5.6.1" "@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/sdk": "npm:5.6.1"
"@hyperlane-xyz/utils": "npm:5.6.1" "@hyperlane-xyz/utils": "npm:5.6.1"
"@inquirer/prompts": "npm:^5.3.8" "@inquirer/prompts": "npm:^5.3.8"
@ -8029,6 +8029,16 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft 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": "@hyperlane-xyz/registry@npm:4.7.0":
version: 4.7.0 version: 4.7.0
resolution: "@hyperlane-xyz/registry@npm:4.7.0" resolution: "@hyperlane-xyz/registry@npm:4.7.0"

Loading…
Cancel
Save