feat: refactor warp monitor to use WarpCore config (#4835)

### Description

- Removes the warp monitor specific config format and all the
handcrafted yamls from infra
- Massive refactor to warp monitor to accommodate this -- tried to make
it a bit easier to work work by splitting it into a few files. The
changes are generally backward compatible with some small exceptions,
like now the metrics use the `TokenStandard` instead of `TokenType` (the
former is present in the warp core config, the latter isn't, and there's
no way atm to map from the former to the latter)
- The warp monitor now takes in a warp ID and gets its config from the
registry (recall this is the registry version defined in `.registryrc`
if running in the monorepo image)
- Deploying the warp monitor now lets you do many at a time and select
the ones to deploy interactively. This makes it a bit easier to work
with the warp IDs
- The names of the warp deploys are based on the warp IDs. Because warp
IDs can change as new chains are added, this may cause issues in the
future - we'll just need to be cautious of removing the old release to
avoid accidentally having 2 of them. For now I think this is fine. The
helm release names are also capped at 53 chars so they needed to be
truncated
- Some changes to the SDK to get the necessary data here. Happy to pull
some of these into the warp monitor if people feel they're inappropriate
for the SDK (like the `getBridgedSupply`)
- Added a test to make sure any warp route IDs in infra are found in the
registry

- Corresponding PR to the registry here
https://github.com/hyperlane-xyz/hyperlane-registry/pull/370

### Drive-by changes

<!--
Are there any minor or drive-by changes also included?
-->

### Related issues

<!--
- Fixes #[issue number here]
-->

### Backward compatibility

<!--
Are these changes backward compatible? Are there any infrastructure
implications, e.g. changes that would prohibit deploying older commits
using this infra tooling?

Yes/No
-->

### Testing

<!--
What kind of testing have these changes undergone?

None/Manual/Unit Tests
-->
pull/4846/head
Trevor Porter 2 weeks ago committed by GitHub
parent 0754d6218c
commit 9fcd592878
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .registryrc
  2. 28
      typescript/infra/config/environments/mainnet3/agent.ts
  3. 28
      typescript/infra/config/environments/mainnet3/warp/AMPHRETH-deployments.yaml
  4. 86
      typescript/infra/config/environments/mainnet3/warp/EZETH-deployments.yaml
  5. 24
      typescript/infra/config/environments/mainnet3/warp/TIA-eclipse-stride-deployments.yaml
  6. 23
      typescript/infra/config/environments/mainnet3/warp/ancient8-USDC-deployments.yaml
  7. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/addresses.json
  8. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/ancient8-USDC-addresses.json
  9. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/arbitrum-TIA-addresses.json
  10. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/arbitrum-neutron-eclip-addresses.json
  11. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/eclipse-stride-TIA-addresses.json
  12. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/eclipse-stride-stTIA-addresses.json
  13. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/inevm-USDC-addresses.json
  14. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/inevm-USDT-addresses.json
  15. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/injective-inevm-addresses.json
  16. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/manta-TIA-addresses.json
  17. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/merkly-erc20-addresses.json
  18. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/merkly-eth-addresses.json
  19. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/merkly-nft-addresses.json
  20. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/renzo-ezETH-addresses-v1.json
  21. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/renzo-ezETH-addresses-v3.json
  22. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/verification.json
  23. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/viction-ETH-addresses.json
  24. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/viction-USDC-addresses.json
  25. 0
      typescript/infra/config/environments/mainnet3/warp/artifacts/viction-USDT-addresses.json
  26. 30
      typescript/infra/config/environments/mainnet3/warp/bsc-lumia-LUMIA-deployments.yaml
  27. 24
      typescript/infra/config/environments/mainnet3/warp/eclipse-ORCA-deployments.yaml
  28. 24
      typescript/infra/config/environments/mainnet3/warp/eclipse-SOL-deployments.yaml
  29. 35
      typescript/infra/config/environments/mainnet3/warp/eclipse-USDC-deployments.yaml
  30. 34
      typescript/infra/config/environments/mainnet3/warp/eclipse-USDT-deployments.yaml
  31. 23
      typescript/infra/config/environments/mainnet3/warp/eclipse-WBTC-deployments.yaml
  32. 25
      typescript/infra/config/environments/mainnet3/warp/eclipse-WIF-deployments.yaml
  33. 24
      typescript/infra/config/environments/mainnet3/warp/eclipse-tETH-deployments.yaml
  34. 23
      typescript/infra/config/environments/mainnet3/warp/ethereumUSDC-inevm-deployments.yaml
  35. 23
      typescript/infra/config/environments/mainnet3/warp/ethereumUSDT-inevm-deployments.yaml
  36. 23
      typescript/infra/config/environments/mainnet3/warp/injective-inevm-deployments.yaml
  37. 31
      typescript/infra/config/environments/mainnet3/warp/nautilus-solana-bsc-deployments.yaml
  38. 23
      typescript/infra/config/environments/mainnet3/warp/neutron-mantapacific-deployments.yaml
  39. 22
      typescript/infra/config/environments/mainnet3/warp/sei-FASTUSD-deployments.yaml
  40. 24
      typescript/infra/config/environments/mainnet3/warp/stTIA-eclipse-stride-deployments.yaml
  41. 22
      typescript/infra/config/environments/mainnet3/warp/viction-ethereum-ETH-deployments.yaml
  42. 23
      typescript/infra/config/environments/mainnet3/warp/viction-ethereum-USDC-deployments.yaml
  43. 23
      typescript/infra/config/environments/mainnet3/warp/viction-ethereum-USDT-deployments.yaml
  44. 5
      typescript/infra/config/environments/mainnet3/warp/warpIds.ts
  45. 9
      typescript/infra/config/registry.ts
  46. 8
      typescript/infra/helm/warp-routes/templates/_helpers.tpl
  47. 4
      typescript/infra/helm/warp-routes/templates/env-var-external-secret.yaml
  48. 62
      typescript/infra/scripts/warp-routes/deploy-warp-monitor.ts
  49. 697
      typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts
  50. 105
      typescript/infra/scripts/warp-routes/monitor/metrics.ts
  51. 254
      typescript/infra/scripts/warp-routes/monitor/monitor-warp-route-balances.ts
  52. 11
      typescript/infra/scripts/warp-routes/monitor/types.ts
  53. 11
      typescript/infra/scripts/warp-routes/monitor/utils.ts
  54. 28
      typescript/infra/src/warp/helm.ts
  55. 15
      typescript/infra/test/warpIds.test.ts
  56. 3
      typescript/sdk/src/index.ts
  57. 11
      typescript/sdk/src/token/Token.ts
  58. 4
      typescript/sdk/src/token/TokenStandard.ts
  59. 18
      typescript/sdk/src/token/adapters/CosmWasmTokenAdapter.ts
  60. 10
      typescript/sdk/src/token/adapters/CosmosTokenAdapter.ts
  61. 112
      typescript/sdk/src/token/adapters/EvmTokenAdapter.ts
  62. 5
      typescript/sdk/src/token/adapters/ITokenAdapter.ts
  63. 29
      typescript/sdk/src/token/adapters/SealevelTokenAdapter.ts

@ -1 +1 @@
d71eb5f42616998f77ce01079fd06a8e118966f7 10fdae0f566c7c213b6b1b27b8620e8776f2895f

@ -24,20 +24,20 @@ import {
supportedChainNames, supportedChainNames,
} from './supportedChainNames.js'; } from './supportedChainNames.js';
import { validatorChainConfig } from './validators.js'; import { validatorChainConfig } from './validators.js';
import ancient8EthereumUsdcAddresses from './warp/ancient8-USDC-addresses.json'; import ancient8EthereumUsdcAddresses from './warp/artifacts/ancient8-USDC-addresses.json';
import arbitrumTIAAddresses from './warp/arbitrum-TIA-addresses.json'; import arbitrumTIAAddresses from './warp/artifacts/arbitrum-TIA-addresses.json';
import arbitrumNeutronEclipAddresses from './warp/arbitrum-neutron-eclip-addresses.json'; import arbitrumNeutronEclipAddresses from './warp/artifacts/arbitrum-neutron-eclip-addresses.json';
import eclipseStrideTiaAddresses from './warp/eclipse-stride-TIA-addresses.json'; import eclipseStrideTiaAddresses from './warp/artifacts/eclipse-stride-TIA-addresses.json';
import eclipseStrideStTiaAddresses from './warp/eclipse-stride-stTIA-addresses.json'; import eclipseStrideStTiaAddresses from './warp/artifacts/eclipse-stride-stTIA-addresses.json';
import inevmEthereumUsdcAddresses from './warp/inevm-USDC-addresses.json'; import inevmEthereumUsdcAddresses from './warp/artifacts/inevm-USDC-addresses.json';
import inevmEthereumUsdtAddresses from './warp/inevm-USDT-addresses.json'; import inevmEthereumUsdtAddresses from './warp/artifacts/inevm-USDT-addresses.json';
import injectiveInevmInjAddresses from './warp/injective-inevm-addresses.json'; import injectiveInevmInjAddresses from './warp/artifacts/injective-inevm-addresses.json';
import mantaTIAAddresses from './warp/manta-TIA-addresses.json'; import mantaTIAAddresses from './warp/artifacts/manta-TIA-addresses.json';
import merklyEthAddresses from './warp/merkly-eth-addresses.json'; import merklyEthAddresses from './warp/artifacts/merkly-eth-addresses.json';
import renzoEzEthAddressesV3 from './warp/renzo-ezETH-addresses-v3.json'; import renzoEzEthAddressesV3 from './warp/artifacts/renzo-ezETH-addresses-v3.json';
import victionEthereumEthAddresses from './warp/viction-ETH-addresses.json'; import victionEthereumEthAddresses from './warp/artifacts/viction-ETH-addresses.json';
import victionEthereumUsdcAddresses from './warp/viction-USDC-addresses.json'; import victionEthereumUsdcAddresses from './warp/artifacts/viction-USDC-addresses.json';
import victionEthereumUsdtAddresses from './warp/viction-USDT-addresses.json'; import victionEthereumUsdtAddresses from './warp/artifacts/viction-USDT-addresses.json';
import { WarpRouteIds } from './warp/warpIds.js'; import { WarpRouteIds } from './warp/warpIds.js';
// const releaseCandidateHelloworldMatchingList = routerMatchingList( // const releaseCandidateHelloworldMatchingList = routerMatchingList(

@ -1,28 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
description: Hyperlane Warp Route artifacts
timestamp: '2024-10-18T14:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
arbitrum:
protocolType: ethereum
type: synthetic
hypAddress: '0x6D251aADfc6Ff69031e01eA39bE3cb5BABf8438f'
name: Amphor Restaked ETH
symbol: AMPHRETH
decimals: 18
ethereum:
protocolType: ethereum
type: collateral
hypAddress: '0xdc89990a6fdC1C922b841f1d977835628A24Ed57'
tokenAddress: '0x5fD13359Ba15A84B76f7F87568309040176167cd'
name: Amphor Restaked ETH
symbol: AMPHRETH
decimals: 18
zircuit:
protocolType: ethereum
type: synthetic
hypAddress: '0x7D5a79539d7B1c9aE5e54d18EEE188840f1Fe4CC'
name: Amphor Restaked ETH
symbol: AMPHRETH
decimals: 18

@ -1,86 +0,0 @@
description: Hyperlane Warp Route artifacts
timestamp: '2024-06-04T16:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
ethereum:
protocolType: ethereum
type: xERC20Lockbox
name: Renzo Restaked ETH
symbol: ezETH
hypAddress: '0xC59336D8edDa9722B4f1Ec104007191Ec16f7087'
tokenAddress: '0xbf5495Efe5DB9ce00f80364C8B423567e58d2110'
tokenCoinGeckoId: renzo-restaked-eth
decimals: 18
bsc:
protocolType: ethereum
type: xERC20
name: Renzo Restaked ETH
symbol: ezETH
hypAddress: '0xE00C6185a5c19219F1FFeD213b4406a254968c26'
tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5'
decimals: 18
arbitrum:
protocolType: ethereum
type: xERC20
name: Renzo Restaked ETH
symbol: ezETH
hypAddress: '0xB26bBfC6d1F469C821Ea25099017862e7368F4E8'
tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5'
decimals: 18
optimism:
protocolType: ethereum
type: xERC20
name: Renzo Restaked ETH
symbol: ezETH
hypAddress: '0xacEB607CdF59EB8022Cc0699eEF3eCF246d149e2'
tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5'
decimals: 18
base:
protocolType: ethereum
type: xERC20
name: Renzo Restaked ETH
symbol: ezETH
hypAddress: '0x2552516453368e42705D791F674b312b8b87CD9e'
tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5'
decimals: 18
blast:
protocolType: ethereum
type: xERC20
name: Renzo Restaked ETH
symbol: ezETH
hypAddress: '0x486b39378f99f073A3043C6Aabe8666876A8F3C5'
tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5'
decimals: 18
mode:
protocolType: ethereum
type: xERC20
name: Renzo Restaked ETH
symbol: ezETH
hypAddress: '0xC59336D8edDa9722B4f1Ec104007191Ec16f7087'
tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5'
decimals: 18
linea:
protocolType: ethereum
type: xERC20
name: Renzo Restaked ETH
symbol: ezETH
hypAddress: '0xC59336D8edDa9722B4f1Ec104007191Ec16f7087'
tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5'
decimals: 18
fraxtal:
protocolType: ethereum
type: xERC20
name: Renzo Restaked ETH
symbol: ezETH
hypAddress: '0x3aE8635A4D581d40a6Edfb3f2ED480f9532994F5'
tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5'
decimals: 18
zircuit:
protocolType: ethereum
type: xERC20
name: Renzo Restaked ETH
symbol: ezETH
hypAddress: '0x2552516453368e42705D791F674b312b8b87CD9e'
tokenAddress: '0x2416092f143378750bb29b79eD961ab195CcEea5'
decimals: 18

@ -1,24 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
description: Hyperlane Warp Route artifacts
timestamp: '2024-10-18T14:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
stride:
protocolType: cosmos
type: collateral
hypAddress: stride1pvtesu3ve7qn7ctll2x495mrqf2ysp6fws68grvcu6f7n2ajghgsh2jdj6
tokenAddress: ibc/BF3B4F53F3694B66E13C23107C84B6485BD2B96296BB7EC680EA77BBA75B4801
tokenCoinGeckoId: celestia
name: Celestia
symbol: TIA
decimals: 6
eclipsemainnet:
protocolType: sealevel
type: synthetic
hypAddress: 'BpXHAiktwjx7fN6M9ST9wr6qKAsH27wZFhdHEhReJsR6'
tokenAddress: '9RryNMhAVJpAwAGjCAMKbbTFwgjapqPkzpGMfTQhEjf8'
isSpl2022: true
name: Turbo Eth
symbol: tETH
decimals: 6

@ -1,23 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
# Between ancient8 and Ethereum
description: Hyperlane Warp Route artifacts
timestamp: '2024-04-15T16:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
ethereum:
protocolType: ethereum
type: collateral
hypAddress: '0x8b4192B9Ad1fCa440A5808641261e5289e6de95D'
tokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' # USDC
tokenCoinGeckoId: usd-coin
name: USDC
symbol: USDC
decimals: 6
ancient8:
protocolType: ethereum
type: synthetic
hypAddress: '0x97423A68BAe94b5De52d767a17aBCc54c157c0E5'
name: USDC
symbol: USDC
decimals: 6

@ -1,30 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
# Between Ethereum and Binance Smart Chain and Lumia
description: Hyperlane Warp Route artifacts
timestamp: '2024-10-18T14:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
ethereum:
protocolType: ethereum
type: collateral
hypAddress: '0xdD313D475f8A9d81CBE2eA953a357f52e10BA357'
tokenAddress: '0xd9343a049d5dbd89cd19dc6bca8c48fb3a0a42a7'
tokenCoinGeckoId: lumia
name: Lumia Token
symbol: LUMIA
decimals: 18
bsc:
protocolType: ethereum
type: synthetic
hypAddress: '0x7F39BcdCa8E0E581c1d43aaa1cB862AA1c8C2047'
name: Lumia Token
symbol: LUMIA
decimals: 18
lumia:
protocolType: ethereum
type: native
hypAddress: '0x6a77331cE28E47c3Cb9Fea48AB6cD1e9594ce0A9'
name: Lumia Token
symbol: LUMIA
decimals: 18

@ -1,24 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
description: Hyperlane Warp Route artifacts
timestamp: '2024-09-19T16:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
solanamainnet:
protocolType: sealevel
type: collateral
hypAddress: '8acihSm2QTGswniKgdgr4JBvJihZ1cakfvbqWCPBLoSp'
tokenAddress: 'orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE'
tokenCoinGeckoId: orca
name: Orca
symbol: ORCA
decimals: 6
eclipsemainnet:
protocolType: sealevel
type: synthetic
hypAddress: '8CvWJS7SPtauAXinJUURkBDLsGUXWiiTdENkEFUPjQ9j'
tokenAddress: '2tGbYEm4nuPFyS6zjDTELzEhvVKizgKewi6xT7AaSKzn'
isSpl2022: true
name: Orca
symbol: ORCA
decimals: 6

@ -1,24 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
# Between injective and inevm
description: Hyperlane Warp Route artifacts
timestamp: '2024-09-19T16:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
solanamainnet:
protocolType: sealevel
type: native
hypAddress: '8DtAGQpcMuD5sG3KdxDy49ydqXUggR1LQtebh2TECbAc'
tokenCoinGeckoId: solana
name: Solana
symbol: SOL
decimals: 9
eclipsemainnet:
protocolType: sealevel
type: synthetic
hypAddress: 'FJu4E1BDYKVg7aTWdwATZRUvytJZ8ZZ2gQuvPfMWAz9y'
tokenAddress: 'BeRUj3h7BqkbdfFU7FBNYbodgf8GCHodzKvF9aVjNNfL'
isSpl2022: true
name: Solana
symbol: SOL
decimals: 9

@ -1,35 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
# Between injective and inevm
description: Hyperlane Warp Route artifacts
timestamp: '2024-09-19T16:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
ethereum:
protocolType: ethereum
type: collateral
hypAddress: '0xe1De9910fe71cC216490AC7FCF019e13a34481D7'
tokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' # USDC
tokenCoinGeckoId: usd-coin
name: USDC
symbol: USDC
decimals: 6
solanamainnet:
protocolType: sealevel
type: collateral
hypAddress: '3EpVCPUgyjq2MfGeCttyey6bs5zya5wjYZ2BE6yDg6bm'
tokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
tokenCoinGeckoId: usd-coin
isSpl2022: false
name: USDC
symbol: USDC
decimals: 6
eclipsemainnet:
protocolType: sealevel
type: synthetic
hypAddress: 'EqRSt9aUDMKYKhzd1DGMderr3KNp29VZH3x5P7LFTC8m'
tokenAddress: 'AKEWE7Bgh87GPp171b4cJPSSZfmZwQ3KaqYqXoKLNAEE'
isSpl2022: true
name: USDC
symbol: USDC
decimals: 6

@ -1,34 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
description: Hyperlane Warp Route artifacts
timestamp: '2024-09-19T16:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
ethereum:
protocolType: ethereum
type: collateral
hypAddress: '0x647C621CEb36853Ef6A907E397Adf18568E70543'
tokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7' # USDT
tokenCoinGeckoId: tether
name: USDT
symbol: USDT
decimals: 6
solanamainnet:
protocolType: sealevel
type: collateral
hypAddress: 'Bk79wMjvpPCh5iQcCEjPWFcG1V2TfgdwaBsWBEYFYSNU'
tokenAddress: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'
tokenCoinGeckoId: tether
isSpl2022: false
name: USDT
symbol: USDT
decimals: 6
eclipsemainnet:
protocolType: sealevel
type: synthetic
hypAddress: '5g5ujyYUNvdydwyDVCpZwPpgYRqH5RYJRi156cxyE3me'
tokenAddress: 'CEBP3CqAbW4zdZA57H2wfaSG1QNdzQ72GiQEbQXyW9Tm'
isSpl2022: true
name: USDT
symbol: USDT
decimals: 6

@ -1,23 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
description: Hyperlane Warp Route artifacts
timestamp: '2024-09-19T16:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
ethereum:
protocolType: ethereum
type: collateral
hypAddress: '0x5B4e223DE74ef8c3218e66EEcC541003CAB3121A'
tokenAddress: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'
name: WBTC
symbol: WBTC
decimals: 8
eclipsemainnet:
protocolType: sealevel
type: synthetic
hypAddress: 'A7EGCDYFw5R7Jfm6cYtKvY8dmkrYMgwRCJFkyQwpHTYu'
tokenAddress: '7UTjr1VC6Z9DPsWD6mh5wPzNtufN17VnzpKS3ASpfAji'
isSpl2022: true
name: WBTC
symbol: WBTC
decimals: 8

@ -1,25 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
# Between injective and inevm
description: Hyperlane Warp Route artifacts
timestamp: '2024-09-19T16:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
solanamainnet:
protocolType: sealevel
type: collateral
hypAddress: 'CuQmsT4eSF4dYiiGUGYYQxJ7c58pUAD5ADE3BbFGzQKx'
tokenAddress: 'EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm'
tokenCoinGeckoId: dogwifcoin
name: dogwifhat
symbol: WIF
decimals: 9
eclipsemainnet:
protocolType: sealevel
type: synthetic
hypAddress: '6tBGmKNxirxBYnm9khkaTtcr2fhJ9YviCgRm1RWM8NJv'
tokenAddress: '841P4tebEgNux2jaWSjCoi9LhrVr9eHGjLc758Va3RPH'
isSpl2022: false
name: dogwifhat
symbol: WIF
decimals: 9

@ -1,24 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
# Between injective and inevm
description: Hyperlane Warp Route artifacts
timestamp: '2024-09-19T16:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
ethereum:
protocolType: ethereum
type: collateral
hypAddress: '0xc2495f3183F043627CAECD56dAaa726e3B2D9c09'
tokenAddress: '0x19e099B7aEd41FA52718D780dDA74678113C0b32'
name: Turbo Eth
symbol: tETH
decimals: 18
eclipsemainnet:
protocolType: sealevel
type: synthetic
hypAddress: '6GM7hy6M6LjhadG1xuKXPQ3jPC1eieEszR8DforoRzUp'
tokenAddress: 'GU7NS9xCwgNPiAdJ69iusFrRfawjDDPjeMBovhV1d4kn'
isSpl2022: false
name: Turbo Eth
symbol: tETH
decimals: 9

@ -1,23 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
# Between injective and inevm
description: Hyperlane Warp Route artifacts
timestamp: '2024-02-06T16:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
ethereum:
protocolType: ethereum
type: collateral
hypAddress: '0xED56728fb977b0bBdacf65bCdD5e17Bb7e84504f'
tokenAddress: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' # USDC
tokenCoinGeckoId: usd-coin
name: USDC
symbol: USDC
decimals: 6
inevm:
protocolType: ethereum
type: synthetic
hypAddress: '0x8358D8291e3bEDb04804975eEa0fe9fe0fAfB147'
name: USDC
symbol: USDC
decimals: 6

@ -1,23 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
# Between injective and inevm
description: Hyperlane Warp Route artifacts
timestamp: '2024-02-06T16:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
ethereum:
protocolType: ethereum
type: collateral
hypAddress: '0xab852e67bf03E74C89aF67C4BA97dd1088D3dA19'
tokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7' # USDT
tokenCoinGeckoId: tether
name: Tether USD
symbol: USDT
decimals: 6
inevm:
protocolType: ethereum
type: synthetic
hypAddress: '0x97423A68BAe94b5De52d767a17aBCc54c157c0E5'
name: Tether USD
symbol: USDT
decimals: 6

@ -1,23 +0,0 @@
# 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
tokenCoinGeckoId: injective-protocol
name: Injective Coin
symbol: INJ
decimals: 18
ibcDenom: inj
inevm:
protocolType: ethereum
type: native
hypAddress: '0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4'
name: Injective coin
symbol: INJ
decimals: 18

@ -1,31 +0,0 @@
# 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

@ -1,23 +0,0 @@
# 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
tokenCoinGeckoId: celestia
name: Celestia
symbol: TIA
decimals: 6
mantapacific:
protocolType: ethereum
type: synthetic
hypAddress: '0x6Fae4D9935E2fcb11fC79a64e917fb2BF14DaFaa'
name: Celestia
symbol: TIA
decimals: 6

@ -1,22 +0,0 @@
description: Hyperlane Warp Route artifacts
timestamp: '2024-10-17T14:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
ethereum:
protocolType: ethereum
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
sei:
protocolType: ethereum
type: xERC20
hypAddress: '0xeA895A7Ff45d8d3857A04c1E38A362f3bd9a076f'
tokenAddress: '0x37a4dD9CED2b19Cfe8FAC251cd727b5787E45269'
name: fastUSD
symbol: fastUSD
decimals: 18

@ -1,24 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
description: Hyperlane Warp Route artifacts
timestamp: '2024-10-18T14:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
stride:
protocolType: cosmos
type: collateral
hypAddress: stride134axwdlam929m3mar3wv95nvkyep7mr87ravkqcpf8dfe3v0pjlqwrw6ee
tokenAddress: 'stutia'
tokenCoinGeckoId: stride-staked-tia
name: Stride Staked TIA
symbol: stTIA
decimals: 6
eclipsemainnet:
protocolType: sealevel
type: synthetic
hypAddress: 'tKUHyJ5NxhnwU94JUmzh1ekukDcHHX8mZF6fqxbMwX6'
tokenAddress: 'V5m1Cc9VK61mKL8xVYrjR7bjD2BC5VpADLa6ws3G8KM'
isSpl2022: true
name: Turbo Eth
symbol: tETH
decimals: 6

@ -1,22 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
# Between viction and ethereum, ETH
description: Hyperlane Warp Route artifacts
timestamp: '2023-02-14T20:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
ethereum:
protocolType: ethereum
type: native
hypAddress: '0x15b5D6B614242B118AA404528A7f3E2Ad241e4A4'
tokenCoinGeckoId: ethereum
name: Ether
symbol: ETH
decimals: 18
viction:
protocolType: ethereum
type: synthetic
hypAddress: '0x182E8d7c5F1B06201b102123FC7dF0EaeB445a7B'
name: ETH
symbol: ETH
decimals: 18

@ -1,23 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
# Between viction and ethereum, USDC
description: Hyperlane Warp Route artifacts
timestamp: '2023-02-14T20:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
ethereum:
protocolType: ethereum
type: collateral
hypAddress: '0x31Dca7762930f56D81292f85E65c9D67575804fE'
tokenAddress: '0x31Dca7762930f56D81292f85E65c9D67575804fE' # USDC
tokenCoinGeckoId: usd-coin
name: USD Coin
symbol: USDC
decimals: 6
viction:
protocolType: ethereum
type: synthetic
hypAddress: '0xBDa330Ea8F3005C421C8088e638fBB64fA71b9e0'
name: USDC
symbol: USDC
decimals: 6

@ -1,23 +0,0 @@
# Configs and artifacts for the deployment of Hyperlane Warp Routes
# Between viction and ethereum, USDT
description: Hyperlane Warp Route artifacts
timestamp: '2023-02-14T20:00:00.000Z'
deployer: Abacus Works (Hyperlane)
data:
config:
ethereum:
protocolType: ethereum
type: collateral
hypAddress: '0x4221a16A01F61c2b38A03C52d828a7041f6AAA49'
tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7' # USDT
tokenCoinGeckoId: tether
name: Tether USD
symbol: USDT
decimals: 6
viction:
protocolType: ethereum
type: synthetic
hypAddress: '0x48083c69f5a42c6b69abbad48ae195bd36770ee2'
name: USDT
symbol: USDT
decimals: 6

@ -9,10 +9,11 @@ export enum WarpRouteIds {
EclipseEthereumTETH = 'tETH/eclipsemainnet-ethereum', EclipseEthereumTETH = 'tETH/eclipsemainnet-ethereum',
EclipseEthereumWBTC = 'WBTC/eclipsemainnet-ethereum', EclipseEthereumWBTC = 'WBTC/eclipsemainnet-ethereum',
EclipseEthereumWeETHs = 'weETHs/eclipsemainnet-ethereum', EclipseEthereumWeETHs = 'weETHs/eclipsemainnet-ethereum',
EclipseSolanaORCA = 'ORCA/eclipsemainnet-solanamainnet',
EclipseSolanaSOL = 'SOL/eclipsemainnet-solanamainnet', EclipseSolanaSOL = 'SOL/eclipsemainnet-solanamainnet',
EclipseSolanaWIF = 'WIF/eclipsemainnet-solanamainnet', EclipseSolanaWIF = 'WIF/eclipsemainnet-solanamainnet',
EclipseStrideSTTIA = 'stTIA/eclipse-stride', EclipseStrideSTTIA = 'stTIA/eclipsemainnet-stride',
EclipseStrideTIA = 'TIA/eclipse-stride', EclipseStrideTIA = 'TIA/eclipsemainnet-stride',
EthereumInevmUSDC = 'USDC/ethereum-inevm', EthereumInevmUSDC = 'USDC/ethereum-inevm',
EthereumInevmUSDT = 'USDT/ethereum-inevm', EthereumInevmUSDT = 'USDT/ethereum-inevm',
EthereumSeiFastUSD = 'FASTUSD/ethereum-sei', EthereumSeiFastUSD = 'FASTUSD/ethereum-sei',

@ -12,6 +12,7 @@ import {
ChainMap, ChainMap,
ChainMetadata, ChainMetadata,
ChainName, ChainName,
WarpCoreConfig,
getDomainId as resolveDomainId, getDomainId as resolveDomainId,
getReorgPeriod as resolveReorgPeriod, getReorgPeriod as resolveReorgPeriod,
} from '@hyperlane-xyz/sdk'; } from '@hyperlane-xyz/sdk';
@ -91,7 +92,7 @@ export function getChainAddresses(): ChainMap<ChainAddresses> {
return getRegistry().getAddresses(); return getRegistry().getAddresses();
} }
export function getWarpAddresses(warpRouteId: string) { export function getWarpCoreConfig(warpRouteId: string): WarpCoreConfig {
const registry = getRegistry(); const registry = getRegistry();
const warpRouteConfig = registry.getWarpRoute(warpRouteId); const warpRouteConfig = registry.getWarpRoute(warpRouteId);
@ -100,8 +101,12 @@ export function getWarpAddresses(warpRouteId: string) {
`Warp route config for ${warpRouteId} not found in registry`, `Warp route config for ${warpRouteId} not found in registry`,
); );
} }
return warpRouteConfig;
}
return warpConfigToWarpAddresses(warpRouteConfig); export function getWarpAddresses(warpRouteId: string) {
const warpCoreConfig = getWarpCoreConfig(warpRouteId);
return warpConfigToWarpAddresses(warpCoreConfig);
} }
export function getEnvChains(env: DeployEnvironment): ChainName[] { export function getEnvChains(env: DeployEnvironment): ChainName[] {

@ -72,11 +72,11 @@ The warp-routes container
value: pretty value: pretty
command: command:
- ./node_modules/.bin/tsx - ./node_modules/.bin/tsx
- ./typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts - ./typescript/infra/scripts/warp-routes/monitor/monitor-warp-route-balances.ts
- -v - -v
- "10000" - "30000"
- -f - --warpRouteId
- {{ .Values.configFilePath }} - {{ .Values.warpRouteId }}
- -e - -e
- {{ .Values.environment}} - {{ .Values.environment}}
envFrom: envFrom:

@ -22,6 +22,7 @@ spec:
data: data:
GCP_SECRET_OVERRIDES_ENABLED: "true" GCP_SECRET_OVERRIDES_ENABLED: "true"
GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_COINGECKO_API_KEY: {{ printf "'{{ .%s_coingecko_api_key | toString }}'" .Values.hyperlane.runEnv }} GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_COINGECKO_API_KEY: {{ printf "'{{ .%s_coingecko_api_key | toString }}'" .Values.hyperlane.runEnv }}
GCP_SECRET_OVERRIDE_HYPERLANE_{{ .Values.hyperlane.runEnv | upper }}_KEY_DEPLOYER: {{ print "'{{ .deployer_key | toString }}'" }}
{{/* {{/*
* For each network, create an environment variable with the RPC endpoint. * For each network, create an environment variable with the RPC endpoint.
* The templating of external-secrets will use the data section below to know how * The templating of external-secrets will use the data section below to know how
@ -34,6 +35,9 @@ spec:
- secretKey: {{ printf "%s_coingecko_api_key" .Values.hyperlane.runEnv }} - secretKey: {{ printf "%s_coingecko_api_key" .Values.hyperlane.runEnv }}
remoteRef: remoteRef:
key: {{ printf "%s-coingecko-api-key" .Values.hyperlane.runEnv }} key: {{ printf "%s-coingecko-api-key" .Values.hyperlane.runEnv }}
- secretKey: deployer_key
remoteRef:
key: {{ printf "hyperlane-%s-key-deployer" .Values.hyperlane.runEnv }}
{{/* {{/*
* For each network, load the secret in GCP secret manager with the form: environment-rpc-endpoint-network, * For each network, load the secret in GCP secret manager with the form: environment-rpc-endpoint-network,
* and associate it with the secret key networkname_rpc. * and associate it with the secret key networkname_rpc.

@ -1,32 +1,58 @@
import { checkbox } from '@inquirer/prompts';
import yargs from 'yargs'; import yargs from 'yargs';
import { Contexts } from '../../config/contexts.js'; import { Contexts } from '../../config/contexts.js';
import { WarpRouteIds } from '../../config/environments/mainnet3/warp/warpIds.js';
import { HelmCommand } from '../../src/utils/helm.js'; import { HelmCommand } from '../../src/utils/helm.js';
import { WarpRouteMonitorHelmManager } from '../../src/warp/helm.js'; import { WarpRouteMonitorHelmManager } from '../../src/warp/helm.js';
import { assertCorrectKubeContext, getAgentConfig } from '../agent-utils.js'; import {
assertCorrectKubeContext,
getAgentConfig,
getArgs,
withWarpRouteId,
} from '../agent-utils.js';
import { getEnvironmentConfig } from '../core-utils.js'; import { getEnvironmentConfig } from '../core-utils.js';
async function main() { async function main() {
const { filePath } = await yargs(process.argv.slice(2)) const { environment, warpRouteId } = await withWarpRouteId(getArgs()).argv;
.alias('f', 'filePath')
.describe( let warpRouteIds;
'filePath', if (warpRouteId) {
'indicate the filepath to the warp route yaml file relative to the monorepo root', warpRouteIds = [warpRouteId];
) } else {
.demandOption('filePath') warpRouteIds = await getWarpRouteIdsInteractive();
.string('filePath') }
.parse();
const environment = 'mainnet3';
await assertCorrectKubeContext(getEnvironmentConfig(environment)); await assertCorrectKubeContext(getEnvironmentConfig(environment));
const agentConfig = getAgentConfig(Contexts.Hyperlane, environment); const agentConfig = getAgentConfig(Contexts.Hyperlane, environment);
const helmManager = new WarpRouteMonitorHelmManager( const deployWarpMonitor = async (warpRouteId: string) => {
filePath, const helmManager = new WarpRouteMonitorHelmManager(
environment, warpRouteId,
agentConfig.environmentChainNames, environment,
); agentConfig.environmentChainNames,
await helmManager.runHelmCommand(HelmCommand.InstallOrUpgrade); );
await helmManager.runHelmCommand(HelmCommand.InstallOrUpgrade);
};
for (const id of warpRouteIds) {
console.log(`Deploying Warp Monitor for Warp Route ID: ${id}`);
await deployWarpMonitor(id);
}
}
async function getWarpRouteIdsInteractive() {
const choices = Object.values(WarpRouteIds).map((id) => ({
value: id,
}));
const selection = await checkbox({
message: 'Select Warp Route IDs to deploy',
choices,
pageSize: 30,
});
return selection;
} }
main() main()

@ -1,697 +0,0 @@
import { SystemProgram } from '@solana/web3.js';
import { ethers } from 'ethers';
import { Gauge, Registry } from 'prom-client';
import {
ERC20__factory,
HypXERC20Lockbox__factory,
HypXERC20__factory,
IXERC20,
IXERC20__factory,
} from '@hyperlane-xyz/core';
import { createWarpRouteConfigId } from '@hyperlane-xyz/registry';
import {
ChainMap,
ChainMetadata,
ChainName,
CoinGeckoTokenPriceGetter,
CosmNativeTokenAdapter,
CwNativeTokenAdapter,
MultiProtocolProvider,
SealevelHypCollateralAdapter,
SealevelHypNativeAdapter,
SealevelHypSyntheticAdapter,
TokenType,
WarpRouteConfig,
WarpRouteConfigSchema,
} from '@hyperlane-xyz/sdk';
import {
ProtocolType,
objMap,
promiseObjAll,
rootLogger,
} from '@hyperlane-xyz/utils';
import { DeployEnvironment } from '../../src/config/environment.js';
import { fetchGCPSecret } from '../../src/utils/gcloud.js';
import { startMetricsServer } from '../../src/utils/metrics.js';
import { readYaml } from '../../src/utils/utils.js';
import { getArgs } from '../agent-utils.js';
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: 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({
name: 'hyperlane_xerc20_limits',
help: 'Current minting and burning limits of xERC20 tokens',
registers: [metricsRegister],
labelNames: ['chain_name', 'limit_type', 'token_name'],
});
interface xERC20Limit {
tokenName: string;
mint: number;
burn: number;
mintMax: number;
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}`);
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, filePath, environment } = await getArgs()
.describe('checkFrequency', 'frequency to check balances in ms')
.demandOption('checkFrequency')
.alias('v', 'checkFrequency') // v as in Greek letter nu
.number('checkFrequency')
.alias('f', 'filePath')
.describe(
'filePath',
'indicate the filepatch to the warp route yaml file relative to typescript/infra',
)
.demandOption('filePath')
.string('filePath')
.parse();
startMetricsServer(metricsRegister);
const tokenConfig: WarpRouteConfig =
readWarpRouteConfig(filePath).data.config;
const envConfig = getEnvironmentConfig(environment);
const registry = await envConfig.getRegistry();
const chainMetadata = await registry.getMetadata();
await checkWarpRouteMetrics(checkFrequency, tokenConfig, chainMetadata);
return true;
}
// TODO: see issue https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2708
async function checkBalance(
tokenConfig: WarpRouteConfig,
multiProtocolProvider: MultiProtocolProvider,
tokenPriceGetter: CoinGeckoTokenPriceGetter,
): Promise<ChainMap<WarpRouteInfo>> {
const output = objMap(
tokenConfig,
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.hypAddress);
return getNativeTokenWarpInfo(
nativeBalance,
token.decimals,
tokenPriceGetter,
chain,
);
}
case ProtocolType.Sealevel: {
const adapter = new SealevelHypNativeAdapter(
chain,
multiProtocolProvider,
{
token: token.tokenAddress,
warpRouter: token.hypAddress,
// Mailbox only required for transfers, using system as placeholder
mailbox: SystemProgram.programId.toBase58(),
},
// Not used for native tokens, but required for the adapter
token?.isSpl2022 ?? false,
);
const balance = ethers.BigNumber.from(
await adapter.getBalance(token.hypAddress),
);
return getNativeTokenWarpInfo(
balance,
token.decimals,
tokenPriceGetter,
chain,
);
}
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 getNativeTokenWarpInfo(
tokenBalance,
token.decimals,
tokenPriceGetter,
chain,
);
}
}
break;
}
case TokenType.collateral: {
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.tokenAddress,
provider,
);
const collateralBalance = await tokenContract.balanceOf(
token.hypAddress,
);
return getCollateralTokenWarpInfo(
collateralBalance,
token.decimals,
tokenPriceGetter,
token.tokenCoinGeckoId,
);
}
case ProtocolType.Sealevel: {
if (!token.tokenAddress)
throw new Error('Token address missing for collateral token');
const adapter = new SealevelHypCollateralAdapter(
chain,
multiProtocolProvider,
{
token: token.tokenAddress,
warpRouter: token.hypAddress,
// Mailbox only required for transfers, using system as placeholder
mailbox: SystemProgram.programId.toBase58(),
},
token?.isSpl2022 ?? false,
);
const collateralBalance = ethers.BigNumber.from(
await adapter.getBalance(token.hypAddress),
);
return getCollateralTokenWarpInfo(
collateralBalance,
token.decimals,
tokenPriceGetter,
token.tokenCoinGeckoId,
);
}
case ProtocolType.Cosmos: {
if (!token.tokenAddress)
throw new Error('Token address missing for cosmos token');
const adapter = new CwNativeTokenAdapter(
chain,
multiProtocolProvider,
{
token: token.hypAddress,
},
token.tokenAddress,
);
const collateralBalance = ethers.BigNumber.from(
await adapter.getBalance(token.hypAddress),
);
return getCollateralTokenWarpInfo(
collateralBalance,
token.decimals,
tokenPriceGetter,
token.tokenCoinGeckoId,
);
}
}
break;
}
case TokenType.synthetic: {
switch (token.protocolType) {
case ProtocolType.Ethereum: {
const provider = multiProtocolProvider.getEthersV5Provider(chain);
const tokenContract = ERC20__factory.connect(
token.hypAddress,
provider,
);
const syntheticBalance = await tokenContract.totalSupply();
return {
balance: parseFloat(
ethers.utils.formatUnits(syntheticBalance, token.decimals),
),
};
}
case ProtocolType.Sealevel: {
if (!token.tokenAddress)
throw new Error('Token address missing for synthetic token');
const adapter = new SealevelHypSyntheticAdapter(
chain,
multiProtocolProvider,
{
token: token.tokenAddress,
warpRouter: token.hypAddress,
// Mailbox only required for transfers, using system as placeholder
mailbox: SystemProgram.programId.toBase58(),
},
token?.isSpl2022 ?? false,
);
const syntheticBalance = ethers.BigNumber.from(
await adapter.getTotalSupply(),
);
return {
balance: parseFloat(
ethers.utils.formatUnits(syntheticBalance, token.decimals),
),
};
}
case ProtocolType.Cosmos:
// TODO - cosmos synthetic
return { balance: 0 };
}
break;
}
case TokenType.XERC20: {
switch (token.protocolType) {
case ProtocolType.Ethereum: {
const provider = multiProtocolProvider.getEthersV5Provider(chain);
const hypXERC20 = HypXERC20__factory.connect(
token.hypAddress,
provider,
);
const xerc20Address = await hypXERC20.wrappedToken();
const xerc20 = IXERC20__factory.connect(xerc20Address, provider);
const syntheticBalance = await xerc20.totalSupply();
return {
balance: parseFloat(
ethers.utils.formatUnits(syntheticBalance, token.decimals),
),
};
}
default:
throw new Error(
`Unsupported protocol type ${token.protocolType} for token type ${token.type}`,
);
}
}
case TokenType.XERC20Lockbox: {
switch (token.protocolType) {
case ProtocolType.Ethereum: {
if (!token.tokenAddress)
throw new Error(
'Token address missing for xERC20Lockbox token',
);
const provider = multiProtocolProvider.getEthersV5Provider(chain);
const hypXERC20Lockbox = HypXERC20Lockbox__factory.connect(
token.hypAddress,
provider,
);
const xerc20LockboxAddress = await hypXERC20Lockbox.lockbox();
const tokenContract = ERC20__factory.connect(
token.tokenAddress,
provider,
);
const collateralBalance = await tokenContract.balanceOf(
xerc20LockboxAddress,
);
return getCollateralTokenWarpInfo(
collateralBalance,
token.decimals,
tokenPriceGetter,
token.tokenCoinGeckoId,
);
}
default:
throw new Error(
`Unsupported protocol type ${token.protocolType} for token type ${token.type}`,
);
}
}
}
return { balance: 0 };
},
);
return promiseObjAll(output);
}
export function updateTokenBalanceMetrics(
tokenConfig: WarpRouteConfig,
balances: ChainMap<WarpRouteInfo>,
) {
objMap(tokenConfig, (chain: ChainName, token: WarpRouteConfig[ChainName]) => {
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,
related_chain_names: metrics.related_chain_names,
warp_route_id: metrics.warp_route_id,
token: metrics.token_name,
balance: balances[chain].balance,
});
});
}
export function updateXERC20LimitsMetrics(
xERC20Limits: ChainMap<xERC20Limit | undefined>,
) {
objMap(xERC20Limits, (chain: ChainName, limits: xERC20Limit | undefined) => {
if (limits) {
xERC20LimitsGauge
.labels({
chain_name: chain,
limit_type: 'mint',
token_name: limits.tokenName,
})
.set(limits.mint);
xERC20LimitsGauge
.labels({
chain_name: chain,
limit_type: 'burn',
token_name: limits.tokenName,
})
.set(limits.burn);
xERC20LimitsGauge
.labels({
chain_name: chain,
limit_type: 'mintMax',
token_name: limits.tokenName,
})
.set(limits.mintMax);
xERC20LimitsGauge
.labels({
chain_name: chain,
limit_type: 'burnMax',
token_name: limits.tokenName,
})
.set(limits.burnMax);
logger.info('xERC20 limits updated for chain', {
chain,
limits,
});
}
});
}
async function getXERC20Limits(
tokenConfig: WarpRouteConfig,
chainMetadata: ChainMap<ChainMetadata>,
): Promise<ChainMap<xERC20Limit | undefined>> {
const multiProtocolProvider = new MultiProtocolProvider(chainMetadata);
const output = objMap(
tokenConfig,
async (chain: ChainName, token: WarpRouteConfig[ChainName]) => {
switch (token.protocolType) {
case ProtocolType.Ethereum: {
switch (token.type) {
case TokenType.XERC20Lockbox: {
const provider = multiProtocolProvider.getEthersV5Provider(chain);
const routerAddress = token.hypAddress;
const lockbox = HypXERC20Lockbox__factory.connect(
token.hypAddress,
provider,
);
const xerc20Address = await lockbox.xERC20();
const xerc20 = IXERC20__factory.connect(xerc20Address, provider);
return getXERC20Limit(
routerAddress,
xerc20,
token.decimals,
token.name,
);
}
case TokenType.XERC20: {
const provider = multiProtocolProvider.getEthersV5Provider(chain);
const routerAddress = token.hypAddress;
const hypXERC20 = HypXERC20__factory.connect(
routerAddress,
provider,
);
const xerc20Address = await hypXERC20.wrappedToken();
const xerc20 = IXERC20__factory.connect(xerc20Address, provider);
return getXERC20Limit(
routerAddress,
xerc20,
token.decimals,
token.name,
);
}
default:
logger.info(
`Unsupported token type ${token.type} for xERC20 limits check on protocol type ${token.protocolType}`,
);
return undefined;
}
}
default:
throw new Error(`Unsupported protocol type ${token.protocolType}`);
}
},
);
return promiseObjAll(output);
}
const getXERC20Limit = async (
routerAddress: string,
xerc20: IXERC20,
decimals: number,
tokenName: string,
): Promise<xERC20Limit> => {
const mintCurrent = await xerc20.mintingCurrentLimitOf(routerAddress);
const mintMax = await xerc20.mintingMaxLimitOf(routerAddress);
const burnCurrent = await xerc20.burningCurrentLimitOf(routerAddress);
const burnMax = await xerc20.burningMaxLimitOf(routerAddress);
return {
tokenName,
mint: parseFloat(ethers.utils.formatUnits(mintCurrent, decimals)),
mintMax: parseFloat(ethers.utils.formatUnits(mintMax, decimals)),
burn: parseFloat(ethers.utils.formatUnits(burnCurrent, decimals)),
burnMax: parseFloat(ethers.utils.formatUnits(burnMax, decimals)),
};
};
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(
checkFrequency: number,
tokenConfig: WarpRouteConfig,
chainMetadata: ChainMap<ChainMetadata>,
) {
const tokenPriceGetter = new CoinGeckoTokenPriceGetter({
chainMetadata,
apiKey: await getCoinGeckoApiKey(),
});
setInterval(async () => {
try {
const multiProtocolProvider = new MultiProtocolProvider(chainMetadata);
const balances = await checkBalance(
tokenConfig,
multiProtocolProvider,
tokenPriceGetter,
);
logger.info('Token Balances:', balances);
updateTokenBalanceMetrics(tokenConfig, balances);
} catch (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 {
const xERC20Limits = await getXERC20Limits(tokenConfig, chainMetadata);
logger.info('xERC20 Limits:', xERC20Limits);
updateXERC20LimitsMetrics(xERC20Limits);
} catch (e) {
logger.error('Error checking xERC20 limits', e);
}
}
}, checkFrequency);
}
async function getCoinGeckoApiKey(): Promise<string | undefined> {
const environment: DeployEnvironment = 'mainnet3';
let apiKey: string | undefined;
try {
apiKey = (await fetchGCPSecret(
`${environment}-coingecko-api-key`,
false,
)) as string;
} catch (e) {
logger.error(
'Error fetching CoinGecko API key, proceeding with public tier',
e,
);
}
return apiKey;
}
main().then(logger.info).catch(logger.error);

@ -0,0 +1,105 @@
import { Gauge, Registry } from 'prom-client';
import { createWarpRouteConfigId } from '@hyperlane-xyz/registry';
import { ChainName, Token, TokenStandard, WarpCore } from '@hyperlane-xyz/sdk';
import { WarpRouteBalance, XERC20Limit } from './types.js';
import { logger } from './utils.js';
export const metricsRegister = new Registry();
type WarpRouteMetricLabels = keyof WarpRouteMetrics;
interface WarpRouteMetrics {
chain_name: ChainName;
token_address: string;
token_name: string;
wallet_address: string;
token_standard: TokenStandard;
warp_route_id: string;
related_chain_names: string;
}
const warpRouteMetricLabels: WarpRouteMetricLabels[] = [
'chain_name',
'token_address',
'token_name',
'wallet_address',
'token_standard',
'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({
name: 'hyperlane_xerc20_limits',
help: 'Current minting and burning limits of xERC20 tokens',
registers: [metricsRegister],
labelNames: ['chain_name', 'limit_type', 'token_name'],
});
export function updateTokenBalanceMetrics(
warpCore: WarpCore,
token: Token,
balanceInfo: WarpRouteBalance,
) {
const metrics: WarpRouteMetrics = {
chain_name: token.chainName,
token_address: token.collateralAddressOrDenom || token.addressOrDenom,
token_name: token.name,
wallet_address: token.addressOrDenom,
token_standard: token.standard,
warp_route_id: createWarpRouteConfigId(
token.symbol,
warpCore.getTokenChains(),
),
related_chain_names: warpCore
.getTokenChains()
.filter((chainName) => chainName !== token.chainName)
.sort()
.join(','),
};
warpRouteTokenBalance.labels(metrics).set(balanceInfo.balance);
logger.info('Wallet balance updated for token', {
labels: metrics,
balance: balanceInfo.balance,
});
if (balanceInfo.valueUSD) {
warpRouteCollateralValue.labels(metrics).set(balanceInfo.valueUSD);
logger.info('Wallet balance updated for token', {
labels: metrics,
valueUSD: balanceInfo.valueUSD,
});
}
}
export function updateXERC20LimitsMetrics(token: Token, limits: XERC20Limit) {
for (const [limitType, limit] of Object.entries(limits)) {
xERC20LimitsGauge
.labels({
chain_name: token.chainName,
limit_type: limitType,
token_name: token.name,
})
.set(limit);
}
logger.info('xERC20 limits updated for chain', {
chain: token.chainName,
limits,
});
}

@ -0,0 +1,254 @@
import { PopulatedTransaction } from 'ethers';
import {
ChainMap,
ChainMetadata,
CoinGeckoTokenPriceGetter,
EvmHypXERC20Adapter,
EvmHypXERC20LockboxAdapter,
IHypXERC20Adapter,
MultiProtocolProvider,
RouterConfig,
Token,
TokenStandard,
WarpCore,
} from '@hyperlane-xyz/sdk';
import { ProtocolType, objMap, objMerge } from '@hyperlane-xyz/utils';
import { getWarpCoreConfig } from '../../../config/registry.js';
import {
DeployEnvironment,
getRouterConfigsForAllVms,
} from '../../../src/config/environment.js';
import { fetchGCPSecret } from '../../../src/utils/gcloud.js';
import { startMetricsServer } from '../../../src/utils/metrics.js';
import { getArgs, withWarpRouteIdRequired } from '../../agent-utils.js';
import { getEnvironmentConfig } from '../../core-utils.js';
import {
metricsRegister,
updateTokenBalanceMetrics,
updateXERC20LimitsMetrics,
} from './metrics.js';
import { WarpRouteBalance, XERC20Limit } from './types.js';
import { logger, tryFn } from './utils.js';
async function main() {
const { checkFrequency, environment, warpRouteId } =
await withWarpRouteIdRequired(getArgs())
.describe('checkFrequency', 'frequency to check balances in ms')
.demandOption('checkFrequency')
.alias('v', 'checkFrequency') // v as in Greek letter nu
.number('checkFrequency')
.parse();
startMetricsServer(metricsRegister);
const envConfig = getEnvironmentConfig(environment);
const registry = await envConfig.getRegistry();
const chainMetadata = await registry.getMetadata();
// The Sealevel warp adapters require the Mailbox address, so we
// get router configs (that include the Mailbox address) for all chains
// and merge them with the chain metadata.
const routerConfig = await getRouterConfigsForAllVms(
envConfig,
await envConfig.getMultiProvider(),
);
const mailboxes = objMap(routerConfig, (_chain, config: RouterConfig) => ({
mailbox: config.mailbox,
}));
const multiProtocolProvider = new MultiProtocolProvider(
objMerge(chainMetadata, mailboxes),
);
const warpCoreConfig = getWarpCoreConfig(warpRouteId);
const warpCore = WarpCore.FromConfig(multiProtocolProvider, warpCoreConfig);
await pollAndUpdateWarpRouteMetrics(checkFrequency, warpCore, chainMetadata);
}
// Indefinitely loops, updating warp route metrics at the specified frequency.
async function pollAndUpdateWarpRouteMetrics(
checkFrequency: number,
warpCore: WarpCore,
chainMetadata: ChainMap<ChainMetadata>,
) {
const tokenPriceGetter = new CoinGeckoTokenPriceGetter({
chainMetadata,
apiKey: await getCoinGeckoApiKey(),
});
setInterval(async () => {
await tryFn(async () => {
await Promise.all(
warpCore.tokens.map((token) =>
updateTokenMetrics(warpCore, token, tokenPriceGetter),
),
);
}, 'Updating warp route metrics');
}, checkFrequency);
}
// Updates the metrics for a single token in a warp route.
async function updateTokenMetrics(
warpCore: WarpCore,
token: Token,
tokenPriceGetter: CoinGeckoTokenPriceGetter,
) {
const promises = [
tryFn(async () => {
const balanceInfo = await getTokenBridgedBalance(
warpCore,
token,
tokenPriceGetter,
);
if (!balanceInfo) {
return;
}
updateTokenBalanceMetrics(warpCore, token, balanceInfo);
}, 'Getting bridged balance and value'),
];
if (token.isXerc20()) {
promises.push(
tryFn(async () => {
const limits = await getXERC20Limits(warpCore, token);
updateXERC20LimitsMetrics(token, limits);
}, 'Getting xERC20 limits'),
);
}
await Promise.all(promises);
}
// Gets the bridged balance and value of a token in a warp route.
async function getTokenBridgedBalance(
warpCore: WarpCore,
token: Token,
tokenPriceGetter: CoinGeckoTokenPriceGetter,
): Promise<WarpRouteBalance | undefined> {
if (!token.isHypToken()) {
logger.warn('Cannot get bridged balance for a non-Hyperlane token', token);
return undefined;
}
const adapter = token.getHypAdapter(warpCore.multiProvider);
const bridgedSupply = await adapter.getBridgedSupply();
if (!bridgedSupply) {
logger.warn('Bridged supply not found for token', token);
return undefined;
}
const balance = token.amount(bridgedSupply).getDecimalFormattedAmount();
let tokenPrice;
// Only record value for collateralized and xERC20 lockbox tokens.
if (
token.isCollateralized() ||
token.standard === TokenStandard.EvmHypXERC20Lockbox
) {
tokenPrice = await tryGetTokenPrice(token, tokenPriceGetter);
}
return {
balance,
valueUSD: tokenPrice ? balance * tokenPrice : undefined,
};
}
async function getXERC20Limits(
warpCore: WarpCore,
token: Token,
): Promise<XERC20Limit> {
if (token.protocol !== ProtocolType.Ethereum) {
throw new Error(`Unsupported XERC20 protocol type ${token.protocol}`);
}
if (token.standard === TokenStandard.EvmHypXERC20) {
const adapter = token.getAdapter(
warpCore.multiProvider,
) as EvmHypXERC20Adapter;
return getXERC20Limit(token, adapter);
} else if (token.standard === TokenStandard.EvmHypXERC20Lockbox) {
const adapter = token.getAdapter(
warpCore.multiProvider,
) as EvmHypXERC20LockboxAdapter;
return getXERC20Limit(token, adapter);
}
throw new Error(`Unsupported XERC20 token standard ${token.standard}`);
}
async function getXERC20Limit(
token: Token,
xerc20: IHypXERC20Adapter<PopulatedTransaction>,
): Promise<XERC20Limit> {
const formatBigInt = (num: bigint) => {
return token.amount(num).getDecimalFormattedAmount();
};
const [mintCurrent, mintMax, burnCurrent, burnMax] = await Promise.all([
xerc20.getMintLimit(),
xerc20.getMintMaxLimit(),
xerc20.getBurnLimit(),
xerc20.getBurnMaxLimit(),
]);
return {
mint: formatBigInt(mintCurrent),
mintMax: formatBigInt(mintMax),
burn: formatBigInt(burnCurrent),
burnMax: formatBigInt(burnMax),
};
}
// Tries to get the price of a token from CoinGecko. Returns undefined if there's no
// CoinGecko ID for the token.
async function tryGetTokenPrice(
token: Token,
tokenPriceGetter: CoinGeckoTokenPriceGetter,
): Promise<number | undefined> {
// We only get a price if the token defines a CoinGecko ID.
// This way we can ignore values of certain types of collateralized warp routes,
// e.g. Native warp routes on rollups that have been pre-funded.
let coinGeckoId = token.coinGeckoId;
if (!coinGeckoId) {
logger.warn('CoinGecko ID missing for token', token.symbol);
return undefined;
}
return getCoingeckoPrice(tokenPriceGetter, coinGeckoId);
}
async function getCoingeckoPrice(
tokenPriceGetter: CoinGeckoTokenPriceGetter,
coingeckoId: string,
): Promise<number | undefined> {
const prices = await tokenPriceGetter.getTokenPriceByIds([coingeckoId]);
if (!prices) return undefined;
return prices[0];
}
async function getCoinGeckoApiKey(): Promise<string | undefined> {
const environment: DeployEnvironment = 'mainnet3';
let apiKey: string | undefined;
try {
apiKey = (await fetchGCPSecret(
`${environment}-coingecko-api-key`,
false,
)) as string;
} catch (e) {
logger.error(
'Error fetching CoinGecko API key, proceeding with public tier',
e,
);
}
return apiKey;
}
main()
.then(logger.info)
.catch((err) => {
logger.error('Error in main', err);
process.exit(1);
});

@ -0,0 +1,11 @@
export interface XERC20Limit {
mint: number;
burn: number;
mintMax: number;
burnMax: number;
}
export interface WarpRouteBalance {
balance: number;
valueUSD?: number;
}

@ -0,0 +1,11 @@
import { rootLogger } from '@hyperlane-xyz/utils';
export const logger = rootLogger.child({ module: 'warp-balance-monitor' });
export async function tryFn(fn: () => Promise<void>, context: string) {
try {
await fn();
} catch (e) {
logger.error(`Error in ${context}`, e);
}
}

@ -11,7 +11,7 @@ export class WarpRouteMonitorHelmManager extends HelmManager {
); );
constructor( constructor(
readonly configFilePath: string, readonly warpRouteId: string,
readonly runEnv: DeployEnvironment, readonly runEnv: DeployEnvironment,
readonly environmentChainNames: string[], readonly environmentChainNames: string[],
) { ) {
@ -19,17 +19,12 @@ export class WarpRouteMonitorHelmManager extends HelmManager {
} }
async helmValues() { async helmValues() {
const pathRelativeToMonorepoRoot = this.configFilePath.includes(
'typescript/infra',
)
? this.configFilePath
: path.join('typescript/infra', this.configFilePath);
return { return {
image: { image: {
repository: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', repository: 'gcr.io/abacus-labs-dev/hyperlane-monorepo',
tag: 'd3925b3-20241105-141649', tag: '91aaa4a-20241108-204429',
}, },
configFilePath: pathRelativeToMonorepoRoot, warpRouteId: this.warpRouteId,
fullnameOverride: this.helmReleaseName, fullnameOverride: this.helmReleaseName,
environment: this.runEnv, environment: this.runEnv,
hyperlane: { hyperlane: {
@ -43,8 +38,19 @@ export class WarpRouteMonitorHelmManager extends HelmManager {
} }
get helmReleaseName(): string { get helmReleaseName(): string {
const match = this.configFilePath.match(/\/([^/]+)-deployments\.yaml$/); let name = `hyperlane-warp-route-${this.warpRouteId
const name = match ? match[1] : this.configFilePath; .toLowerCase()
return `hyperlane-warp-route-${name.toLowerCase()}`; // helm requires lower case release names .replaceAll('/', '-')}`;
// 52 because the max label length is 63, and there is an auto appended 11 char
// suffix, e.g. `controller-revision-hash=hyperlane-warp-route-tia-mantapacific-neutron-566dc75599`
const maxChars = 52;
// Max out length, and it can't end with a dash.
if (name.length > maxChars) {
name = name.slice(0, maxChars);
name = name.replace(/-+$/, '');
}
return name;
} }
} }

@ -0,0 +1,15 @@
import { expect } from 'chai';
import { WarpRouteIds } from '../config/environments/mainnet3/warp/warpIds.js';
import { getRegistry } from '../config/registry.js';
describe('Warp IDs', () => {
it('Has all warp IDs in the registry', () => {
const registry = getRegistry();
for (const warpId of Object.values(WarpRouteIds)) {
// That's a long sentence!
expect(registry.getWarpRoute(warpId), `Warp ID ${warpId} not in registry`)
.to.not.be.null.and.not.be.undefined;
}
});
});

@ -420,11 +420,14 @@ export {
EvmHypCollateralAdapter, EvmHypCollateralAdapter,
EvmHypNativeAdapter, EvmHypNativeAdapter,
EvmHypSyntheticAdapter, EvmHypSyntheticAdapter,
EvmHypXERC20Adapter,
EvmHypXERC20LockboxAdapter,
EvmNativeTokenAdapter, EvmNativeTokenAdapter,
EvmTokenAdapter, EvmTokenAdapter,
} from './token/adapters/EvmTokenAdapter.js'; } from './token/adapters/EvmTokenAdapter.js';
export { export {
IHypTokenAdapter, IHypTokenAdapter,
IHypXERC20Adapter,
ITokenAdapter, ITokenAdapter,
InterchainGasQuote, InterchainGasQuote,
TransferParams, TransferParams,

@ -24,6 +24,7 @@ import {
TOKEN_NFT_STANDARDS, TOKEN_NFT_STANDARDS,
TOKEN_STANDARD_TO_PROTOCOL, TOKEN_STANDARD_TO_PROTOCOL,
TokenStandard, TokenStandard,
XERC20_STANDARDS,
} from './TokenStandard.js'; } from './TokenStandard.js';
import { import {
CwHypCollateralAdapter, CwHypCollateralAdapter,
@ -353,10 +354,18 @@ export class Token implements IToken {
return Object.values(PROTOCOL_TO_NATIVE_STANDARD).includes(this.standard); return Object.values(PROTOCOL_TO_NATIVE_STANDARD).includes(this.standard);
} }
isCollateralized(): boolean {
return TOKEN_COLLATERALIZED_STANDARDS.includes(this.standard);
}
isHypToken(): boolean { isHypToken(): boolean {
return TOKEN_HYP_STANDARDS.includes(this.standard); return TOKEN_HYP_STANDARDS.includes(this.standard);
} }
isXerc20(): boolean {
return XERC20_STANDARDS.includes(this.standard);
}
isIbcToken(): boolean { isIbcToken(): boolean {
return this.standard === TokenStandard.CosmosIbc; return this.standard === TokenStandard.CosmosIbc;
} }
@ -417,7 +426,7 @@ export class Token implements IToken {
if (this.equals(token)) return true; if (this.equals(token)) return true;
if (TOKEN_COLLATERALIZED_STANDARDS.includes(this.standard)) { if (this.isCollateralized()) {
if ( if (
this.collateralAddressOrDenom && this.collateralAddressOrDenom &&
eqAddress(this.collateralAddressOrDenom, token.addressOrDenom) eqAddress(this.collateralAddressOrDenom, token.addressOrDenom)

@ -108,11 +108,13 @@ export const TOKEN_COLLATERALIZED_STANDARDS = [
TokenStandard.CwHypNative, TokenStandard.CwHypNative,
]; ];
export const MINT_LIMITED_STANDARDS = [ export const XERC20_STANDARDS = [
TokenStandard.EvmHypXERC20, TokenStandard.EvmHypXERC20,
TokenStandard.EvmHypXERC20Lockbox, TokenStandard.EvmHypXERC20Lockbox,
]; ];
export const MINT_LIMITED_STANDARDS = [...XERC20_STANDARDS];
export const TOKEN_HYP_STANDARDS = [ export const TOKEN_HYP_STANDARDS = [
TokenStandard.EvmHypNative, TokenStandard.EvmHypNative,
TokenStandard.EvmHypCollateral, TokenStandard.EvmHypCollateral,

@ -90,6 +90,11 @@ export class CwNativeTokenAdapter
], ],
}; };
} }
async getTotalSupply(): Promise<bigint | undefined> {
// Not implemented.
return undefined;
}
} }
export type CW20Metadata = TokenMetadata; export type CW20Metadata = TokenMetadata;
@ -172,6 +177,11 @@ export class CwTokenAdapter
}, },
}); });
} }
async getTotalSupply(): Promise<bigint | undefined> {
// Not implemented.
return undefined;
}
} }
type TokenRouterResponse = type TokenRouterResponse =
@ -274,6 +284,10 @@ export class CwHypSyntheticAdapter
})); }));
} }
getBridgedSupply(): Promise<bigint | undefined> {
return this.getTotalSupply();
}
async quoteTransferRemoteGas( async quoteTransferRemoteGas(
_destination: Domain, _destination: Domain,
): Promise<InterchainGasQuote> { ): Promise<InterchainGasQuote> {
@ -366,6 +380,10 @@ export class CwHypNativeAdapter
return this.cw20adapter.getAllRouters(); return this.cw20adapter.getAllRouters();
} }
getBridgedSupply(): Promise<bigint> {
return this.getBalance(this.addresses.warpRouter);
}
quoteTransferRemoteGas(destination: Domain): Promise<InterchainGasQuote> { quoteTransferRemoteGas(destination: Domain): Promise<InterchainGasQuote> {
return this.cw20adapter.quoteTransferRemoteGas(destination); return this.cw20adapter.quoteTransferRemoteGas(destination);
} }

@ -61,6 +61,11 @@ export class CosmNativeTokenAdapter
): Promise<MsgTransferEncodeObject> { ): Promise<MsgTransferEncodeObject> {
throw new Error('TODO not yet implemented'); throw new Error('TODO not yet implemented');
} }
async getTotalSupply(): Promise<bigint | undefined> {
// Not implemented.
return undefined;
}
} }
// Interacts with native tokens on a Cosmos chain and adds support for IBC transfers // Interacts with native tokens on a Cosmos chain and adds support for IBC transfers
@ -103,6 +108,11 @@ export class CosmIbcTokenAdapter
> { > {
throw new Error('Method not applicable to IBC adapters'); throw new Error('Method not applicable to IBC adapters');
} }
getBridgedSupply(): Promise<bigint | undefined> {
throw new Error('Method not applicable to IBC adapters');
}
async quoteTransferRemoteGas( async quoteTransferRemoteGas(
_destination: Domain, _destination: Domain,
): Promise<InterchainGasQuote> { ): Promise<InterchainGasQuote> {

@ -11,6 +11,7 @@ import {
HypXERC20Lockbox, HypXERC20Lockbox,
HypXERC20Lockbox__factory, HypXERC20Lockbox__factory,
HypXERC20__factory, HypXERC20__factory,
IXERC20,
IXERC20__factory, IXERC20__factory,
} from '@hyperlane-xyz/core'; } from '@hyperlane-xyz/core';
import { import {
@ -77,6 +78,11 @@ export class EvmNativeTokenAdapter
const value = BigNumber.from(weiAmountOrId.toString()); const value = BigNumber.from(weiAmountOrId.toString());
return { value, to: recipient }; return { value, to: recipient };
} }
async getTotalSupply(): Promise<bigint | undefined> {
// Not implemented, native tokens don't have an accessible total supply
return undefined;
}
} }
// Interacts with ERC20/721 contracts // Interacts with ERC20/721 contracts
@ -109,7 +115,7 @@ export class EvmTokenAdapter<T extends ERC20 = ERC20>
isNft ? 0 : this.contract.decimals(), isNft ? 0 : this.contract.decimals(),
this.contract.symbol(), this.contract.symbol(),
this.contract.name(), this.contract.name(),
this.contract.totalSupply(), this.getTotalSupply(),
]); ]);
return { decimals, symbol, name, totalSupply: totalSupply.toString() }; return { decimals, symbol, name, totalSupply: totalSupply.toString() };
} }
@ -142,6 +148,11 @@ export class EvmTokenAdapter<T extends ERC20 = ERC20>
weiAmountOrId.toString(), weiAmountOrId.toString(),
); );
} }
async getTotalSupply(): Promise<bigint> {
const totalSupply = await this.contract.totalSupply();
return totalSupply.toBigInt();
}
} }
// Interacts with Hyp Synthetic token contracts (aka 'HypTokens') // Interacts with Hyp Synthetic token contracts (aka 'HypTokens')
@ -192,6 +203,10 @@ export class EvmHypSyntheticAdapter
return domains.map((d, i) => ({ domain: d, address: routers[i] })); return domains.map((d, i) => ({ domain: d, address: routers[i] }));
} }
getBridgedSupply(): Promise<bigint | undefined> {
return this.getTotalSupply();
}
async quoteTransferRemoteGas( async quoteTransferRemoteGas(
destination: Domain, destination: Domain,
): Promise<InterchainGasQuote> { ): Promise<InterchainGasQuote> {
@ -254,6 +269,10 @@ export class EvmHypCollateralAdapter
}); });
} }
override getBridgedSupply(): Promise<bigint | undefined> {
return this.getBalance(this.addresses.token);
}
override getMetadata(isNft?: boolean): Promise<TokenMetadata> { override getMetadata(isNft?: boolean): Promise<TokenMetadata> {
return this.getWrappedTokenAdapter().then((t) => t.getMetadata(isNft)); return this.getWrappedTokenAdapter().then((t) => t.getMetadata(isNft));
} }
@ -305,26 +324,44 @@ export class EvmHypXERC20LockboxAdapter
); );
} }
async getMintLimit(): Promise<bigint> { /**
const xERC20 = await this.hypXERC20Lockbox.xERC20(); * Note this may be inaccurate, as this returns the balance
* of the lockbox contract, which may be used by other bridges.
* However this is the best we can do with a simple view call.
*/
override async getBridgedSupply(): Promise<bigint> {
const lockboxAddress = await this.hypXERC20Lockbox.lockbox();
return this.getBalance(lockboxAddress);
}
const limit = await IXERC20__factory.connect( async getMintLimit(): Promise<bigint> {
xERC20, const xERC20 = await this.getXErc20();
this.getProvider(), const limit = await xERC20.mintingCurrentLimitOf(this.contract.address);
).mintingCurrentLimitOf(this.contract.address); return limit.toBigInt();
}
return BigInt(limit.toString()); async getMintMaxLimit(): Promise<bigint> {
const xERC20 = await this.getXErc20();
const limit = await xERC20.mintingMaxLimitOf(this.contract.address);
return limit.toBigInt();
} }
async getBurnLimit(): Promise<bigint> { async getBurnLimit(): Promise<bigint> {
const xERC20 = await this.hypXERC20Lockbox.xERC20(); const xERC20 = await this.getXErc20();
const limit = await xERC20.burningCurrentLimitOf(this.contract.address);
return limit.toBigInt();
}
const limit = await IXERC20__factory.connect( async getBurnMaxLimit(): Promise<bigint> {
xERC20, const xERC20 = await this.getXErc20();
this.getProvider(), const limit = await xERC20.burningMaxLimitOf(this.contract.address);
).mintingCurrentLimitOf(this.contract.address); return limit.toBigInt();
}
async getXErc20(): Promise<IXERC20> {
const xERC20 = await this.hypXERC20Lockbox.xERC20();
return BigInt(limit.toString()); return IXERC20__factory.connect(xERC20, this.getProvider());
} }
} }
@ -348,26 +385,47 @@ export class EvmHypXERC20Adapter
); );
} }
async getMintLimit(): Promise<bigint> { /**
const xERC20 = await this.hypXERC20.wrappedToken(); * Note this may be inaccurate, as this returns the total supply
* of the xERC20 contract, which may be used by other bridges.
* However this is the best we can do with a simple view call.
*/
override async getBridgedSupply(): Promise<bigint> {
const xerc20TokenAddress = await this.hypXERC20.wrappedToken();
const xerc20 = new EvmTokenAdapter(this.chainName, this.multiProvider, {
token: xerc20TokenAddress,
});
return xerc20.getTotalSupply();
}
const limit = await IXERC20__factory.connect( async getMintLimit(): Promise<bigint> {
xERC20, const xERC20 = await this.getXErc20();
this.getProvider(), const limit = await xERC20.mintingCurrentLimitOf(this.contract.address);
).mintingCurrentLimitOf(this.contract.address); return limit.toBigInt();
}
return BigInt(limit.toString()); async getMintMaxLimit(): Promise<bigint> {
const xERC20 = await this.getXErc20();
const limit = await xERC20.mintingMaxLimitOf(this.contract.address);
return limit.toBigInt();
} }
async getBurnLimit(): Promise<bigint> { async getBurnLimit(): Promise<bigint> {
const xERC20 = await this.hypXERC20.wrappedToken(); const xERC20 = await this.getXErc20();
const limit = await xERC20.burningCurrentLimitOf(this.contract.address);
return limit.toBigInt();
}
const limit = await IXERC20__factory.connect( async getBurnMaxLimit(): Promise<bigint> {
xERC20, const xERC20 = await this.getXErc20();
this.getProvider(), const limit = await xERC20.burningMaxLimitOf(this.contract.address);
).burningCurrentLimitOf(this.contract.address); return limit.toBigInt();
}
async getXErc20(): Promise<IXERC20> {
const xERC20 = await this.hypXERC20.wrappedToken();
return BigInt(limit.toString()); return IXERC20__factory.connect(xERC20, this.getProvider());
} }
} }

@ -23,6 +23,7 @@ export interface InterchainGasQuote {
export interface ITokenAdapter<Tx> { export interface ITokenAdapter<Tx> {
getBalance(address: Address): Promise<bigint>; getBalance(address: Address): Promise<bigint>;
getTotalSupply(): Promise<bigint | undefined>;
getMetadata(isNft?: boolean): Promise<TokenMetadata>; getMetadata(isNft?: boolean): Promise<TokenMetadata>;
isApproveRequired( isApproveRequired(
owner: Address, owner: Address,
@ -37,11 +38,15 @@ export interface IHypTokenAdapter<Tx> extends ITokenAdapter<Tx> {
getDomains(): Promise<Domain[]>; getDomains(): Promise<Domain[]>;
getRouterAddress(domain: Domain): Promise<Buffer>; getRouterAddress(domain: Domain): Promise<Buffer>;
getAllRouters(): Promise<Array<{ domain: Domain; address: Buffer }>>; getAllRouters(): Promise<Array<{ domain: Domain; address: Buffer }>>;
getBridgedSupply(): Promise<bigint | undefined>;
quoteTransferRemoteGas(destination: Domain): Promise<InterchainGasQuote>; quoteTransferRemoteGas(destination: Domain): Promise<InterchainGasQuote>;
populateTransferRemoteTx(p: TransferRemoteParams): Promise<Tx>; populateTransferRemoteTx(p: TransferRemoteParams): Promise<Tx>;
} }
export interface IHypXERC20Adapter<Tx> extends IHypTokenAdapter<Tx> { export interface IHypXERC20Adapter<Tx> extends IHypTokenAdapter<Tx> {
getMintLimit(): Promise<bigint>; getMintLimit(): Promise<bigint>;
getMintMaxLimit(): Promise<bigint>;
getBurnLimit(): Promise<bigint>; getBurnLimit(): Promise<bigint>;
getBurnMaxLimit(): Promise<bigint>;
} }

@ -113,6 +113,11 @@ export class SealevelNativeTokenAdapter
}), }),
); );
} }
async getTotalSupply(): Promise<bigint | undefined> {
// Not implemented.
return undefined;
}
} }
// Interacts with SPL token programs // Interacts with SPL token programs
@ -190,6 +195,13 @@ export class SealevelTokenAdapter
this.getTokenProgramId(), this.getTokenProgramId(),
); );
} }
async getTotalSupply(): Promise<bigint | undefined> {
const response = await this.getProvider().getTokenSupply(
this.tokenMintPubKey,
);
return BigInt(response.value.amount);
}
} }
interface HypTokenAddresses { interface HypTokenAddresses {
@ -266,6 +278,11 @@ export abstract class SealevelHypTokenAdapter
})); }));
} }
// Intended to be overridden by subclasses
async getBridgedSupply(): Promise<bigint | undefined> {
return undefined;
}
async quoteTransferRemoteGas( async quoteTransferRemoteGas(
_destination: Domain, _destination: Domain,
): Promise<InterchainGasQuote> { ): Promise<InterchainGasQuote> {
@ -581,6 +598,10 @@ export class SealevelHypNativeAdapter extends SealevelHypTokenAdapter {
return this.wrappedNative.getBalance(owner); return this.wrappedNative.getBalance(owner);
} }
override async getBridgedSupply(): Promise<bigint> {
return this.getBalance(this.addresses.warpRouter);
}
override async getMetadata(): Promise<TokenMetadata> { override async getMetadata(): Promise<TokenMetadata> {
return this.wrappedNative.getMetadata(); return this.wrappedNative.getMetadata();
} }
@ -632,6 +653,10 @@ export class SealevelHypCollateralAdapter extends SealevelHypTokenAdapter {
return super.getBalance(owner); return super.getBalance(owner);
} }
override async getBridgedSupply(): Promise<bigint> {
return this.getBalance(this.addresses.warpRouter);
}
override getTransferInstructionKeyList( override getTransferInstructionKeyList(
params: KeyListParams, params: KeyListParams,
): Array<AccountMeta> { ): Array<AccountMeta> {
@ -697,6 +722,10 @@ export class SealevelHypSyntheticAdapter extends SealevelHypTokenAdapter {
} }
} }
override async getBridgedSupply(): Promise<bigint> {
return this.getTotalSupply();
}
async getTotalSupply(): Promise<bigint> { async getTotalSupply(): Promise<bigint> {
const response = await this.getProvider().getTokenSupply( const response = await this.getProvider().getTokenSupply(
this.tokenMintPubKey, this.tokenMintPubKey,

Loading…
Cancel
Save