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 10 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:
- ./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 }}

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

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

@ -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<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')
.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<boolean> {
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<boolean> {
// TODO: see issue https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2708
async function checkBalance(
tokenConfig: WarpTokenConfig,
tokenConfig: WarpRouteConfig,
multiProtocolProvider: MultiProtocolProvider,
): Promise<ChainMap<number>> {
const output: ChainMap<Promise<number>> = 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<number>,
) {
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]);

@ -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 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<T>(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)) {

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

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