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 <kunalarora1729@gmail.com>
pull/3237/head
Yorke Rhodes 9 months ago committed by GitHub
parent fa058a0e57
commit 54aeb6420b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/six-planets-love.md
  2. 22
      typescript/infra/config/environments/mainnet3/warp/injective-inevm-deployments.yaml
  3. 31
      typescript/infra/config/environments/mainnet3/warp/nautilus-solana-bsc-deployments.yaml
  4. 22
      typescript/infra/config/environments/mainnet3/warp/neutron-mantapacific-deployments.yaml
  5. 6
      typescript/infra/helm/warp-routes/templates/_helpers.tpl
  6. 14
      typescript/infra/scripts/warp-routes/deploy-warp-monitor.ts
  7. 22
      typescript/infra/scripts/warp-routes/helm.ts
  8. 120
      typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts
  9. 104
      typescript/infra/src/config/grafana_token_config.ts
  10. 5
      typescript/infra/src/utils/utils.ts
  11. 6
      typescript/sdk/src/index.ts
  12. 27
      typescript/sdk/src/metadata/warpRouteConfig.ts

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/infra': minor
'@hyperlane-xyz/sdk': minor
---
Added warp route artifacts type adopting registry schema

@ -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

@ -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

@ -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

@ -61,8 +61,8 @@ The warp-routes container
command: command:
- ./node_modules/.bin/ts-node - ./node_modules/.bin/ts-node
- ./typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts - ./typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts
- -l - -v
- "10000" - "10000"
- -c - -f
- {{ .Values.config }} - {{ .Values.configFilePath }}
{{- end }} {{- end }}

@ -1,12 +1,24 @@
import yargs from 'yargs';
import { HelmCommand } from '../../src/utils/helm'; import { HelmCommand } from '../../src/utils/helm';
import { runWarpRouteHelmCommand } from './helm'; import { runWarpRouteHelmCommand } from './helm';
async function main() { 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( await runWarpRouteHelmCommand(
HelmCommand.InstallOrUpgrade, HelmCommand.InstallOrUpgrade,
'mainnet3', 'mainnet3',
'neutron', filePath,
); );
} }

@ -6,32 +6,32 @@ import { assertCorrectKubeContext, getEnvironmentConfig } from '../utils';
export async function runWarpRouteHelmCommand( export async function runWarpRouteHelmCommand(
helmCommand: HelmCommand, helmCommand: HelmCommand,
runEnv: DeployEnvironment, runEnv: DeployEnvironment,
config: string, configFilePath: string,
) { ) {
const envConfig = getEnvironmentConfig(runEnv); const envConfig = getEnvironmentConfig(runEnv);
await assertCorrectKubeContext(envConfig); await assertCorrectKubeContext(envConfig);
const values = getWarpRoutesHelmValues(config); const values = getWarpRoutesHelmValues(configFilePath);
const releaseName = getHelmReleaseName(configFilePath);
return execCmd( return execCmd(
`helm ${helmCommand} ${getHelmReleaseName( `helm ${helmCommand} ${releaseName} ./helm/warp-routes --namespace ${runEnv} ${values.join(
config,
)} ./helm/warp-routes --namespace ${runEnv} ${values.join(
' ', ' ',
)} --set fullnameOverride="${getHelmReleaseName(config)}"`, )} --set fullnameOverride="${releaseName}"`,
); );
} }
function getHelmReleaseName(route: string): string { 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 = { const values = {
image: { image: {
repository: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', 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); return helmifyValues(values);
} }

@ -7,10 +7,13 @@ import { ERC20__factory } from '@hyperlane-xyz/core';
import { import {
ChainMap, ChainMap,
ChainName, ChainName,
CosmNativeTokenAdapter,
CwNativeTokenAdapter, CwNativeTokenAdapter,
MultiProtocolProvider, MultiProtocolProvider,
SealevelHypCollateralAdapter, SealevelHypCollateralAdapter,
TokenType, TokenType,
WarpRouteConfig,
WarpRouteConfigSchema,
} from '@hyperlane-xyz/sdk'; } from '@hyperlane-xyz/sdk';
import { import {
ProtocolType, ProtocolType,
@ -19,12 +22,8 @@ import {
promiseObjAll, promiseObjAll,
} from '@hyperlane-xyz/utils'; } from '@hyperlane-xyz/utils';
import {
WarpTokenConfig,
nautilusList,
neutronList,
} from '../../src/config/grafana_token_config';
import { startMetricsServer } from '../../src/utils/metrics'; import { startMetricsServer } from '../../src/utils/metrics';
import { readYaml } from '../../src/utils/utils';
const metricsRegister = new Registry(); const metricsRegister = new Registry();
const warpRouteTokenBalance = new Gauge({ 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<boolean> { async function main(): Promise<boolean> {
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') .describe('checkFrequency', 'frequency to check balances in ms')
.demandOption('checkFrequency') .demandOption('checkFrequency')
.alias('l', 'checkFrequency') .alias('v', 'checkFrequency') // v as in Greek letter nu
.number('checkFrequency') .number('checkFrequency')
.alias('c', 'config') .alias('f', 'filePath')
.describe('config', 'choose warp token config') .describe(
.demandOption('config') 'filePath',
.choices('config', ['neutron', 'nautilus']) 'indicate the filepatch to the warp route yaml file relative to typescript/infra',
)
.demandOption('filePath')
.string('filePath')
.parse(); .parse();
const tokenList: WarpTokenConfig = const tokenConfig: WarpRouteConfig =
config === 'neutron' ? neutronList : nautilusList; readWarpRouteConfig(filePath).data.config;
startMetricsServer(metricsRegister); startMetricsServer(metricsRegister);
@ -63,8 +78,8 @@ async function main(): Promise<boolean> {
setInterval(async () => { setInterval(async () => {
try { try {
debug('Checking balances'); debug('Checking balances');
const balances = await checkBalance(tokenList, multiProtocolProvider); const balances = await checkBalance(tokenConfig, multiProtocolProvider);
updateTokenBalanceMetrics(tokenList, balances); updateTokenBalanceMetrics(tokenConfig, balances);
} catch (e) { } catch (e) {
console.error('Error checking balances', e); console.error('Error checking balances', e);
} }
@ -74,20 +89,18 @@ async function main(): Promise<boolean> {
// TODO: see issue https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2708 // TODO: see issue https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2708
async function checkBalance( async function checkBalance(
tokenConfig: WarpTokenConfig, tokenConfig: WarpRouteConfig,
multiProtocolProvider: MultiProtocolProvider, multiProtocolProvider: MultiProtocolProvider,
): Promise<ChainMap<number>> { ): Promise<ChainMap<number>> {
const output: ChainMap<Promise<number>> = objMap( const output = objMap(
tokenConfig, tokenConfig,
async (chain: ChainName, token: WarpTokenConfig[ChainName]) => { async (chain: ChainName, token: WarpRouteConfig[ChainName]) => {
switch (token.type) { switch (token.type) {
case TokenType.native: { case TokenType.native: {
switch (token.protocolType) { switch (token.protocolType) {
case ProtocolType.Ethereum: { case ProtocolType.Ethereum: {
const provider = multiProtocolProvider.getEthersV5Provider(chain); const provider = multiProtocolProvider.getEthersV5Provider(chain);
const nativeBalance = await provider.getBalance( const nativeBalance = await provider.getBalance(token.hypAddress);
token.hypNativeAddress,
);
return parseFloat( return parseFloat(
ethers.utils.formatUnits(nativeBalance, token.decimals), ethers.utils.formatUnits(nativeBalance, token.decimals),
); );
@ -95,9 +108,20 @@ async function checkBalance(
case ProtocolType.Sealevel: case ProtocolType.Sealevel:
// TODO - solana native // TODO - solana native
return 0; return 0;
case ProtocolType.Cosmos: case ProtocolType.Cosmos: {
// TODO - cosmos native if (!token.ibcDenom)
return 0; 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; break;
} }
@ -105,12 +129,14 @@ async function checkBalance(
switch (token.protocolType) { switch (token.protocolType) {
case ProtocolType.Ethereum: { case ProtocolType.Ethereum: {
const provider = multiProtocolProvider.getEthersV5Provider(chain); const provider = multiProtocolProvider.getEthersV5Provider(chain);
if (!token.tokenAddress)
throw new Error('Token address missing for collateral token');
const tokenContract = ERC20__factory.connect( const tokenContract = ERC20__factory.connect(
token.address, token.tokenAddress,
provider, provider,
); );
const collateralBalance = await tokenContract.balanceOf( const collateralBalance = await tokenContract.balanceOf(
token.hypCollateralAddress, token.hypAddress,
); );
return parseFloat( return parseFloat(
@ -118,19 +144,21 @@ async function checkBalance(
); );
} }
case ProtocolType.Sealevel: { case ProtocolType.Sealevel: {
if (!token.tokenAddress)
throw new Error('Token address missing for synthetic token');
const adapter = new SealevelHypCollateralAdapter( const adapter = new SealevelHypCollateralAdapter(
chain, chain,
multiProtocolProvider, multiProtocolProvider,
{ {
token: token.address, token: token.tokenAddress,
warpRouter: token.hypCollateralAddress, warpRouter: token.hypAddress,
// Mailbox only required for transfers, using system as placeholder // Mailbox only required for transfers, using system as placeholder
mailbox: SystemProgram.programId.toBase58(), mailbox: SystemProgram.programId.toBase58(),
}, },
token.isSpl2022, token?.isSpl2022 ?? false,
); );
const collateralBalance = ethers.BigNumber.from( const collateralBalance = ethers.BigNumber.from(
await adapter.getBalance(token.hypCollateralAddress), await adapter.getBalance(token.hypAddress),
); );
return parseFloat( return parseFloat(
ethers.utils.formatUnits(collateralBalance, token.decimals), ethers.utils.formatUnits(collateralBalance, token.decimals),
@ -141,12 +169,12 @@ async function checkBalance(
chain, chain,
multiProtocolProvider, multiProtocolProvider,
{ {
token: token.address, token: token.hypAddress,
}, },
token.address, token.tokenAddress,
); );
const collateralBalance = ethers.BigNumber.from( const collateralBalance = ethers.BigNumber.from(
await adapter.getBalance(token.hypCollateralAddress), await adapter.getBalance(token.hypAddress),
); );
return parseFloat( return parseFloat(
ethers.utils.formatUnits(collateralBalance, token.decimals), ethers.utils.formatUnits(collateralBalance, token.decimals),
@ -160,7 +188,7 @@ async function checkBalance(
case ProtocolType.Ethereum: { case ProtocolType.Ethereum: {
const provider = multiProtocolProvider.getEthersV5Provider(chain); const provider = multiProtocolProvider.getEthersV5Provider(chain);
const tokenContract = ERC20__factory.connect( const tokenContract = ERC20__factory.connect(
token.hypSyntheticAddress, token.hypAddress,
provider, provider,
); );
const syntheticBalance = await tokenContract.totalSupply(); const syntheticBalance = await tokenContract.totalSupply();
@ -178,36 +206,24 @@ async function checkBalance(
break; break;
} }
} }
return 0;
}, },
); );
return await promiseObjAll(output); return await promiseObjAll(output);
} }
function updateTokenBalanceMetrics( export function updateTokenBalanceMetrics(
tokenConfig: WarpTokenConfig, tokenConfig: WarpRouteConfig,
balances: ChainMap<number>, balances: ChainMap<number>,
) { ) {
objMap(tokenConfig, (chain: ChainName, token: WarpTokenConfig[ChainName]) => { objMap(tokenConfig, (chain: ChainName, token: WarpRouteConfig[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;
warpRouteTokenBalance warpRouteTokenBalance
.labels({ .labels({
chain_name: chain, chain_name: chain,
token_address: tokenAddress, token_address: token.tokenAddress ?? ethers.constants.AddressZero,
token_name: token.name, token_name: token.name,
wallet_address: walletAddress, wallet_address: token.hypAddress,
token_type: token.type, token_type: token.type,
}) })
.set(balances[chain]); .set(balances[chain]);

@ -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,
},
};

@ -4,6 +4,7 @@ import { exec } from 'child_process';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { parse as yamlParse } from 'yaml';
import { import {
AllChains, AllChains,
@ -200,6 +201,10 @@ export function readJSON(directory: string, filename: string) {
return readJSONAtPath(path.join(directory, filename)); return readJSONAtPath(path.join(directory, filename));
} }
export function readYaml<T>(filepath: string): T {
return yamlParse(readFileAtPath(filepath)) as T;
}
export function assertRole(roleStr: string) { export function assertRole(roleStr: string) {
const role = roleStr as Role; const role = roleStr as Role;
if (!Object.values(Role).includes(role)) { if (!Object.values(Role).includes(role)) {

@ -185,8 +185,8 @@ export {
RpcUrlSchema, RpcUrlSchema,
getChainIdNumber, getChainIdNumber,
getDomainId, getDomainId,
isValidChainMetadata,
getReorgPeriod, getReorgPeriod,
isValidChainMetadata,
} from './metadata/chainMetadataTypes'; } from './metadata/chainMetadataTypes';
export { ZHash } from './metadata/customZodTypes'; export { ZHash } from './metadata/customZodTypes';
export { export {
@ -194,6 +194,10 @@ export {
HyperlaneDeploymentArtifactsSchema, HyperlaneDeploymentArtifactsSchema,
} from './metadata/deploymentArtifacts'; } from './metadata/deploymentArtifacts';
export { MatchingList } from './metadata/matchingList'; export { MatchingList } from './metadata/matchingList';
export {
WarpRouteConfig,
WarpRouteConfigSchema,
} from './metadata/warpRouteConfig';
export { InterchainAccount } from './middleware/account/InterchainAccount'; export { InterchainAccount } from './middleware/account/InterchainAccount';
export { InterchainAccountChecker } from './middleware/account/InterchainAccountChecker'; export { InterchainAccountChecker } from './middleware/account/InterchainAccountChecker';
export { export {

@ -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<z.infer<typeof TokenConfigSchema>>;
Loading…
Cancel
Save