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
parent
0754d6218c
commit
9fcd592878
@ -1 +1 @@ |
||||
d71eb5f42616998f77ce01079fd06a8e118966f7 |
||||
10fdae0f566c7c213b6b1b27b8620e8776f2895f |
||||
|
@ -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 |
@ -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); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
}); |
||||
}); |
Loading…
Reference in new issue