From 54aeb6420babcea44b9e84a2a1398b3a5300ba1a Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 6 Feb 2024 11:30:58 -0500 Subject: [PATCH] feat/CLI tool for monitoring warp route balances (#3231) ### Description Replay https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/3204 on main Co-authored-by: -f --- .changeset/six-planets-love.md | 6 + .../warp/injective-inevm-deployments.yaml | 22 ++++ .../warp/nautilus-solana-bsc-deployments.yaml | 31 +++++ .../neutron-mantapacific-deployments.yaml | 22 ++++ .../helm/warp-routes/templates/_helpers.tpl | 6 +- .../warp-routes/deploy-warp-monitor.ts | 14 +- typescript/infra/scripts/warp-routes/helm.ts | 22 ++-- .../monitor-warp-routes-balances.ts | 120 ++++++++++-------- .../infra/src/config/grafana_token_config.ts | 104 --------------- typescript/infra/src/utils/utils.ts | 5 + typescript/sdk/src/index.ts | 6 +- .../sdk/src/metadata/warpRouteConfig.ts | 27 ++++ 12 files changed, 213 insertions(+), 172 deletions(-) create mode 100644 .changeset/six-planets-love.md create mode 100644 typescript/infra/config/environments/mainnet3/warp/injective-inevm-deployments.yaml create mode 100644 typescript/infra/config/environments/mainnet3/warp/nautilus-solana-bsc-deployments.yaml create mode 100644 typescript/infra/config/environments/mainnet3/warp/neutron-mantapacific-deployments.yaml delete mode 100644 typescript/infra/src/config/grafana_token_config.ts create mode 100644 typescript/sdk/src/metadata/warpRouteConfig.ts diff --git a/.changeset/six-planets-love.md b/.changeset/six-planets-love.md new file mode 100644 index 0000000000..e8d1e62e34 --- /dev/null +++ b/.changeset/six-planets-love.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/infra': minor +'@hyperlane-xyz/sdk': minor +--- + +Added warp route artifacts type adopting registry schema diff --git a/typescript/infra/config/environments/mainnet3/warp/injective-inevm-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/injective-inevm-deployments.yaml new file mode 100644 index 0000000000..efabaf59da --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/warp/injective-inevm-deployments.yaml @@ -0,0 +1,22 @@ +# Configs and artifacts for the deployment of Hyperlane Warp Routes +# Between injective and inevm +description: Hyperlane Warp Route artifacts +timestamp: '2024-01-31T16:00:00.000Z' +deployer: Abacus Works (Hyperlane) +data: + config: + injective: + protocolType: cosmos + type: native + hypAddress: inj1mv9tjvkaw7x8w8y9vds8pkfq46g2vcfkjehc6k + name: Injective Coin + symbol: INJ + decimals: 18 + ibcDenom: inj + inevm: + protocolType: ethereum + type: native + hypAddress: '0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4' + name: Injective coin + symbol: INJ + decimals: 18 diff --git a/typescript/infra/config/environments/mainnet3/warp/nautilus-solana-bsc-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/nautilus-solana-bsc-deployments.yaml new file mode 100644 index 0000000000..e3e8770078 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/warp/nautilus-solana-bsc-deployments.yaml @@ -0,0 +1,31 @@ +# Configs and artifacts for the deployment of Hyperlane Warp Routes +# Between nautilus and bsc, solana +description: Hyperlane Warp Route artifacts +timestamp: '2023-09-23T16:00:00.000Z' +deployer: Abacus Works (Hyperlane) +data: + config: + bsc: + protocolType: ethereum + type: collateral + hypAddress: '0xC27980812E2E66491FD457D488509b7E04144b98' + tokenAddress: '0x37a56cdcD83Dce2868f721De58cB3830C44C6303' + name: Zebec + symbol: ZBC + decimals: 9 + nautilus: + protocolType: ethereum + type: native + hypAddress: '0x4501bBE6e731A4bC5c60C03A77435b2f6d5e9Fe7' + name: Zebec + symbol: ZBC + decimals: 18 + solana: + protocolType: sealevel + type: collateral + tokenAddress: 'wzbcJyhGhQDLTV1S99apZiiBdE4jmYfbw99saMMdP59' + hypAddress: 'EJqwFjvVJSAxH8Ur2PYuMfdvoJeutjmH6GkoEFQ4MdSa' + name: Zebec + symbol: ZBC + decimals: 9 + isSpl2022: true diff --git a/typescript/infra/config/environments/mainnet3/warp/neutron-mantapacific-deployments.yaml b/typescript/infra/config/environments/mainnet3/warp/neutron-mantapacific-deployments.yaml new file mode 100644 index 0000000000..ae13acf3d7 --- /dev/null +++ b/typescript/infra/config/environments/mainnet3/warp/neutron-mantapacific-deployments.yaml @@ -0,0 +1,22 @@ +# Configs and artifacts for the deployment of Hyperlane Warp Routes +# Between neutron and mantapacific +description: Hyperlane Warp Route artifacts +timestamp: '2023-09-23T16:00:00.000Z' +deployer: Abacus Works (Hyperlane) +data: + config: + neutron: + protocolType: cosmos + type: collateral + hypAddress: neutron1ch7x3xgpnj62weyes8vfada35zff6z59kt2psqhnx9gjnt2ttqdqtva3pa + tokenAddress: ibc/773B4D0A3CD667B2275D5A4A7A2F0909C0BA0F4059C0B9181E680DDF4965DCC7 + name: Celestia + symbol: TIA + decimals: 6 + mantapacific: + protocolType: ethereum + type: synthetic + hypAddress: '0x6Fae4D9935E2fcb11fC79a64e917fb2BF14DaFaa' + name: Celestia + symbol: TIA + decimals: 6 diff --git a/typescript/infra/helm/warp-routes/templates/_helpers.tpl b/typescript/infra/helm/warp-routes/templates/_helpers.tpl index 338d8ad502..285a5842b8 100644 --- a/typescript/infra/helm/warp-routes/templates/_helpers.tpl +++ b/typescript/infra/helm/warp-routes/templates/_helpers.tpl @@ -61,8 +61,8 @@ The warp-routes container command: - ./node_modules/.bin/ts-node - ./typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts - - -l + - -v - "10000" - - -c - - {{ .Values.config }} + - -f + - {{ .Values.configFilePath }} {{- end }} diff --git a/typescript/infra/scripts/warp-routes/deploy-warp-monitor.ts b/typescript/infra/scripts/warp-routes/deploy-warp-monitor.ts index 534267004f..4d903b2c38 100644 --- a/typescript/infra/scripts/warp-routes/deploy-warp-monitor.ts +++ b/typescript/infra/scripts/warp-routes/deploy-warp-monitor.ts @@ -1,12 +1,24 @@ +import yargs from 'yargs'; + import { HelmCommand } from '../../src/utils/helm'; import { runWarpRouteHelmCommand } from './helm'; async function main() { + const { filePath } = await yargs(process.argv.slice(2)) + .alias('f', 'filePath') + .describe( + 'filePath', + 'indicate the filepatch to the warp route yaml file relative to typescript/infra', + ) + .demandOption('filePath') + .string('filePath') + .parse(); + await runWarpRouteHelmCommand( HelmCommand.InstallOrUpgrade, 'mainnet3', - 'neutron', + filePath, ); } diff --git a/typescript/infra/scripts/warp-routes/helm.ts b/typescript/infra/scripts/warp-routes/helm.ts index 170800e003..3e489d1d71 100644 --- a/typescript/infra/scripts/warp-routes/helm.ts +++ b/typescript/infra/scripts/warp-routes/helm.ts @@ -6,32 +6,32 @@ import { assertCorrectKubeContext, getEnvironmentConfig } from '../utils'; export async function runWarpRouteHelmCommand( helmCommand: HelmCommand, runEnv: DeployEnvironment, - config: string, + configFilePath: string, ) { const envConfig = getEnvironmentConfig(runEnv); await assertCorrectKubeContext(envConfig); - const values = getWarpRoutesHelmValues(config); - + const values = getWarpRoutesHelmValues(configFilePath); + const releaseName = getHelmReleaseName(configFilePath); return execCmd( - `helm ${helmCommand} ${getHelmReleaseName( - config, - )} ./helm/warp-routes --namespace ${runEnv} ${values.join( + `helm ${helmCommand} ${releaseName} ./helm/warp-routes --namespace ${runEnv} ${values.join( ' ', - )} --set fullnameOverride="${getHelmReleaseName(config)}"`, + )} --set fullnameOverride="${releaseName}"`, ); } function getHelmReleaseName(route: string): string { - return `hyperlane-warp-route-${route}`; + const match = route.match(/\/([^/]+)-deployments\.yaml$/); + const name = match ? match[1] : route; + return `hyperlane-warp-route-${name}`; } -function getWarpRoutesHelmValues(config: string) { +function getWarpRoutesHelmValues(configFilePath: string) { const values = { image: { repository: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: 'ae8ce44-20231101-012032', + tag: 'a84e439-20240131-224743', }, - config: config, // nautilus or neutron + configFilePath: configFilePath, }; return helmifyValues(values); } 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 b2631a87cd..dd96d83ec1 100644 --- a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts +++ b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts @@ -7,10 +7,13 @@ import { ERC20__factory } from '@hyperlane-xyz/core'; import { ChainMap, ChainName, + CosmNativeTokenAdapter, CwNativeTokenAdapter, MultiProtocolProvider, SealevelHypCollateralAdapter, TokenType, + WarpRouteConfig, + WarpRouteConfigSchema, } from '@hyperlane-xyz/sdk'; import { ProtocolType, @@ -19,12 +22,8 @@ import { promiseObjAll, } from '@hyperlane-xyz/utils'; -import { - WarpTokenConfig, - nautilusList, - neutronList, -} from '../../src/config/grafana_token_config'; import { startMetricsServer } from '../../src/utils/metrics'; +import { readYaml } from '../../src/utils/utils'; const metricsRegister = new Registry(); const warpRouteTokenBalance = new Gauge({ @@ -40,20 +39,36 @@ const warpRouteTokenBalance = new Gauge({ ], }); +export function readWarpRouteConfig(filePath: string) { + const config = readYaml(filePath); + if (!config) throw new Error(`No warp config found at ${filePath}`); + const result = WarpRouteConfigSchema.safeParse(config); + if (!result.success) { + const errorMessages = result.error.issues.map( + (issue: any) => `${issue.path} => ${issue.message}`, + ); + throw new Error(`Invalid warp config:\n ${errorMessages.join('\n')}`); + } + return result.data; +} + async function main(): Promise { - const { checkFrequency, config } = await yargs(process.argv.slice(2)) + const { checkFrequency, filePath } = await yargs(process.argv.slice(2)) .describe('checkFrequency', 'frequency to check balances in ms') .demandOption('checkFrequency') - .alias('l', 'checkFrequency') + .alias('v', 'checkFrequency') // v as in Greek letter nu .number('checkFrequency') - .alias('c', 'config') - .describe('config', 'choose warp token config') - .demandOption('config') - .choices('config', ['neutron', 'nautilus']) + .alias('f', 'filePath') + .describe( + 'filePath', + 'indicate the filepatch to the warp route yaml file relative to typescript/infra', + ) + .demandOption('filePath') + .string('filePath') .parse(); - const tokenList: WarpTokenConfig = - config === 'neutron' ? neutronList : nautilusList; + const tokenConfig: WarpRouteConfig = + readWarpRouteConfig(filePath).data.config; startMetricsServer(metricsRegister); @@ -63,8 +78,8 @@ async function main(): Promise { setInterval(async () => { try { debug('Checking balances'); - const balances = await checkBalance(tokenList, multiProtocolProvider); - updateTokenBalanceMetrics(tokenList, balances); + const balances = await checkBalance(tokenConfig, multiProtocolProvider); + updateTokenBalanceMetrics(tokenConfig, balances); } catch (e) { console.error('Error checking balances', e); } @@ -74,20 +89,18 @@ async function main(): Promise { // TODO: see issue https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2708 async function checkBalance( - tokenConfig: WarpTokenConfig, + tokenConfig: WarpRouteConfig, multiProtocolProvider: MultiProtocolProvider, ): Promise> { - const output: ChainMap> = objMap( + const output = objMap( tokenConfig, - async (chain: ChainName, token: WarpTokenConfig[ChainName]) => { + async (chain: ChainName, token: WarpRouteConfig[ChainName]) => { switch (token.type) { case TokenType.native: { switch (token.protocolType) { case ProtocolType.Ethereum: { const provider = multiProtocolProvider.getEthersV5Provider(chain); - const nativeBalance = await provider.getBalance( - token.hypNativeAddress, - ); + const nativeBalance = await provider.getBalance(token.hypAddress); return parseFloat( ethers.utils.formatUnits(nativeBalance, token.decimals), ); @@ -95,9 +108,20 @@ async function checkBalance( case ProtocolType.Sealevel: // TODO - solana native return 0; - case ProtocolType.Cosmos: - // TODO - cosmos native - return 0; + case ProtocolType.Cosmos: { + if (!token.ibcDenom) + throw new Error('IBC denom missing for native token'); + const adapter = new CosmNativeTokenAdapter( + chain, + multiProtocolProvider, + {}, + { ibcDenom: token.ibcDenom }, + ); + const tokenBalance = await adapter.getBalance(token.hypAddress); + return parseFloat( + ethers.utils.formatUnits(tokenBalance, token.decimals), + ); + } } break; } @@ -105,12 +129,14 @@ async function checkBalance( switch (token.protocolType) { case ProtocolType.Ethereum: { const provider = multiProtocolProvider.getEthersV5Provider(chain); + if (!token.tokenAddress) + throw new Error('Token address missing for collateral token'); const tokenContract = ERC20__factory.connect( - token.address, + token.tokenAddress, provider, ); const collateralBalance = await tokenContract.balanceOf( - token.hypCollateralAddress, + token.hypAddress, ); return parseFloat( @@ -118,19 +144,21 @@ async function checkBalance( ); } case ProtocolType.Sealevel: { + if (!token.tokenAddress) + throw new Error('Token address missing for synthetic token'); const adapter = new SealevelHypCollateralAdapter( chain, multiProtocolProvider, { - token: token.address, - warpRouter: token.hypCollateralAddress, + token: token.tokenAddress, + warpRouter: token.hypAddress, // Mailbox only required for transfers, using system as placeholder mailbox: SystemProgram.programId.toBase58(), }, - token.isSpl2022, + token?.isSpl2022 ?? false, ); const collateralBalance = ethers.BigNumber.from( - await adapter.getBalance(token.hypCollateralAddress), + await adapter.getBalance(token.hypAddress), ); return parseFloat( ethers.utils.formatUnits(collateralBalance, token.decimals), @@ -141,12 +169,12 @@ async function checkBalance( chain, multiProtocolProvider, { - token: token.address, + token: token.hypAddress, }, - token.address, + token.tokenAddress, ); const collateralBalance = ethers.BigNumber.from( - await adapter.getBalance(token.hypCollateralAddress), + await adapter.getBalance(token.hypAddress), ); return parseFloat( ethers.utils.formatUnits(collateralBalance, token.decimals), @@ -160,7 +188,7 @@ async function checkBalance( case ProtocolType.Ethereum: { const provider = multiProtocolProvider.getEthersV5Provider(chain); const tokenContract = ERC20__factory.connect( - token.hypSyntheticAddress, + token.hypAddress, provider, ); const syntheticBalance = await tokenContract.totalSupply(); @@ -178,36 +206,24 @@ async function checkBalance( break; } } + return 0; }, ); return await promiseObjAll(output); } -function updateTokenBalanceMetrics( - tokenConfig: WarpTokenConfig, +export function updateTokenBalanceMetrics( + tokenConfig: WarpRouteConfig, balances: ChainMap, ) { - objMap(tokenConfig, (chain: ChainName, token: WarpTokenConfig[ChainName]) => { - const tokenAddress = - token.type === TokenType.native - ? ethers.constants.AddressZero - : token.type === TokenType.collateral - ? token.address - : token.hypSyntheticAddress; - const walletAddress = - token.type === TokenType.native - ? token.hypNativeAddress - : token.type === TokenType.collateral - ? token.hypCollateralAddress - : token.hypSyntheticAddress; - + objMap(tokenConfig, (chain: ChainName, token: WarpRouteConfig[ChainName]) => { warpRouteTokenBalance .labels({ chain_name: chain, - token_address: tokenAddress, + token_address: token.tokenAddress ?? ethers.constants.AddressZero, token_name: token.name, - wallet_address: walletAddress, + wallet_address: token.hypAddress, token_type: token.type, }) .set(balances[chain]); diff --git a/typescript/infra/src/config/grafana_token_config.ts b/typescript/infra/src/config/grafana_token_config.ts deleted file mode 100644 index 9d7e6f4c34..0000000000 --- a/typescript/infra/src/config/grafana_token_config.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { ChainMap, TokenType } from '@hyperlane-xyz/sdk'; -import { ProtocolType } from '@hyperlane-xyz/utils'; - -interface NativeTokenConfig { - symbol: string; - name: string; - type: TokenType.native; - decimals: number; - hypNativeAddress: string; - protocolType: - | ProtocolType.Ethereum - | ProtocolType.Sealevel - | ProtocolType.Cosmos; -} - -interface CollateralTokenConfig { - type: TokenType.collateral; - address: string; - decimals: number; - symbol: string; - name: string; - hypCollateralAddress: string; - isSpl2022?: boolean; - protocolType: - | ProtocolType.Ethereum - | ProtocolType.Sealevel - | ProtocolType.Cosmos; -} - -interface SyntheticTokenConfig { - type: TokenType.synthetic; - hypSyntheticAddress: string; - decimals: number; - symbol: string; - name: string; - protocolType: - | ProtocolType.Ethereum - | ProtocolType.Sealevel - | ProtocolType.Cosmos; -} - -// TODO: migrate and dedupe to SDK from infra and Warp UI -export type WarpTokenConfig = ChainMap< - CollateralTokenConfig | NativeTokenConfig | SyntheticTokenConfig ->; - -/// nautilus configs -export const nautilusList: WarpTokenConfig = { - // bsc - bsc: { - type: TokenType.collateral, - address: '0x37a56cdcD83Dce2868f721De58cB3830C44C6303', - hypCollateralAddress: '0xC27980812E2E66491FD457D488509b7E04144b98', - symbol: 'ZBC', - name: 'Zebec', - decimals: 9, - protocolType: ProtocolType.Ethereum, - }, - - // nautilus - nautilus: { - type: TokenType.native, - hypNativeAddress: '0x4501bBE6e731A4bC5c60C03A77435b2f6d5e9Fe7', - symbol: 'ZBC', - name: 'Zebec', - decimals: 18, - protocolType: ProtocolType.Ethereum, - }, - - // solana - solana: { - type: TokenType.collateral, - address: 'wzbcJyhGhQDLTV1S99apZiiBdE4jmYfbw99saMMdP59', - hypCollateralAddress: 'EJqwFjvVJSAxH8Ur2PYuMfdvoJeutjmH6GkoEFQ4MdSa', - name: 'Zebec', - symbol: 'ZBC', - decimals: 9, - isSpl2022: false, - protocolType: ProtocolType.Sealevel, - }, -}; - -/// neutron configs -export const neutronList: WarpTokenConfig = { - neutron: { - type: TokenType.collateral, - address: - 'ibc/773B4D0A3CD667B2275D5A4A7A2F0909C0BA0F4059C0B9181E680DDF4965DCC7', - hypCollateralAddress: - 'neutron1ch7x3xgpnj62weyes8vfada35zff6z59kt2psqhnx9gjnt2ttqdqtva3pa', - name: 'Celestia', - symbol: 'TIA', - decimals: 6, - protocolType: ProtocolType.Cosmos, - }, - mantapacific: { - type: TokenType.synthetic, - hypSyntheticAddress: '0x6Fae4D9935E2fcb11fC79a64e917fb2BF14DaFaa', - name: 'Celestia', - symbol: 'TIA', - decimals: 6, - protocolType: ProtocolType.Ethereum, - }, -}; diff --git a/typescript/infra/src/utils/utils.ts b/typescript/infra/src/utils/utils.ts index 003cfed145..0b6d8a410e 100644 --- a/typescript/infra/src/utils/utils.ts +++ b/typescript/infra/src/utils/utils.ts @@ -4,6 +4,7 @@ import { exec } from 'child_process'; import { ethers } from 'ethers'; import fs from 'fs'; import path from 'path'; +import { parse as yamlParse } from 'yaml'; import { AllChains, @@ -200,6 +201,10 @@ export function readJSON(directory: string, filename: string) { return readJSONAtPath(path.join(directory, filename)); } +export function readYaml(filepath: string): T { + return yamlParse(readFileAtPath(filepath)) as T; +} + export function assertRole(roleStr: string) { const role = roleStr as Role; if (!Object.values(Role).includes(role)) { diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index d9dc0501e2..87b03652f2 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -185,8 +185,8 @@ export { RpcUrlSchema, getChainIdNumber, getDomainId, - isValidChainMetadata, getReorgPeriod, + isValidChainMetadata, } from './metadata/chainMetadataTypes'; export { ZHash } from './metadata/customZodTypes'; export { @@ -194,6 +194,10 @@ export { HyperlaneDeploymentArtifactsSchema, } from './metadata/deploymentArtifacts'; export { MatchingList } from './metadata/matchingList'; +export { + WarpRouteConfig, + WarpRouteConfigSchema, +} from './metadata/warpRouteConfig'; export { InterchainAccount } from './middleware/account/InterchainAccount'; export { InterchainAccountChecker } from './middleware/account/InterchainAccountChecker'; export { diff --git a/typescript/sdk/src/metadata/warpRouteConfig.ts b/typescript/sdk/src/metadata/warpRouteConfig.ts new file mode 100644 index 0000000000..e8028c8155 --- /dev/null +++ b/typescript/sdk/src/metadata/warpRouteConfig.ts @@ -0,0 +1,27 @@ +import { z } from 'zod'; + +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { TokenType } from '../token/config'; +import { ChainMap } from '../types'; + +const TokenConfigSchema = z.object({ + protocolType: z.nativeEnum(ProtocolType), + 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) + name: z.string(), + symbol: z.string(), + decimals: z.number(), + isSpl2022: z.boolean().optional(), // Solana Program Library 2022, sealevel specific + ibcDenom: z.string().optional(), // IBC denom for cosmos native token +}); + +export const WarpRouteConfigSchema = z.object({ + description: z.string().optional(), + timeStamp: z.string().optional(), // can make it non-optional if we make it part of the warp route deployment progress + deployer: z.string().optional(), + data: z.object({ config: z.record(TokenConfigSchema) }), +}); + +export type WarpRouteConfig = ChainMap>;