Migrate SDK consts to Registry and use new registry utilities (#3615)

### Description

- Remove most consts from SDK
- Refactor tests that relied on chainMetadata
- Remove JSON imports/exports in the SDK
- Update the HelloWorld package to read metadata from the registry lib
- Update the Infra package to read/write to a LocalRegistry
- Update the CLI to read and write from registries

### Drive-by changes

- Fix typo in `EvmHypCollateralVault` token standard 
- Use consistent, newest yaml lib version

### Related issues

Fixes https://github.com/hyperlane-xyz/issues/issues/917
Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2810

### Backward compatibility

No: 
- Several SDK exports like `chainMetadata` have been removed
- Common args for the CLI like `--chain` have been replaced with `--registry` and `--overrides`
pull/3685/head
J M Rossy 7 months ago committed by GitHub
parent 292879126c
commit 3528b281e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .changeset/khaki-days-float.md
  2. 5
      .changeset/moody-colts-dress.md
  3. 5
      .changeset/nine-masks-guess.md
  4. 100
      .github/workflows/cron.yml
  5. 2
      .github/workflows/release.yml
  6. 1
      .github/workflows/test.yml
  7. 51
      rust/config/mainnet_config.json
  8. 6
      rust/config/testnet_config.json
  9. 9
      solidity/exportBuildArtifact.sh
  10. 6
      solidity/package.json
  11. 10
      typescript/cli/.gitignore
  12. 153
      typescript/cli/ci-test.sh
  13. 20
      typescript/cli/cli.ts
  14. 22
      typescript/cli/examples/anvil-chains.yaml
  15. 84
      typescript/cli/examples/chain-config.yaml
  16. 17
      typescript/cli/examples/dry-run/anvil-chains.yaml
  17. 21
      typescript/cli/examples/fork/anvil-chains.yaml
  18. 3
      typescript/cli/package.json
  19. 94
      typescript/cli/src/commands/chains.ts
  20. 170
      typescript/cli/src/commands/config.ts
  21. 151
      typescript/cli/src/commands/deploy.ts
  22. 38
      typescript/cli/src/commands/hook.ts
  23. 44
      typescript/cli/src/commands/ism.ts
  24. 114
      typescript/cli/src/commands/options.ts
  25. 161
      typescript/cli/src/commands/send.ts
  26. 11
      typescript/cli/src/commands/signCommands.ts
  27. 42
      typescript/cli/src/commands/status.ts
  28. 86
      typescript/cli/src/config/artifacts.ts
  29. 9
      typescript/cli/src/config/chain.test.ts
  30. 79
      typescript/cli/src/config/chain.ts
  31. 41
      typescript/cli/src/config/hooks.ts
  32. 16
      typescript/cli/src/config/ism.ts
  33. 20
      typescript/cli/src/config/multisig.ts
  34. 41
      typescript/cli/src/config/warp.ts
  35. 23
      typescript/cli/src/context.test.ts
  36. 213
      typescript/cli/src/context.ts
  37. 134
      typescript/cli/src/context/context.ts
  38. 43
      typescript/cli/src/context/types.ts
  39. 20
      typescript/cli/src/deploy/agent.ts
  40. 242
      typescript/cli/src/deploy/core.ts
  41. 5
      typescript/cli/src/deploy/dry-run.ts
  42. 39
      typescript/cli/src/deploy/utils.ts
  43. 169
      typescript/cli/src/deploy/warp.ts
  44. 38
      typescript/cli/src/hook/read.ts
  45. 38
      typescript/cli/src/ism/read.ts
  46. 156
      typescript/cli/src/registry/MergedRegistry.ts
  47. 53
      typescript/cli/src/send/message.ts
  48. 97
      typescript/cli/src/send/transfer.ts
  49. 28
      typescript/cli/src/status/message.ts
  50. 32
      typescript/cli/src/utils/chains.ts
  51. 38
      typescript/cli/src/utils/files.ts
  52. 48
      typescript/cli/src/utils/keys.ts
  53. 19
      typescript/cli/src/utils/tokens.ts
  54. 5
      typescript/cli/src/utils/version-check.ts
  55. 14
      typescript/cli/test-configs/anvil/chains/anvil1/metadata.yaml
  56. 7
      typescript/cli/test-configs/anvil/chains/anvil2/metadata.yaml
  57. 13
      typescript/cli/test-configs/dry-run/chains/alfajores/metadata.yaml
  58. 10
      typescript/cli/test-configs/dry-run/chains/anvil/metadata.yaml
  59. 6
      typescript/cli/test-configs/dry-run/chains/fuji/metadata.yaml
  60. 0
      typescript/cli/test-configs/dry-run/ism.yaml
  61. 0
      typescript/cli/test-configs/dry-run/warp-route-deployment.yaml
  62. 11
      typescript/cli/test-configs/fork/chains/anvil1/metadata.yaml
  63. 9
      typescript/cli/test-configs/fork/chains/ethereum/metadata.yaml
  64. 3
      typescript/cli/test-configs/fork/ism.yaml
  65. 2
      typescript/cli/test-configs/fork/warp-route-deployment.yaml
  66. 1
      typescript/helloworld/package.json
  67. 7
      typescript/helloworld/src/deploy/config.ts
  68. 9
      typescript/helloworld/src/scripts/check.ts
  69. 5
      typescript/helloworld/src/scripts/deploy.ts
  70. 6
      typescript/helloworld/src/test/helloworld.test.ts
  71. 8
      typescript/infra/README.md
  72. 132
      typescript/infra/config/environments/mainnet3/agent.ts
  73. 32
      typescript/infra/config/environments/mainnet3/chains.ts
  74. 7
      typescript/infra/config/environments/mainnet3/core.ts
  75. 15
      typescript/infra/config/environments/mainnet3/igp.ts
  76. 12
      typescript/infra/config/environments/mainnet3/liquidityLayer.ts
  77. 12
      typescript/infra/config/environments/mainnet3/owners.ts
  78. 24
      typescript/infra/config/environments/mainnet3/supportedChainNames.ts
  79. 9
      typescript/infra/config/environments/mainnet3/token-bridge.ts
  80. 43
      typescript/infra/config/environments/mainnet3/validators.ts
  81. 4
      typescript/infra/config/environments/test/agent.ts
  82. 21
      typescript/infra/config/environments/test/chains.ts
  83. 8
      typescript/infra/config/environments/test/gas-oracle.ts
  84. 9
      typescript/infra/config/environments/test/igp.ts
  85. 5
      typescript/infra/config/environments/test/index.ts
  86. 4
      typescript/infra/config/environments/test/owners.ts
  87. 52
      typescript/infra/config/environments/testnet4/agent.ts
  88. 35
      typescript/infra/config/environments/testnet4/chains.ts
  89. 7
      typescript/infra/config/environments/testnet4/core.ts
  90. 4
      typescript/infra/config/environments/testnet4/gas-oracle.ts
  91. 2
      typescript/infra/config/environments/testnet4/igp.ts
  92. 19
      typescript/infra/config/environments/testnet4/liquidityLayer.ts
  93. 4
      typescript/infra/config/environments/testnet4/owners.ts
  94. 12
      typescript/infra/config/environments/testnet4/supportedChainNames.ts
  95. 19
      typescript/infra/config/environments/testnet4/token-bridge.ts
  96. 19
      typescript/infra/config/environments/testnet4/validators.ts
  97. 6
      typescript/infra/config/environments/utils.ts
  98. 17
      typescript/infra/config/multisigIsm.ts
  99. 109
      typescript/infra/config/registry.ts
  100. 14
      typescript/infra/config/routingIsm.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,5 +1,5 @@
---
"@hyperlane-xyz/sdk": patch
'@hyperlane-xyz/sdk': patch
---
Allow gasLimit overrides in the SDK/CLI for deploy txs

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---
Remove consts such as chainMetadata from SDK

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/cli': minor
---
Restructure CLI params around registries

@ -1,100 +0,0 @@
name: cron
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows
on:
schedule:
- cron: '45 14 * * *'
workflow_dispatch:
env:
LOG_LEVEL: DEBUG
LOG_FORMAT: PRETTY
jobs:
# copied from test.yml
yarn-install:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v3
with:
node-version: 18
- uses: actions/checkout@v3
with:
ref: ${{ github.sha }}
submodules: recursive
- name: yarn-cache
uses: actions/cache@v3
with:
path: |
**/node_modules
.yarn
key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }}
- name: yarn-install
run: yarn install
# copied from test.yml
yarn-build:
runs-on: ubuntu-latest
needs: [yarn-install]
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.sha }}
submodules: recursive
fetch-depth: 0
- name: yarn-cache
uses: actions/cache@v3
with:
path: |
**/node_modules
.yarn
key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }}
- name: build-cache
uses: actions/cache@v3
with:
path: |
./*
!./rust
key: ${{ github.sha }}
- name: build
run: yarn build
metadata-check:
runs-on: ubuntu-latest
needs: [yarn-build]
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.sha }}
submodules: recursive
fetch-depth: 0
- name: yarn-cache
uses: actions/cache@v3
with:
path: |
**/node_modules
.yarn
key: ${{ runner.os }}-yarn-cache-${{ hashFiles('./yarn.lock') }}
- name: build-cache
uses: actions/cache@v3
with:
path: |
./*
!./rust
key: ${{ github.sha }}
- name: Metadata Health Check
run: yarn workspace @hyperlane-xyz/sdk run test:metadata
- name: Post to discord webhook if metadata check fails
if: failure()
run: |
curl -X POST -H 'Content-type: application/json' --data '{"content":"SDK metadata check failed, see ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' ${{ secrets.DISCORD_WEBHOOK_URL }}

@ -31,7 +31,7 @@ jobs:
node-version: 18.x
- name: Install Dependencies
run: yarn
run: yarn install --immutable
- name: Create Release PR or Publish to NPM
id: changesets

@ -18,6 +18,7 @@ env:
LOG_FORMAT: PRETTY
CARGO_TERM_COLOR: always
RUST_BACKTRACE: full
REGISTRY_URI: ../../node_modules/@hyperlane-xyz/registry/dist
jobs:
yarn-install:

@ -77,6 +77,8 @@
"index": {
"from": 143649797
},
"interchainAccountIsm": "0xfa8bfcE55B3A0631dF38257615cEF7FCD3523A48",
"interchainAccountRouter": "0xCD0CFFf6eFD943b4b81f2c15847730dbcD30e3aE",
"interchainGasPaymaster": "0x3b6044acd6767f017e99318AA6Ef93b7B06A5a22",
"interchainSecurityModule": "0xD0DBBF922076352cC50B285A0023536561F00EEa",
"mailbox": "0x979Ca5202784112f4738403dBec5D0F3B9daabB9",
@ -104,6 +106,7 @@
"technicalStack": "arbitrumnitro",
"testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35",
"testTokenRecipient": "0x85ac1164878e017b67660a74ff1f41f3D05C02Bb",
"timelockController": "0x0000000000000000000000000000000000000000",
"validatorAnnounce": "0x1df063280C4166AF9a725e3828b4dAC6c7113B08"
},
"avalanche": {
@ -131,6 +134,8 @@
"index": {
"from": 36874693
},
"interchainAccountIsm": "0x786c26C1857032617c215f265509d6E44e44Bfe3",
"interchainAccountRouter": "0xA967A6CE0e73fAf672843DECaA372511996E8852",
"interchainGasPaymaster": "0x95519ba800BBd0d34eeAE026fEc620AD978176C0",
"interchainSecurityModule": "0xA36B02a83564f52d9244310Ea439ee6F6AfeFb60",
"mailbox": "0xFf06aFcaABaDDd1fb08371f9ccA15D73D51FeBD6",
@ -161,6 +166,7 @@
"storageGasOracle": "0x175821F30AdCAA4bbB72Ce98eF76C2E0De2C3f21",
"testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35",
"testTokenRecipient": "0x85ac1164878e017b67660a74ff1f41f3D05C02Bb",
"timelockController": "0x0000000000000000000000000000000000000000",
"validatorAnnounce": "0x9Cad0eC82328CEE2386Ec14a12E81d070a27712f"
},
"base": {
@ -188,6 +194,8 @@
"index": {
"from": 5695475
},
"interchainAccountIsm": "0x861908E6c8F992537F557da5Fb5876836036b347",
"interchainAccountRouter": "0xa85F9e4fdA2FFF1c07f2726a630443af3faDF830",
"interchainGasPaymaster": "0xc3F23848Ed2e04C0c6d41bd7804fa8f89F940B94",
"interchainSecurityModule": "0x5D1e7D7c5B9e6dDC8439F67F10c578f2A1084f6F",
"mailbox": "0xeA87ae93Fa0019a82A727bfd3eBd1cFCa8f64f1D",
@ -218,16 +226,11 @@
"staticMerkleRootMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE",
"staticMessageIdMultisigIsmFactory": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A",
"storageGasOracle": "0xBF12ef4B9f307463D3FB59c3604F294dDCe287E2",
"timelockController": "0x0000000000000000000000000000000000000000",
"validatorAnnounce": "0x182E8d7c5F1B06201b102123FC7dF0EaeB445a7B"
},
"blast": {
"blockExplorers": [
{
"apiUrl": "https://api.blastscan.io/api",
"family": "etherscan",
"name": "Blast Explorer",
"url": "https://blastscan.io"
},
{
"apiUrl": "https://api.routescan.io/v2/network/mainnet/evm/81457/etherscan/api",
"family": "routescan",
@ -303,6 +306,8 @@
"index": {
"from": 32893043
},
"interchainAccountIsm": "0xB274Bbbc1df5f1d1763216A93d473fde6f5de043",
"interchainAccountRouter": "0x4BBd67dC995572b40Dc6B3eB6CdE5185a5373868",
"interchainGasPaymaster": "0x78E25e7f84416e69b9339B0A6336EB6EFfF6b451",
"interchainSecurityModule": "0xab3df354baBee6c2B88E2CeD3b2e030e31aA5e61",
"mailbox": "0x2971b9Aec44bE4eb673DF1B88cDB57b96eefe8a4",
@ -335,6 +340,7 @@
"storageGasOracle": "0x91d23D603d60445411C06e6443d81395593B7940",
"testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35",
"testTokenRecipient": "0x85ac1164878e017b67660a74ff1f41f3D05C02Bb",
"timelockController": "0x0000000000000000000000000000000000000000",
"transactionOverrides": {
"gasPrice": 3000000000
},
@ -371,6 +377,8 @@
"index": {
"from": 22102340
},
"interchainAccountIsm": "0x30a8DEc5318e2aAa9ad5b069fC606c4CfF6f5676",
"interchainAccountRouter": "0x4ED23E3885e1651E62564F78817D91865beba575",
"interchainGasPaymaster": "0x571f1435613381208477ac5d6974310d88AC7cB7",
"interchainSecurityModule": "0x99e8E56Dce3402D6E09A82718937fc1cA2A9491E",
"mailbox": "0x50da3B3907A08a24fe4999F4Dcf337E8dC7954bb",
@ -399,6 +407,7 @@
"storageGasOracle": "0xD9A9966E7dA9a7f0032bF449FB12696a638E673C",
"testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35",
"testTokenRecipient": "0x85ac1164878e017b67660a74ff1f41f3D05C02Bb",
"timelockController": "0x0000000000000000000000000000000000000000",
"validatorAnnounce": "0xCeF677b65FDaA6804d4403083bb12B8dB3991FE1"
},
"ethereum": {
@ -431,6 +440,8 @@
"index": {
"from": 18422581
},
"interchainAccountIsm": "0x609707355a53d2aAb6366f48E2b607C599D26B29",
"interchainAccountRouter": "0x8dBae9B1616c46A20591fE0006Bf015E28ca5cC9",
"interchainGasPaymaster": "0x9e6B1022bE9BBF5aFd152483DAD9b88911bC8611",
"interchainSecurityModule": "0xB42b88243F749F47697F01Ae1cbBCA9d4763902a",
"mailbox": "0xc005dc82818d67AF737725bD4bf75435d065D239",
@ -460,6 +471,7 @@
"storageGasOracle": "0xc9a103990A8dB11b4f627bc5CD1D0c2685484Ec5",
"testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35",
"testTokenRecipient": "0x85ac1164878e017b67660a74ff1f41f3D05C02Bb",
"timelockController": "0x0000000000000000000000000000000000000000",
"transactionOverrides": {
"maxFeePerGas": 150000000000,
"maxPriorityFeePerGas": 5000000000
@ -491,6 +503,8 @@
"index": {
"from": 30620793
},
"interchainAccountIsm": "0x5a56dff3D92D635372718f86e6dF09C1129CFf53",
"interchainAccountRouter": "0x5E59EBAedeB691408EBAcF6C37218fa2cFcaC9f2",
"interchainGasPaymaster": "0xDd260B99d302f0A3fF885728c086f729c06f227f",
"interchainSecurityModule": "0x8e1aa0687B6d939D5a44304D13B7c922ebB012f1",
"mailbox": "0xaD09d78f4c6b9dA2Ae82b1D34107802d380Bb74f",
@ -521,6 +535,7 @@
"storageGasOracle": "0x5E01d8F34b629E3f92d69546bbc4142A7Adee7e9",
"testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35",
"testTokenRecipient": "0x85ac1164878e017b67660a74ff1f41f3D05C02Bb",
"timelockController": "0x0000000000000000000000000000000000000000",
"validatorAnnounce": "0x87ED6926abc9E38b9C7C19f835B41943b622663c"
},
"inevm": {
@ -548,6 +563,8 @@
"index": {
"from": 18972465
},
"interchainAccountIsm": "0x31894E7a734540B343d67E491148EB4FC9f7A45B",
"interchainAccountRouter": "0x4E55aDA3ef1942049EA43E904EB01F4A0a9c39bd",
"interchainGasPaymaster": "0x19dc38aeae620380430C200a6E990D5Af5480117",
"interchainSecurityModule": "0x3052aD50De54aAAc5D364d80bBE681d29e924597",
"mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7",
@ -574,6 +591,7 @@
"staticMerkleRootMultisigIsmFactory": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC",
"staticMessageIdMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE",
"storageGasOracle": "0x6119E37Bd66406A1Db74920aC79C15fB8411Ba76",
"timelockController": "0x0000000000000000000000000000000000000000",
"validatorAnnounce": "0x15ab173bDB6832f9b64276bA128659b0eD77730B"
},
"injective": {
@ -636,6 +654,8 @@
"index": {
"from": 437300
},
"interchainAccountIsm": "0xA34ceDf9068C5deE726C67A4e1DCfCc2D6E2A7fD",
"interchainAccountRouter": "0x0f6fF770Eda6Ba1433C39cCf47d4059b254224Aa",
"interchainGasPaymaster": "0x0D63128D887159d63De29497dfa45AFc7C699AE4",
"interchainSecurityModule": "0xEda7cCD2A8CF717dc997D0002e363e4D10bF5c0d",
"isTestnet": false,
@ -663,6 +683,7 @@
"storageGasOracle": "0x19dc38aeae620380430C200a6E990D5Af5480117",
"testRecipient": "0x4E1c88DD261BEe2941e6c1814597e30F53330428",
"testTokenRecipient": "0x5060eCD5dFAD300A90592C04e504600A7cdcF70b",
"timelockController": "0x0000000000000000000000000000000000000000",
"validatorAnnounce": "0x2fa5F5C96419C222cDbCeC797D696e6cE428A7A9"
},
"mode": {
@ -740,6 +761,8 @@
"index": {
"from": 4719713
},
"interchainAccountIsm": "0x799eA6f430f5CA901b59335fFC2fA10531106009",
"interchainAccountRouter": "0x6b142f596FFc761ac3fFaaC1ecaDe54f4EE09977",
"interchainGasPaymaster": "0x14760E32C0746094cF14D97124865BC7F0F7368F",
"interchainSecurityModule": "0x373836DFa82f2D27ec79Ca32A197Aa1665F0E1e9",
"mailbox": "0x094d03E751f49908080EFf000Dd6FD177fd44CC3",
@ -766,6 +789,7 @@
"storageGasOracle": "0x448b7ADB0dA36d41AA2AfDc9d63b97541A7b3819",
"testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35",
"testTokenRecipient": "0x85ac1164878e017b67660a74ff1f41f3D05C02Bb",
"timelockController": "0x0000000000000000000000000000000000000000",
"transactionOverrides": {
"maxFeePerGas": 350000000000,
"maxPriorityFeePerGas": 50000000000
@ -836,6 +860,8 @@
"index": {
"from": 111290758
},
"interchainAccountIsm": "0x0389faCac114023C123E22F3E54394944cAbcb48",
"interchainAccountRouter": "0x33Ef006E7083BB38E0AFe3C3979F4e9b84415bf1",
"interchainGasPaymaster": "0xD8A76C4D91fCbB7Cc8eA795DFDF870E48368995C",
"interchainSecurityModule": "0x04938856bE60c8e734ffDe5f720E2238302BE8D2",
"mailbox": "0xd4C1905BB1D26BC93DAC913e13CaCC278CdCC80D",
@ -862,6 +888,7 @@
"storageGasOracle": "0x27e88AeB8EA4B159d81df06355Ea3d20bEB1de38",
"testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35",
"testTokenRecipient": "0x85ac1164878e017b67660a74ff1f41f3D05C02Bb",
"timelockController": "0x0000000000000000000000000000000000000000",
"validatorAnnounce": "0x30f5b08e01808643221528BB2f7953bf2830Ef38"
},
"polygon": {
@ -889,6 +916,8 @@
"index": {
"from": 49108065
},
"interchainAccountIsm": "0x90384bC552e3C48af51Ef7D9473A9bF87431f5c7",
"interchainAccountRouter": "0x5e80f3474825B61183c0F0f0726796F589082420",
"interchainGasPaymaster": "0x0071740Bf129b05C4684abfbBeD248D80971cce2",
"interchainSecurityModule": "0x9a795fB62f86146ec06e2377e3C95Af65c7C20eB",
"mailbox": "0x5d934f4e2f797775e53561bB72aca21ba36B96BB",
@ -921,6 +950,7 @@
"storageGasOracle": "0xA3a24EC5670F1F416AB9fD554FcE2f226AE9D7eB",
"testRecipient": "0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35",
"testTokenRecipient": "0x85ac1164878e017b67660a74ff1f41f3D05C02Bb",
"timelockController": "0x0000000000000000000000000000000000000000",
"transactionOverrides": {
"maxFeePerGas": 800000000000,
"maxPriorityFeePerGas": 50000000000
@ -952,6 +982,8 @@
"index": {
"from": 6577743
},
"interchainAccountIsm": "0xC49aF4965264FA7BB6424CE37aA06773ad177224",
"interchainAccountRouter": "0xF15D70941dE2Bf95A23d6488eBCbedE0a444137f",
"interchainGasPaymaster": "0x0D63128D887159d63De29497dfa45AFc7C699AE4",
"interchainSecurityModule": "0xf2BEE9D2c15Ba9D7e06799B5912dE1F05533c141",
"mailbox": "0x3a464f746D23Ab22155710f44dB16dcA53e0775E",
@ -979,6 +1011,7 @@
"staticMerkleRootMultisigIsmFactory": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A",
"staticMessageIdMultisigIsmFactory": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6",
"storageGasOracle": "0x19dc38aeae620380430C200a6E990D5Af5480117",
"timelockController": "0x0000000000000000000000000000000000000000",
"validatorAnnounce": "0x2fa5F5C96419C222cDbCeC797D696e6cE428A7A9"
},
"scroll": {
@ -1005,6 +1038,8 @@
"index": {
"from": 271840
},
"interchainAccountIsm": "0xb89c6ED617f5F46175E41551350725A09110bbCE",
"interchainAccountRouter": "0x9629c28990F11c31735765A6FD59E1E1bC197DbD",
"interchainGasPaymaster": "0xBF12ef4B9f307463D3FB59c3604F294dDCe287E2",
"interchainSecurityModule": "0xaDc0cB48E8DB81855A930C0C1165ea3dCe4Ba5C7",
"mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7",
@ -1029,6 +1064,7 @@
"staticMerkleRootMultisigIsmFactory": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC",
"staticMessageIdMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE",
"storageGasOracle": "0x481171eb1aad17eDE6a56005B7F1aB00C581ef13",
"timelockController": "0x0000000000000000000000000000000000000000",
"transactionOverrides": {
"gasPrice": 2000000000
},
@ -1057,6 +1093,8 @@
"chunk": 1000,
"from": 73573878
},
"interchainAccountIsm": "0xD1E267d2d7876e97E217BfE61c34AB50FEF52807",
"interchainAccountRouter": "0x1956848601549de5aa0c887892061fA5aB4f6fC4",
"interchainGasPaymaster": "0x0D63128D887159d63De29497dfa45AFc7C699AE4",
"interchainSecurityModule": "0xBD70Ea9D599a0FC8158B026797177773C3445730",
"mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7",
@ -1085,6 +1123,7 @@
"storageGasOracle": "0x19dc38aeae620380430C200a6E990D5Af5480117",
"testRecipient": "0x17E216fBb22dF4ef8A6640ae9Cb147C92710ac84",
"testTokenRecipient": "0xe042D1fbDf59828dd16b9649Ede7abFc856F7a6c",
"timelockController": "0x0000000000000000000000000000000000000000",
"validatorAnnounce": "0x2fa5F5C96419C222cDbCeC797D696e6cE428A7A9"
}
},

@ -227,7 +227,7 @@
"domainRoutingIsmFactory": "0x54148470292C24345fb828B003461a9444414517",
"fallbackRoutingHook": "0x19Be55D859368e02d7b9C00803Eb677BDC1359Bd",
"index": {
"from": 4206
"from": 5284139
},
"interchainAccountIsm": "0x7c115c16E34c74afdb88bd268EaB19bC705891FE",
"interchainAccountRouter": "0xB6F8aA9B1b314A6E6DFB465DD3e0E95936347517",
@ -382,10 +382,10 @@
"solanatestnet": {
"blockExplorers": [
{
"apiUrl": "https://explorer.solana.com",
"apiUrl": "https://explorer.solana.com?cluster=testnet",
"family": "other",
"name": "Solana Explorer",
"url": "https://explorer.solana.com"
"url": "https://explorer.solana.com?cluster=testnet"
}
],
"blocks": {

@ -6,7 +6,9 @@ cd "$(dirname "$0")"
# Define the artifacts directory
artifactsDir="./artifacts/build-info"
# Define the output file
outputFile="./buildArtifact.json"
outputFileJson="./dist/buildArtifact.json"
outputFileJs="./dist/buildArtifact.js"
outputFileTsd="./dist/buildArtifact.d.ts"
# log that we're in the script
echo 'Finding and processing hardhat build artifact...'
@ -26,7 +28,10 @@ if [ ! -f "$jsonFiles" ]; then
fi
# Extract required keys and write to outputFile
if jq -c '{input, solcLongVersion}' "$jsonFiles" > "$outputFile"; then
if jq -c '{input, solcLongVersion}' "$jsonFiles" > "$outputFileJson"; then
echo "export const buildArtifact = " > "$outputFileJs"
cat "$outputFileJson" >> "$outputFileJs"
echo "export const buildArtifact: any" > "$outputFileTsd"
echo 'Finished processing build artifact.'
else
echo 'Failed to process build artifact with jq'

@ -37,12 +37,12 @@
"exports": {
".": "./dist/index.js",
"./mailbox": "./dist/contracts/Mailbox.js",
"./buildArtifact.json": "./buildArtifact.json",
"./buildArtifact.js": "./dist/buildArtifact.js",
"./buildArtifact.json": "./dist/buildArtifact.json",
"./contracts": "./contracts"
},
"types": "./dist/index.d.ts",
"files": [
"/buildArtifact.json",
"/dist",
"/contracts"
],
@ -57,7 +57,7 @@
],
"license": "Apache-2.0",
"scripts": {
"build": "yarn hardhat-esm compile && ./exportBuildArtifact.sh && tsc",
"build": "yarn hardhat-esm compile && tsc && ./exportBuildArtifact.sh",
"lint": "solhint contracts/**/*.sol",
"clean": "yarn hardhat-esm clean && rm -rf ./dist ./cache ./types ./coverage ./out ./forge-cache",
"coverage": "./coverage.sh",

@ -1,5 +1,13 @@
.env*
/dist
/cache
# Deployment artifacts and local registry configs
/configs
/artifacts
/artifacts
/chains
/deployments
# Test artifacts
/test-configs/**/addresses.yaml
/test-configs/*/deployments

@ -15,14 +15,11 @@ _main() {
# with the routing over igp hook (which is closer to production deployment)
TEST_TYPE=$1
if [ -z "$TEST_TYPE" ]; then
echo "Usage: ci-test.sh <test-type>"
echo "Usage: ci-test.sh <$TEST_TYPE_PRESET_HOOK | $TEST_TYPE_CONFIGURED_HOOK | $TEST_TYPE_PI_CORE>"
exit 1
fi
HOOK_FLAG=false
if [ "$TEST_TYPE" == $TEST_TYPE_CONFIGURED_HOOK ]; then
HOOK_FLAG=true
fi
prepare_environment_vars;
prepare_anvil;
@ -48,23 +45,36 @@ _main() {
echo "Done";
}
prepare_anvil() {
prepare_environment_vars() {
ANVIL_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
CHAIN1=anvil1
CHAIN2=anvil2
EXAMPLES_PATH=./examples
TEST_CONFIGS_PATH=./test-configs
CLI_PATH=./typescript/cli
REGISTRY_PATH="$TEST_CONFIGS_PATH/anvil"
CORE_ISM_PATH="$EXAMPLES_PATH/ism.yaml"
WARP_DEPLOY_CONFIG_PATH="$EXAMPLES_PATH/warp-route-deployment.yaml"
DEPLOY_ERC20_PATH=./src/tests/deployTestErc20.ts
# use different chain names and config for pi<>core test
if [ "$TEST_TYPE" == $TEST_TYPE_PI_CORE ]; then
CHAIN1=anvil
CHAIN2=ethereum
EXAMPLES_PATH=./examples/fork
REGISTRY_PATH="$TEST_CONFIGS_PATH/fork"
CORE_ISM_PATH="$REGISTRY_PATH/ism.yaml"
WARP_DEPLOY_CONFIG_PATH="$REGISTRY_PATH/warp-route-deployment.yaml"
fi
CHAIN1_CAPS=$(echo "${CHAIN1}" | tr '[:lower:]' '[:upper:]')
CHAIN2_CAPS=$(echo "${CHAIN2}" | tr '[:lower:]' '[:upper:]')
HOOK_FLAG=false
if [ "$TEST_TYPE" == $TEST_TYPE_CONFIGURED_HOOK ]; then
HOOK_FLAG=true
fi
}
prepare_anvil() {
CHAIN1_PORT=8545
CHAIN2_PORT=8555
@ -73,6 +83,8 @@ prepare_anvil() {
rm -rf /tmp/${CHAIN1}*
rm -rf /tmp/${CHAIN2}*
rm -rf /tmp/relayer
rm -f $CLI_PATH/$TEST_CONFIGS_PATH/*/chains/*/addresses.yaml
rm -rf $CLI_PATH/$TEST_CONFIGS_PATH/*/deployments
if [[ $OSTYPE == 'darwin'* ]]; then
# kill child processes on exit, but only locally because
@ -95,7 +107,7 @@ prepare_anvil() {
if [ "$TEST_TYPE" == $TEST_TYPE_PI_CORE ]; then
# Fetch the RPC of chain to fork
cd typescript/infra
RPC_URL=$(yarn tsx scripts/print-chain-metadatas.ts -e mainnet3 | jq -r ".${CHAIN2}.rpcUrls[0].http")
RPC_URL=$(LOG_LEVEL=error yarn tsx scripts/print-chain-metadatas.ts -e mainnet3 | jq -r ".${CHAIN2}.rpcUrls[0].http")
cd ../../
# run the fork chain
@ -112,8 +124,6 @@ prepare_anvil() {
fi
set -e
echo "{}" > /tmp/empty-artifacts.json
}
reset_anvil() {
@ -129,31 +139,20 @@ run_hyperlane_deploy_core_dry_run() {
return;
fi
BEFORE_CORE_DRY_RUN=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT});
update_deployer_balance;
echo -e "\nDry-running contract deployments to Alfajores"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy core \
--dry-run alfajores \
--targets alfajores \
--chains ${EXAMPLES_PATH}/dry-run/anvil-chains.yaml \
--artifacts /tmp/empty-artifacts.json \
--registry ${TEST_CONFIGS_PATH}/dry-run \
--overrides " " \
$(if [ "$HOOK_FLAG" == "true" ]; then echo "--hook ${EXAMPLES_PATH}/hooks.yaml"; fi) \
--ism ${EXAMPLES_PATH}/ism.yaml \
--out /tmp \
--ism ${TEST_CONFIGS_PATH}/dry-run/ism.yaml \
--key 0xfaD1C94469700833717Fa8a3017278BC1cA8031C \
--yes
AFTER_CORE_DRY_RUN=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT})
GAS_PRICE=$(cast gas-price --rpc-url http://127.0.0.1:${CHAIN1_PORT})
CORE_MIN_GAS=$(bc <<< "($BEFORE_CORE_DRY_RUN - $AFTER_CORE_DRY_RUN) / $GAS_PRICE")
echo "Gas used: $CORE_MIN_GAS"
CORE_ARTIFACTS_PATH=`find /tmp/core-deployment* -type f -exec ls -t1 {} + | head -1`
echo "Core artifacts:"
echo $CORE_ARTIFACTS_PATH
cat $CORE_ARTIFACTS_PATH
AGENT_CONFIG_FILENAME=`ls -t1 /tmp | grep agent-config | head -1`
check_deployer_balance;
}
run_hyperlane_deploy_warp_dry_run() {
@ -161,63 +160,44 @@ run_hyperlane_deploy_warp_dry_run() {
return;
fi
BEFORE_WARP_DRY_RUN=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT});
update_deployer_balance;
echo -e "\nDry-running warp route deployments to Alfajores"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy warp \
--dry-run alfajores \
--chains ${EXAMPLES_PATH}/dry-run/anvil-chains.yaml \
--core $CORE_ARTIFACTS_PATH \
--config ${EXAMPLES_PATH}/dry-run/warp-route-deployment.yaml \
--out /tmp \
--overrides ${TEST_CONFIGS_PATH}/dry-run \
--config ${TEST_CONFIGS_PATH}/dry-run/warp-route-deployment.yaml \
--key 0xfaD1C94469700833717Fa8a3017278BC1cA8031C \
--yes
AFTER_WARP_DRY_RUN=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT})
GAS_PRICE=$(cast gas-price --rpc-url http://127.0.0.1:${CHAIN1_PORT})
WARP_MIN_GAS=$(bc <<< "($BEFORE_WARP_DRY_RUN - $AFTER_WARP_DRY_RUN) / $GAS_PRICE")
echo "Gas used: $WARP_MIN_GAS"
WARP_ARTIFACTS_PATH=`find /tmp/dry-run_warp-route-deployment* -type f -exec ls -t1 {} + | head -1`
echo "Warp dry-run artifacts:"
echo $WARP_ARTIFACTS_PATH
cat $WARP_ARTIFACTS_PATH
check_deployer_balance;
}
run_hyperlane_deploy_core() {
BEFORE_CORE=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT});
update_deployer_balance;
echo -e "\nDeploying contracts to ${CHAIN1} and ${CHAIN2}"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy core \
--registry $REGISTRY_PATH \
--overrides " " \
--targets ${CHAIN1},${CHAIN2} \
--chains ${EXAMPLES_PATH}/anvil-chains.yaml \
--artifacts /tmp/empty-artifacts.json \
$(if [ "$HOOK_FLAG" == "true" ]; then echo "--hook ${EXAMPLES_PATH}/hooks.yaml"; fi) \
--ism ${EXAMPLES_PATH}/ism.yaml \
--out /tmp \
--ism $CORE_ISM_PATH \
--agent /tmp/agent-config.json \
--key $ANVIL_KEY \
--yes
AFTER_CORE=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT})
GAS_PRICE=$(cast gas-price --rpc-url http://127.0.0.1:${CHAIN1_PORT})
CORE_MIN_GAS=$(bc <<< "($BEFORE_CORE - $AFTER_CORE) / $GAS_PRICE")
echo "Gas used: $CORE_MIN_GAS"
CORE_ARTIFACTS_PATH=`find /tmp/core-deployment* -type f -exec ls -t1 {} + | head -1`
echo "Core artifacts:"
echo $CORE_ARTIFACTS_PATH
cat $CORE_ARTIFACTS_PATH
AGENT_CONFIG_FILENAME=`ls -t1 /tmp | grep agent-config | head -1`
check_deployer_balance;
}
run_hyperlane_deploy_warp() {
update_deployer_balance;
echo -e "\nDeploying hypNative warp route"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy warp \
--chains ${EXAMPLES_PATH}/anvil-chains.yaml \
--core $CORE_ARTIFACTS_PATH \
--config ${EXAMPLES_PATH}/warp-route-deployment.yaml \
--out /tmp \
--registry $REGISTRY_PATH \
--overrides " " \
--config $WARP_DEPLOY_CONFIG_PATH \
--key $ANVIL_KEY \
--yes
@ -228,51 +208,43 @@ run_hyperlane_deploy_warp() {
echo "Deploying hypCollateral warp route"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy warp \
--chains ${EXAMPLES_PATH}/anvil-chains.yaml \
--core $CORE_ARTIFACTS_PATH \
--registry $REGISTRY_PATH \
--overrides " " \
--config /tmp/warp-collateral-deployment.json \
--out /tmp \
--key $ANVIL_KEY \
--yes
AFTER_WARP=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT})
GAS_PRICE=$(cast gas-price --rpc-url http://127.0.0.1:${CHAIN1_PORT})
WARP_MIN_GAS=$(bc <<< "($AFTER_CORE - $AFTER_WARP) / $GAS_PRICE")
echo "Gas used: $WARP_MIN_GAS"
check_deployer_balance;
}
run_hyperlane_send_message() {
update_deployer_balance;
echo -e "\nSending test message"
yarn workspace @hyperlane-xyz/cli run hyperlane send message \
--registry $REGISTRY_PATH \
--overrides " " \
--origin ${CHAIN1} \
--destination ${CHAIN2} \
--messageBody "Howdy!" \
--chains ${EXAMPLES_PATH}/anvil-chains.yaml \
--core $CORE_ARTIFACTS_PATH \
--body "Howdy!" \
--quick \
--key $ANVIL_KEY \
| tee /tmp/message1
AFTER_MSG=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT})
GAS_PRICE=$(cast gas-price --rpc-url http://127.0.0.1:${CHAIN1_PORT})
MSG_MIN_GAS=$(bc <<< "($AFTER_WARP - $AFTER_MSG) / $GAS_PRICE")
echo "Gas used: $MSG_MIN_GAS"
check_deployer_balance;
MESSAGE1_ID=`cat /tmp/message1 | grep "Message ID" | grep -E -o '0x[0-9a-f]+'`
echo "Message 1 ID: $MESSAGE1_ID"
WARP_CONFIG_FILE=`find /tmp/warp-config* -type f -exec ls -t1 {} + | head -1`
CHAIN1_ROUTER="${CHAIN1_CAPS}_ROUTER"
declare $CHAIN1_ROUTER=$(jq -r --arg CHAIN1 "$CHAIN1" '.tokens[] | select(.chainName==$CHAIN1) | .addressOrDenom' $WARP_CONFIG_FILE)
WARP_CONFIG_FILE="$REGISTRY_PATH/deployments/warp_routes/FAKE/${CHAIN1}-${CHAIN2}-config.yaml"
echo -e "\nSending test warp transfer"
yarn workspace @hyperlane-xyz/cli run hyperlane send transfer \
--registry $REGISTRY_PATH \
--overrides " " \
--origin ${CHAIN1} \
--destination ${CHAIN2} \
--chains ${EXAMPLES_PATH}/anvil-chains.yaml \
--core $CORE_ARTIFACTS_PATH \
--warp ${WARP_CONFIG_FILE} \
--router ${!CHAIN1_ROUTER} \
--quick \
--key $ANVIL_KEY \
| tee /tmp/message2
@ -303,7 +275,7 @@ run_validator() {
VALIDATOR_PORT=$((VALIDATOR_PORT+1))
echo "Running validator on $CHAIN on port $VALIDATOR_PORT"
export CONFIG_FILES=/tmp/${AGENT_CONFIG_FILENAME}
export CONFIG_FILES=/tmp/agent-config.json
export HYP_ORIGINCHAINNAME=${CHAIN}
export HYP_VALIDATOR_INTERVAL=1
export HYP_VALIDATOR_TYPE=hexKey
@ -340,7 +312,7 @@ run_relayer() {
cargo build --bin relayer
echo "Running relayer"
export CONFIG_FILES=/tmp/${AGENT_CONFIG_FILENAME}
export CONFIG_FILES=/tmp/agent-config.json
export HYP_RELAYCHAINS=${CHAIN1},${CHAIN2}
export HYP_ALLOWLOCALCHECKPOINTSYNCERS=true
export HYP_DB=/tmp/relayer
@ -367,8 +339,8 @@ run_hyperlane_status() {
yarn workspace @hyperlane-xyz/cli run hyperlane status \
--id $2 \
--destination ${CHAIN2} \
--chains ${EXAMPLES_PATH}/anvil-chains.yaml \
--core $CORE_ARTIFACTS_PATH \
--registry $REGISTRY_PATH \
--overrides " " \
| tee /tmp/message-status-$1
if ! grep -q "$2 was delivered" /tmp/message-status-$1; then
echo "ERROR: Message $1 was not delivered"
@ -379,6 +351,17 @@ run_hyperlane_status() {
done
}
update_deployer_balance() {
OLD_BALANCE=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT});
}
check_deployer_balance() {
NEW_BALANCE=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT})
GAS_PRICE=$(cast gas-price --rpc-url http://127.0.0.1:${CHAIN1_PORT})
GAS_USED=$(bc <<< "($OLD_BALANCE - $NEW_BALANCE) / $GAS_PRICE")
echo "Gas used: $GAS_USED"
}
_main "$@";
exit;

@ -11,11 +11,16 @@ import { deployCommand } from './src/commands/deploy.js';
import { hookCommand } from './src/commands/hook.js';
import { ismCommand } from './src/commands/ism.js';
import {
keyCommandOption,
logFormatCommandOption,
logLevelCommandOption,
overrideRegistryUriCommandOption,
registryUriCommandOption,
skipConfirmationOption,
} from './src/commands/options.js';
import { sendCommand } from './src/commands/send.js';
import { statusCommand } from './src/commands/status.js';
import { contextMiddleware } from './src/context/context.js';
import { configureLogger, errorRed } from './src/logger.js';
import { checkVersion } from './src/utils/version-check.js';
import { VERSION } from './src/version.js';
@ -32,10 +37,17 @@ try {
.scriptName('hyperlane')
.option('log', logFormatCommandOption)
.option('verbosity', logLevelCommandOption)
.global(['log', 'verbosity'])
.middleware((argv) => {
configureLogger(argv.log as LogFormat, argv.verbosity as LogLevel);
})
.option('registry', registryUriCommandOption)
.option('overrides', overrideRegistryUriCommandOption)
.option('key', keyCommandOption)
.option('yes', skipConfirmationOption)
.global(['log', 'verbosity', 'registry', 'overrides', 'yes'])
.middleware([
(argv) => {
configureLogger(argv.log as LogFormat, argv.verbosity as LogLevel);
},
contextMiddleware,
])
.command(chainsCommand)
.command(configCommand)
.command(deployCommand)

@ -1,22 +0,0 @@
# Configs for describing chain metadata for use in Hyperlane deployments or apps
# Consists of a map of chain names to metadata
# Schema here: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/metadata/chainMetadataTypes.ts
---
anvil1:
chainId: 31337
domainId: 31337
name: anvil1
protocol: ethereum
rpcUrls:
- http: http://127.0.0.1:8545
nativeToken:
name: Ether
symbol: ETH
decimals: 18
anvil2:
chainId: 31338
domainId: 31338
name: anvil2
protocol: ethereum
rpcUrls:
- http: http://127.0.0.1:8555

@ -1,50 +1,42 @@
# Configs for describing chain metadata for use in Hyperlane deployments or apps
# Consists of a map of chain names to metadata
# Schema here: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/metadata/chainMetadataTypes.ts
---
# You can define a full config for a new chain
mychainname:
# Required fields:
chainId: 1234567890 # Number: Use EIP-155 for EVM chains
domainId: 1234567890 # Number: Recommend matching chainId when possible
name: mychainname # String: Unique identifier for the chain, must match key above
protocol: ethereum # ProtocolType: Ethereum, Sealevel, etc.
rpcUrls: # Array: List of RPC configs
# Only http field is required
- http: https://mychain.com/rpc # String: HTTP URL of the RPC endpoint (preferably HTTPS)
# Others here are optional
pagination:
maxBlockRange: 1000 # Number
maxBlockAge: 1000 # Number
minBlockNumber: 1000 # Number
retry:
maxRequests: 5 # Number
baseRetryMs: 1000 # Number
# Optional fields, not required for Hyperlane deployments but useful for apps:
isTestnet: false # Boolean: Whether the chain is considered a testnet or a mainnet
blockExplorers: # Array: List of BlockExplorer configs
# Required fields:
chainId: 1234567890 # Number: Use EIP-155 for EVM chains
domainId: 1234567890 # Number: Recommend matching chainId when possible
name: mychainname # String: Unique identifier for the chain, must match key above
protocol: ethereum # ProtocolType: Ethereum, Sealevel, etc.
rpcUrls: # Array: List of RPC configs
# Only http field is required
- http: https://mychain.com/rpc # String: HTTP URL of the RPC endpoint (preferably HTTPS)
# Others here are optional
pagination:
maxBlockRange: 1000 # Number
maxBlockAge: 1000 # Number
minBlockNumber: 1000 # Number
retry:
maxRequests: 5 # Number
baseRetryMs: 1000 # Number
# Optional fields, not required for Hyperlane deployments but useful for apps:
isTestnet: false # Boolean: Whether the chain is considered a testnet or a mainnet
blockExplorers: # Array: List of BlockExplorer configs
# Required fields:
- name: My Chain Explorer # String: Human-readable name for the explorer
url: https://mychain.com/explorer # String: Base URL for the explorer
apiUrl: https://mychain.com/api # String: Base URL for the explorer API
# Optional fields:
apiKey: myapikey # String: API key for the explorer (optional)
family: etherscan # ExplorerFamily: See ExplorerFamily for valid values
nativeToken:
name: Eth # String
symbol: ETH # String
decimals: 18 # Number
displayName: My Chain Name # String: Human-readable name of the chain
displayNameShort: My Chain # String: A shorter human-readable name
logoURI: https://mychain.com/logo.png # String: URI to a logo image for the chain
blocks:
confirmations: 12 # Number: Blocks to wait before considering a transaction confirmed
reorgPeriod: 100 # Number: Blocks before a transaction has a near-zero chance of reverting
estimateBlockTime: 15 # Number: Rough estimate of time per block in seconds
# transactionOverrides: # Object: Properties to include when forming transaction requests
# Any tx fields are allowed
# Alternatively, you can extend a core chain config with only fields to be overridden
sepolia:
rpcUrls:
- http: https://mycustomrpc.com
- name: My Chain Explorer # String: Human-readable name for the explorer
url: https://mychain.com/explorer # String: Base URL for the explorer
apiUrl: https://mychain.com/api # String: Base URL for the explorer API
# Optional fields:
apiKey: myapikey # String: API key for the explorer (optional)
family: etherscan # ExplorerFamily: See ExplorerFamily for valid values
nativeToken:
name: Eth # String
symbol: ETH # String
decimals: 18 # Number
displayName: My Chain Name # String: Human-readable name of the chain
displayNameShort: My Chain # String: A shorter human-readable name
logoURI: https://mychain.com/logo.png # String: URI to a logo image for the chain
blocks:
confirmations: 12 # Number: Blocks to wait before considering a transaction confirmed
reorgPeriod: 100 # Number: Blocks before a transaction has a near-zero chance of reverting
estimateBlockTime: 15 # Number: Rough estimate of time per block in seconds
# transactionOverrides: # Object: Properties to include when forming transaction requests
# Any tx fields are allowed

@ -1,17 +0,0 @@
anvil:
chainId: 31337
domainId: 31337
name: anvil
protocol: ethereum
rpcUrls:
- http: http://127.0.0.1:8545
nativeToken:
name: Ether
symbol: ETH
decimals: 18
alfajores:
rpcUrls:
- http: https://alfajores-forno.celo-testnet.org
blocks:
confirmations: 1
estimateBlockTime: 1

@ -1,21 +0,0 @@
# Configs for describing chain metadata for use in Hyperlane deployments or apps
# Consists of a map of chain names to metadata
# Schema here: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/metadata/chainMetadataTypes.ts
---
anvil:
chainId: 31337
domainId: 31337
name: anvil
protocol: ethereum
rpcUrls:
- http: http://127.0.0.1:8545
nativeToken:
name: Ether
symbol: ETH
decimals: 18
ethereum:
rpcUrls:
- http: http://127.0.0.1:8555
blocks:
confirmations: 1
estimateBlockTime: 1

@ -3,6 +3,7 @@
"version": "3.10.0",
"description": "A command-line utility for common Hyperlane operations",
"dependencies": {
"@hyperlane-xyz/registry": "^1.0.7",
"@hyperlane-xyz/sdk": "3.10.0",
"@hyperlane-xyz/utils": "3.10.0",
"@inquirer/prompts": "^3.0.0",
@ -12,7 +13,7 @@
"latest-version": "^8.0.0",
"terminal-link": "^3.0.0",
"tsx": "^4.7.1",
"yaml": "^2.3.1",
"yaml": "^2.4.1",
"yargs": "^17.7.2",
"zod": "^3.21.2"
},

@ -1,22 +1,17 @@
import { CommandModule } from 'yargs';
import {
Chains,
CoreChainName,
HyperlaneEnvironment,
chainMetadata,
hyperlaneContractAddresses,
hyperlaneEnvironments,
} from '@hyperlane-xyz/sdk';
import { CommandModuleWithContext } from '../context/types.js';
import { log, logBlue, logGray, logTable } from '../logger.js';
const ChainTypes = ['mainnet', 'testnet'];
type ChainType = (typeof ChainTypes)[number];
/**
* Parent command
*/
export const chainsCommand: CommandModule = {
command: 'chains',
describe: 'View information about core Hyperlane chains',
describe: 'View information about Hyperlane chains in a registry',
builder: (yargs) =>
yargs
.command(listCommand)
@ -29,39 +24,39 @@ export const chainsCommand: CommandModule = {
/**
* List command
*/
const listCommand: CommandModule = {
const listCommand: CommandModuleWithContext<{ type: ChainType }> = {
command: 'list',
describe: 'List all core chains included in the Hyperlane SDK',
builder: (yargs) =>
yargs.option('environment', {
alias: 'e',
describe: 'Specify the environment to list chains for',
choices: ['mainnet', 'testnet'],
}),
handler: (args) => {
const environment = args.environment as HyperlaneEnvironment | undefined;
const serializer = (env: HyperlaneEnvironment) =>
Object.keys(hyperlaneEnvironments[env]).reduce<any>((result, chain) => {
const { chainId, displayName } = chainMetadata[chain];
result[chain] = {
describe: 'List all chains included in a registry',
builder: {
type: {
describe: 'Specify the type of chains',
choices: ChainTypes,
},
},
handler: async ({ type, context }) => {
const logChainsForType = (type: ChainType) => {
logBlue(`\nHyperlane ${type} chains:`);
logGray('------------------------------');
const chains = Object.values(context.chainMetadata).filter((c) => {
if (type === 'mainnet') return !c.isTestnet;
else return !!c.isTestnet;
});
const tableData = chains.reduce<any>((result, chain) => {
const { chainId, displayName } = chain;
result[chain.name] = {
'Display Name': displayName,
'Chain Id': chainId,
};
return result;
}, {});
const logChainsForEnv = (env: HyperlaneEnvironment) => {
logBlue(`\nHyperlane core ${env} chains:`);
logGray('------------------------------');
logTable(serializer(env));
logTable(tableData);
};
if (environment) {
logChainsForEnv(environment);
if (type) {
logChainsForType(type);
} else {
logChainsForEnv('mainnet');
logChainsForEnv('testnet');
logChainsForType('mainnet');
logChainsForType('testnet');
}
},
};
@ -69,28 +64,27 @@ const listCommand: CommandModule = {
/**
* Addresses command
*/
const addressesCommand: CommandModule = {
const addressesCommand: CommandModuleWithContext<{ name: string }> = {
command: 'addresses',
describe: 'Display the addresses of core Hyperlane contracts',
builder: (yargs) =>
yargs.options({
name: {
type: 'string',
description: 'Chain to display addresses for',
choices: Object.values(Chains),
alias: 'chain',
},
}),
handler: (args) => {
const name = args.name as CoreChainName | undefined;
if (name && hyperlaneContractAddresses[name]) {
builder: {
name: {
type: 'string',
description: 'Chain to display addresses for',
alias: 'chain',
},
},
handler: async ({ name, context }) => {
if (name) {
const result = await context.registry.getChainAddresses(name);
logBlue('Hyperlane contract addresses for:', name);
logGray('---------------------------------');
log(JSON.stringify(hyperlaneContractAddresses[name], null, 2));
log(JSON.stringify(result, null, 2));
} else {
logBlue('Hyperlane core contract addresses:');
const result = await context.registry.getAddresses();
logBlue('Hyperlane contract addresses:');
logGray('----------------------------------');
log(JSON.stringify(hyperlaneContractAddresses, null, 2));
log(JSON.stringify(result, null, 2));
}
},
};

@ -11,14 +11,10 @@ import {
createWarpRouteDeployConfig,
readWarpRouteDeployConfig,
} from '../config/warp.js';
import { CommandModuleWithContext } from '../context/types.js';
import { log, logGreen } from '../logger.js';
import { FileFormat } from '../utils/files.js';
import {
chainsCommandOption,
fileFormatOption,
outputFileOption,
} from './options.js';
import { inputFileOption, outputFileOption } from './options.js';
/**
* Parent command
@ -52,84 +48,61 @@ const createCommand: CommandModule = {
handler: () => log('Command required'),
};
const createChainConfigCommand: CommandModule = {
const createChainConfigCommand: CommandModuleWithContext<{}> = {
command: 'chain',
describe: 'Create a new, minimal Hyperlane chain config (aka chain metadata)',
builder: (yargs) =>
yargs.options({
output: outputFileOption('./configs/chains.yaml'),
format: fileFormatOption,
}),
handler: async (argv: any) => {
const format: FileFormat = argv.format;
const outPath: string = argv.output;
await createChainConfig({ format, outPath });
handler: async ({ context }) => {
await createChainConfig({ context });
process.exit(0);
},
};
const createIsmConfigCommand: CommandModule = {
const createIsmConfigCommand: CommandModuleWithContext<{
out: string;
advanced: boolean;
}> = {
command: 'ism',
describe: 'Create a basic or advanced ISM config for a validator set',
builder: (yargs) =>
yargs.options({
output: outputFileOption('./configs/ism.yaml'),
format: fileFormatOption,
chains: chainsCommandOption,
advanced: {
type: 'boolean',
describe: 'Create an advanced ISM configuration',
default: false,
},
}),
handler: async (argv: any) => {
const format: FileFormat = argv.format;
const outPath: string = argv.output;
const chainConfigPath: string = argv.chains;
const isAdvanced: boolean = argv.advanced;
if (isAdvanced) {
await createIsmConfigMap({ format, outPath, chainConfigPath });
builder: {
out: outputFileOption('./configs/ism.yaml'),
advanced: {
type: 'boolean',
describe: 'Create an advanced ISM configuration',
default: false,
},
},
handler: async ({ context, out, advanced }) => {
if (advanced) {
await createIsmConfigMap({ context, outPath: out });
} else {
await createMultisigConfig({ format, outPath, chainConfigPath });
await createMultisigConfig({ context, outPath: out });
}
process.exit(0);
},
};
const createHookConfigCommand: CommandModule = {
const createHookConfigCommand: CommandModuleWithContext<{ out: string }> = {
command: 'hooks',
describe: 'Create a new hooks config (required & default)',
builder: (yargs) =>
yargs.options({
output: outputFileOption('./configs/hooks.yaml'),
format: fileFormatOption,
chains: chainsCommandOption,
}),
handler: async (argv: any) => {
const format: FileFormat = argv.format;
const outPath: string = argv.output;
const chainConfigPath: string = argv.chains;
await createHooksConfigMap({ format, outPath, chainConfigPath });
builder: {
out: outputFileOption('./configs/hooks.yaml'),
},
handler: async ({ context, out }) => {
await createHooksConfigMap({ context, outPath: out });
process.exit(0);
},
};
const createWarpRouteDeployConfigCommand: CommandModule = {
const createWarpRouteDeployConfigCommand: CommandModuleWithContext<{
out: string;
}> = {
command: 'warp',
describe: 'Create a new Warp Route deployment config',
builder: (yargs) =>
yargs.options({
output: outputFileOption('./configs/warp-route-deployment.yaml'),
format: fileFormatOption,
chains: chainsCommandOption,
}),
handler: async (argv: any) => {
const format: FileFormat = argv.format;
const outPath: string = argv.output;
const chainConfigPath: string = argv.chains;
await createWarpRouteDeployConfig({ format, outPath, chainConfigPath });
builder: {
out: outputFileOption('./configs/warp-route-deployment.yaml'),
},
handler: async ({ context, out }) => {
await createWarpRouteDeployConfig({ context, outPath: out });
process.exit(0);
},
};
@ -151,75 +124,52 @@ const validateCommand: CommandModule = {
handler: () => log('Command required'),
};
const validateChainCommand: CommandModule = {
const validateChainCommand: CommandModuleWithContext<{ path: string }> = {
command: 'chain',
describe: 'Validate a chain config in a YAML or JSON file',
builder: (yargs) =>
yargs.options({
path: {
type: 'string',
description: 'Input file path',
demandOption: true,
},
}),
handler: async (argv) => {
const path = argv.path as string;
describe: 'Validate a chain config file',
builder: {
path: inputFileOption,
},
handler: async ({ path }) => {
readChainConfigs(path);
logGreen(`All chain configs in ${path} are valid`);
process.exit(0);
},
};
const validateIsmCommand: CommandModule = {
const validateIsmCommand: CommandModuleWithContext<{ path: string }> = {
command: 'ism',
describe: 'Validate the basic ISM config in a YAML or JSON file',
builder: (yargs) =>
yargs.options({
path: {
type: 'string',
description: 'Input file path',
demandOption: true,
},
}),
handler: async (argv) => {
const path = argv.path as string;
describe: 'Validate the basic ISM config file',
builder: {
path: inputFileOption,
},
handler: async ({ path }) => {
readMultisigConfig(path);
logGreen('Config is valid');
process.exit(0);
},
};
const validateIsmAdvancedCommand: CommandModule = {
const validateIsmAdvancedCommand: CommandModuleWithContext<{ path: string }> = {
command: 'ism-advanced',
describe: 'Validate the advanced ISM config in a YAML or JSON file',
builder: (yargs) =>
yargs.options({
path: {
type: 'string',
description: 'Input file path',
demandOption: true,
},
}),
handler: async (argv) => {
const path = argv.path as string;
describe: 'Validate the advanced ISM config file',
builder: {
path: inputFileOption,
},
handler: async ({ path }) => {
readIsmConfig(path);
logGreen('Config is valid');
process.exit(0);
},
};
const validateWarpCommand: CommandModule = {
const validateWarpCommand: CommandModuleWithContext<{ path: string }> = {
command: 'warp',
describe: 'Validate a Warp Route config in a YAML or JSON file',
builder: (yargs) =>
yargs.options({
path: {
type: 'string',
description: 'Input file path',
demandOption: true,
},
}),
handler: async (argv) => {
const path = argv.path as string;
describe: 'Validate a Warp Route deployment config file',
builder: {
path: inputFileOption,
},
handler: async ({ path }) => {
readWarpRouteDeployConfig(path);
logGreen('Config is valid');
process.exit(0);

@ -1,5 +1,9 @@
import { CommandModule } from 'yargs';
import {
CommandModuleWithContext,
CommandModuleWithWriteContext,
} from '../context/types.js';
import { runKurtosisAgentDeploy } from '../deploy/agent.js';
import { runCoreDeploy } from '../deploy/core.js';
import { evaluateIfDryRunFailure, verifyAnvil } from '../deploy/dry-run.js';
@ -7,36 +11,21 @@ import { runWarpRouteDeploy } from '../deploy/warp.js';
import { log, logGray } from '../logger.js';
import {
AgentCommandOptions,
CoreCommandOptions,
WarpCommandOptions,
agentConfigCommandOption,
agentTargetsCommandOption,
chainsCommandOption,
coreArtifactsOption,
coreTargetsCommandOption,
dryRunOption,
hookCommandOption,
ismCommandOption,
keyCommandOption,
originCommandOption,
outDirCommandOption,
skipConfirmationOption,
warpConfigCommandOption,
} from './options.js';
export enum Command {
DEPLOY = 'deploy',
KURTOSIS_AGENTS = 'kurtosis-agents',
CORE = 'core',
WARP = 'warp',
}
/**
* Parent command
*/
export const deployCommand: CommandModule = {
command: Command.DEPLOY,
command: 'deploy',
describe: 'Permissionlessly deploy a Hyperlane contracts or extensions',
builder: (yargs) =>
yargs
@ -51,28 +40,26 @@ export const deployCommand: CommandModule = {
/**
* Agent command
*/
const agentCommand: CommandModule = {
command: Command.KURTOSIS_AGENTS,
const agentCommand: CommandModuleWithContext<{
origin?: string;
targets?: string;
config?: string;
}> = {
command: 'kurtosis-agents',
describe: 'Deploy Hyperlane agents with Kurtosis',
builder: (yargs) =>
yargs.options<AgentCommandOptions>({
origin: originCommandOption,
targets: agentTargetsCommandOption,
chains: chainsCommandOption,
config: agentConfigCommandOption,
}),
handler: async (argv: any) => {
builder: {
origin: originCommandOption,
targets: agentTargetsCommandOption,
config: agentConfigCommandOption(true),
},
handler: async ({ context, origin, targets, config }) => {
logGray('Hyperlane Agent Deployment with Kurtosis');
logGray('----------------------------------------');
const chainConfigPath: string = argv.chains;
const originChain: string = argv.origin;
const agentConfigurationPath: string = argv.config;
const relayChains: string = argv.targets;
await runKurtosisAgentDeploy({
originChain,
relayChains,
chainConfigPath,
agentConfigurationPath,
context,
originChain: origin,
relayChains: targets,
agentConfigurationPath: config,
});
process.exit(0);
},
@ -81,34 +68,23 @@ const agentCommand: CommandModule = {
/**
* Core command
*/
const coreCommand: CommandModule = {
command: Command.CORE,
const coreCommand: CommandModuleWithWriteContext<{
targets: string;
ism?: string;
hook?: string;
'dry-run': boolean;
agent: string;
}> = {
command: 'core',
describe: 'Deploy core Hyperlane contracts',
builder: (yargs) =>
yargs.options<CoreCommandOptions>({
targets: coreTargetsCommandOption,
chains: chainsCommandOption,
artifacts: coreArtifactsOption,
ism: ismCommandOption,
hook: hookCommandOption,
out: outDirCommandOption,
key: keyCommandOption,
yes: skipConfirmationOption,
'dry-run': dryRunOption,
}),
handler: async (argv: any) => {
const key: string | undefined = argv.key;
const chainConfigPath: string = argv.chains;
const outPath: string = argv.out;
const chains: string[] | undefined = argv.targets
?.split(',')
.map((r: string) => r.trim());
const artifactsPath: string = argv.artifacts;
const ismConfigPath: string = argv.ism;
const hookConfigPath: string = argv.hook;
const skipConfirmation: boolean = argv.yes;
const dryRun: string = argv.dryRun;
builder: {
targets: coreTargetsCommandOption,
ism: ismCommandOption,
hook: hookCommandOption,
agent: agentConfigCommandOption(false, './configs/agent.json'),
'dry-run': dryRunOption,
},
handler: async ({ context, targets, ism, hook, agent, dryRun }) => {
logGray(
`Hyperlane permissionless core deployment${dryRun ? ' dry-run' : ''}`,
);
@ -117,16 +93,13 @@ const coreCommand: CommandModule = {
if (dryRun) await verifyAnvil();
try {
const chains = targets?.split(',').map((r: string) => r.trim());
await runCoreDeploy({
key,
chainConfigPath,
context,
chains,
artifactsPath,
ismConfigPath,
hookConfigPath,
outPath,
skipConfirmation,
dryRun,
ismConfigPath: ism,
hookConfigPath: hook,
agentOutPath: agent,
});
} catch (error: any) {
evaluateIfDryRunFailure(error, dryRun);
@ -139,28 +112,17 @@ const coreCommand: CommandModule = {
/**
* Warp command
*/
const warpCommand: CommandModule = {
command: Command.WARP,
const warpCommand: CommandModuleWithWriteContext<{
config: string;
'dry-run': boolean;
}> = {
command: 'warp',
describe: 'Deploy Warp Route contracts',
builder: (yargs) =>
yargs.options<WarpCommandOptions>({
config: warpConfigCommandOption,
core: coreArtifactsOption,
chains: chainsCommandOption,
out: outDirCommandOption,
key: keyCommandOption,
yes: skipConfirmationOption,
'dry-run': dryRunOption,
}),
handler: async (argv: any) => {
const key: string | undefined = argv.key;
const chainConfigPath: string = argv.chains;
const warpRouteDeploymentConfigPath: string | undefined = argv.config;
const coreArtifactsPath: string | undefined = argv.core;
const outPath: string = argv.out;
const skipConfirmation: boolean = argv.yes;
const dryRun: string = argv.dryRun;
builder: {
config: warpConfigCommandOption,
'dry-run': dryRunOption,
},
handler: async ({ context, config, dryRun }) => {
logGray(`Hyperlane warp route deployment${dryRun ? ' dry-run' : ''}`);
logGray('------------------------------------------------');
@ -168,13 +130,8 @@ const warpCommand: CommandModule = {
try {
await runWarpRouteDeploy({
key,
chainConfigPath,
warpRouteDeploymentConfigPath,
coreArtifactsPath,
outPath,
skipConfirmation,
dryRun,
context,
warpRouteDeploymentConfigPath: config,
});
} catch (error: any) {
evaluateIfDryRunFailure(error, dryRun);

@ -1,13 +1,12 @@
import { CommandModule } from 'yargs';
import { CommandModuleWithContext } from '../context/types.js';
import { readHookConfig } from '../hook/read.js';
import { log } from '../logger.js';
import {
addressCommandOption,
chainCommandOption,
chainsCommandOption,
fileFormatOption,
outputFileOption,
} from './options.js';
@ -26,28 +25,23 @@ export const hookCommand: CommandModule = {
// hyperlane hook read --chain polygon --address 0xca4cCe24E7e06241846F5EA0cda9947F0507C40C
// IGP hook on inevm (may take 5s):
// hyperlane hook read --chain inevm --address 0x19dc38aeae620380430C200a6E990D5Af5480117
export const read: CommandModule = {
export const read: CommandModuleWithContext<{
chain: string;
address: string;
out: string;
}> = {
command: 'read',
describe: 'Reads onchain Hook configuration for a given address',
builder: (yargs) =>
yargs.options({
chains: chainsCommandOption,
chain: {
...chainCommandOption,
demandOption: true,
},
address: addressCommandOption('Address of the Hook to read.', true),
format: fileFormatOption,
output: outputFileOption(),
}),
handler: async (argv: any) => {
await readHookConfig({
chain: argv.chain,
address: argv.address,
chainConfigPath: argv.chains,
format: argv.format,
output: argv.output,
});
builder: {
chain: {
...chainCommandOption,
demandOption: true,
},
address: addressCommandOption('Address of the Hook to read.', true),
out: outputFileOption(),
},
handler: async (args) => {
await readHookConfig(args);
process.exit(0);
},
};

@ -1,13 +1,12 @@
import { CommandModule } from 'yargs';
import { CommandModuleWithContext } from '../context/types.js';
import { readIsmConfig } from '../ism/read.js';
import { log } from '../logger.js';
import {
addressCommandOption,
chainCommandOption,
chainsCommandOption,
fileFormatOption,
outputFileOption,
} from './options.js';
@ -28,31 +27,26 @@ export const ismCommand: CommandModule = {
// hyperlane ism read --chain inevm --address 0x79A7c7Fe443971CBc6baD623Fdf8019C379a7178
// Test ISM on alfajores testnet
// hyperlane ism read --chain alfajores --address 0xdB52E4853b6A40D2972E6797E0BDBDb3eB761966
export const read: CommandModule = {
export const read: CommandModuleWithContext<{
chain: string;
address: string;
out: string;
}> = {
command: 'read',
describe: 'Reads onchain ISM configuration for a given address',
builder: (yargs) =>
yargs.options({
chains: chainsCommandOption,
chain: {
...chainCommandOption,
demandOption: true,
},
address: addressCommandOption(
'Address of the Interchain Security Module to read.',
true,
),
format: fileFormatOption,
output: outputFileOption(),
}),
handler: async (argv: any) => {
await readIsmConfig({
chain: argv.chain,
address: argv.address,
chainConfigPath: argv.chains,
format: argv.format,
output: argv.output,
});
builder: {
chain: {
...chainCommandOption,
demandOption: true,
},
address: addressCommandOption(
'Address of the Interchain Security Module to read.',
true,
),
out: outputFileOption(),
},
handler: async (argv) => {
await readIsmConfig(argv);
process.exit(0);
},
};

@ -1,9 +1,12 @@
import { Options } from 'yargs';
import { DEFAULT_GITHUB_REGISTRY } from '@hyperlane-xyz/registry';
import { LogFormat, LogLevel } from '@hyperlane-xyz/utils';
import { ENV } from '../utils/env.js';
/* Global options */
export const logFormatCommandOption: Options = {
type: 'string',
description: 'Log output format',
@ -16,33 +19,36 @@ export const logLevelCommandOption: Options = {
choices: Object.values(LogLevel),
};
export type CommandOptions = {
chains: Options;
export const registryUriCommandOption: Options = {
type: 'string',
description: 'Registry URI, such as a Github repo URL or a local file path',
alias: 'r',
default: DEFAULT_GITHUB_REGISTRY,
};
export type AgentCommandOptions = CommandOptions & {
origin: Options;
targets: Options;
config: Options;
export const overrideRegistryUriCommandOption: Options = {
type: 'string',
description: 'Path to a local registry to override the default registry',
default: './',
};
export type CoreCommandOptions = CommandOptions & {
targets: Options;
artifacts: Options;
ism: Options;
hook: Options;
out: Options;
key: Options;
yes: Options;
'dry-run': Options;
export const skipConfirmationOption: Options = {
type: 'boolean',
description: 'Skip confirmation prompts',
default: false,
alias: 'y',
};
export type WarpCommandOptions = CommandOptions & {
config: Options;
core: Options;
out: Options;
key: Options;
yes: Options;
'dry-run': Options;
export const keyCommandOption: Options = {
type: 'string',
description: `A hex private key or seed phrase for transaction signing, or use the HYP_KEY env var.
Dry-run: An address to simulate transaction signing on a forked network`,
alias: 'k',
default: ENV.HYP_KEY,
};
/* Command-specific options */
export const coreTargetsCommandOption: Options = {
type: 'string',
description:
@ -79,52 +85,24 @@ export const warpConfigCommandOption: Options = {
alias: 'w',
};
export const keyCommandOption: Options = {
type: 'string',
description: `Default: A hex private key or seed phrase for transaction signing, or use the HYP_KEY env var.
Dry-run: An address to simulate transaction signing on a forked network, or use the HYP_KEY env var.`,
alias: 'k',
default: ENV.HYP_KEY,
};
export const chainsCommandOption: Options = {
type: 'string',
description: 'A path to a JSON or YAML file with chain configs',
default: './configs/chains.yaml',
alias: 'c',
};
export const outDirCommandOption: Options = {
type: 'string',
description: 'A folder name output artifacts into',
default: './artifacts',
alias: 'o',
};
export const coreArtifactsOption: Options = {
type: 'string',
description: 'File path to core deployment output artifacts',
alias: 'a',
};
export const warpConfigOption: Options = {
type: 'string',
description: 'File path to Warp config',
description: 'File path to Warp Route config',
alias: 'w',
// TODO make this optional and have the commands get it from the registry
demandOption: true,
};
export const agentConfigCommandOption: Options = {
type: 'string',
description: 'File path to agent configuration artifacts',
};
export const fileFormatOption: Options = {
export const agentConfigCommandOption = (
isIn: boolean,
defaultPath?: string,
): Options => ({
type: 'string',
description: 'Output file format',
choices: ['json', 'yaml'],
default: 'yaml',
alias: 'f',
};
description: `${
isIn ? 'Input' : 'Output'
} file path for the agent configuration`,
default: defaultPath,
});
export const outputFileOption = (defaultPath?: string): Options => ({
type: 'string',
@ -133,18 +111,18 @@ export const outputFileOption = (defaultPath?: string): Options => ({
alias: 'o',
});
export const skipConfirmationOption: Options = {
type: 'boolean',
description: 'Skip confirmation prompts',
default: false,
alias: 'y',
export const inputFileOption: Options = {
type: 'string',
description: 'Input file path',
alias: 'i',
demandOption: true,
};
export const dryRunOption: Options = {
type: 'string',
description:
'Chain name to fork and simulate deployment. Please ensure an anvil node instance is running during execution via `anvil`.',
alias: ['d', 'dr'],
alias: ['d'],
};
export const chainCommandOption: Options = {

@ -1,16 +1,12 @@
import { ethers } from 'ethers';
import { CommandModule, Options } from 'yargs';
import { CommandModuleWithWriteContext } from '../context/types.js';
import { log } from '../logger.js';
import { sendTestMessage } from '../send/message.js';
import { sendTestTransfer } from '../send/transfer.js';
import {
chainsCommandOption,
coreArtifactsOption,
keyCommandOption,
warpConfigOption,
} from './options.js';
import { warpConfigOption } from './options.js';
/**
* Parent command
@ -27,18 +23,10 @@ export const sendCommand: CommandModule = {
handler: () => log('Command required'),
};
export const selfrelay: Options = {
type: 'boolean',
description: 'Relay message on destination chain',
default: false,
alias: ['s', 'sr'],
};
/**
* Message command
*/
export const messageOptions: { [k: string]: Options } = {
key: keyCommandOption,
origin: {
type: 'string',
description: 'Origin chain to send message from',
@ -47,53 +35,61 @@ export const messageOptions: { [k: string]: Options } = {
type: 'string',
description: 'Destination chain to send message to',
},
chains: chainsCommandOption,
core: coreArtifactsOption,
timeout: {
type: 'number',
description: 'Timeout in seconds',
default: 5 * 60,
},
'self-relay': selfrelay,
quick: {
type: 'boolean',
description: 'Skip wait for message to be delivered',
default: false,
},
relay: {
type: 'boolean',
description: 'Handle self-relay of message on destination chain',
default: false,
},
};
const messageCommand: CommandModule = {
export interface MessageOptionsArgTypes {
origin?: string;
destination?: string;
timeout: number;
quick: boolean;
relay: boolean;
}
const messageCommand: CommandModuleWithWriteContext<
MessageOptionsArgTypes & { body: string }
> = {
command: 'message',
describe: 'Send a test message to a remote chain',
builder: (yargs) =>
yargs.options({
...messageOptions,
messageBody: {
type: 'string',
description: 'Optional Message body',
default: 'Hello!',
},
}),
handler: async (argv: any) => {
const key: string | undefined = argv.key;
const chainConfigPath: string = argv.chains;
const coreArtifactsPath: string | undefined = argv.core;
const origin: string | undefined = argv.origin;
const destination: string | undefined = argv.destination;
const timeoutSec: number = argv.timeout;
const skipWaitForDelivery: boolean = argv.quick;
const messageBody: string = argv.messageBody;
const selfRelay: boolean = argv['selfrelay'];
builder: {
...messageOptions,
body: {
type: 'string',
description: 'Optional Message body',
default: 'Hello!',
},
},
handler: async ({
context,
origin,
destination,
timeout,
quick,
relay,
body,
}) => {
await sendTestMessage({
key,
chainConfigPath,
coreArtifactsPath,
context,
origin,
destination,
messageBody: ethers.utils.hexlify(ethers.utils.toUtf8Bytes(messageBody)),
timeoutSec,
skipWaitForDelivery,
selfRelay,
messageBody: ethers.utils.hexlify(ethers.utils.toUtf8Bytes(body)),
timeoutSec: timeout,
skipWaitForDelivery: quick,
selfRelay: relay,
});
process.exit(0);
},
@ -102,53 +98,50 @@ const messageCommand: CommandModule = {
/**
* Transfer command
*/
const transferCommand: CommandModule = {
const transferCommand: CommandModuleWithWriteContext<
MessageOptionsArgTypes & {
warp: string;
router?: string;
wei: string;
recipient?: string;
}
> = {
command: 'transfer',
describe: 'Send a test token transfer on a warp route',
builder: (yargs) =>
yargs.options({
...messageOptions,
warp: warpConfigOption,
router: {
type: 'string',
description: 'The address of the token router contract',
},
wei: {
type: 'string',
description: 'Amount in wei to send',
default: 1,
},
recipient: {
type: 'string',
description: 'Token recipient address (defaults to sender)',
},
}),
handler: async (argv: any) => {
const key: string | undefined = argv.key;
const chainConfigPath: string = argv.chains;
const coreArtifactsPath: string | undefined = argv.core;
const warpConfigPath: string = argv.warp;
const origin: string | undefined = argv.origin;
const destination: string | undefined = argv.destination;
const timeoutSec: number = argv.timeout;
const routerAddress: string | undefined = argv.router;
const wei: string = argv.wei;
const recipient: string | undefined = argv.recipient;
const skipWaitForDelivery: boolean = argv.quick;
const selfRelay: boolean = argv['self-relay'];
builder: {
...messageOptions,
warp: warpConfigOption,
wei: {
type: 'string',
description: 'Amount in wei to send',
default: 1,
},
recipient: {
type: 'string',
description: 'Token recipient address (defaults to sender)',
},
},
handler: async ({
context,
origin,
destination,
timeout,
quick,
relay,
warp,
wei,
recipient,
}) => {
await sendTestTransfer({
key,
chainConfigPath,
coreArtifactsPath,
warpConfigPath,
context,
warpConfigPath: warp,
origin,
destination,
routerAddress,
wei,
recipient,
timeoutSec,
skipWaitForDelivery,
selfRelay,
timeoutSec: timeout,
skipWaitForDelivery: quick,
selfRelay: relay,
});
process.exit(0);
},

@ -0,0 +1,11 @@
// Commands that send tx and require a key to sign.
// It's useful to have this listed here so the context
// middleware can request keys up front when required.
export const SIGN_COMMANDS = ['deploy', 'send'];
export function isSignCommand(argv: any): boolean {
return (
SIGN_COMMANDS.includes(argv._[0]) ||
(argv._.length > 1 && SIGN_COMMANDS.includes(argv._[1]))
);
}

@ -1,37 +1,27 @@
import { CommandModule } from 'yargs';
import { CommandModuleWithContext } from '../context/types.js';
import { checkMessageStatus } from '../status/message.js';
import { messageOptions } from './send.js';
import { MessageOptionsArgTypes, messageOptions } from './send.js';
export const statusCommand: CommandModule = {
export const statusCommand: CommandModuleWithContext<
MessageOptionsArgTypes & { id?: string }
> = {
command: 'status',
describe: 'Check status of a message',
builder: (yargs) =>
yargs.options({
...messageOptions,
id: {
type: 'string',
description: 'Message ID',
},
}),
handler: async (argv: any) => {
const chainConfigPath: string = argv.chains;
const coreArtifactsPath: string | undefined = argv.core;
const messageId: string | undefined = argv.id;
const destination: string | undefined = argv.destination;
const origin: string | undefined = argv.origin;
const selfRelay: boolean = argv['self-relay'];
const key: string | undefined = argv.key;
builder: {
...messageOptions,
id: {
type: 'string',
description: 'Message ID',
},
},
handler: async ({ context, origin, destination, id, relay }) => {
await checkMessageStatus({
chainConfigPath,
coreArtifactsPath,
messageId,
context,
messageId: id,
destination,
origin,
selfRelay,
key,
selfRelay: relay,
});
process.exit(0);
},

@ -1,86 +0,0 @@
import { confirm } from '@inquirer/prompts';
import { ZodTypeAny, z } from 'zod';
import { ChainName, HyperlaneContractsMap } from '@hyperlane-xyz/sdk';
import { log, logBlue } from '../logger.js';
import { readYamlOrJson, runFileSelectionStep } from '../utils/files.js';
const RecursiveObjectSchema: ZodTypeAny = z.lazy(() =>
z.object({}).catchall(z.union([z.string(), RecursiveObjectSchema])),
);
const DeploymentArtifactsSchema = z.object({}).catchall(RecursiveObjectSchema);
export function readDeploymentArtifacts(filePath: string) {
const artifacts = readYamlOrJson<HyperlaneContractsMap<any>>(filePath);
if (!artifacts) throw new Error(`No artifacts found at ${filePath}`);
const result = DeploymentArtifactsSchema.safeParse(artifacts);
if (!result.success) {
const firstIssue = result.error.issues[0];
logBlue(
`Read deployment artifacts from ${JSON.stringify(
result.error.issues,
null,
4,
)}`,
);
throw new Error(
`Invalid artifacts: ${firstIssue.path} => ${firstIssue.message}`,
);
}
return artifacts;
}
/**
* Prompts the user to specify deployment artifacts, or to generate new ones if none are present or selected.
* @returns the selected artifacts, or undefined if they are to be generated from scratch
*/
export async function runDeploymentArtifactStep({
artifactsPath,
message,
selectedChains,
defaultArtifactsPath = './artifacts',
defaultArtifactsNamePattern = 'core-deployment',
skipConfirmation = false,
dryRun = false,
}: {
artifactsPath?: string;
message?: string;
selectedChains?: ChainName[];
defaultArtifactsPath?: string;
defaultArtifactsNamePattern?: string;
skipConfirmation?: boolean;
dryRun?: boolean;
}): Promise<HyperlaneContractsMap<any> | undefined> {
if (!artifactsPath) {
if (skipConfirmation) return undefined;
if (dryRun) defaultArtifactsNamePattern = 'dry-run_core-deployment';
const useArtifacts = await confirm({
message: message || 'Do you want use some existing contract addresses?',
});
if (!useArtifacts) return undefined;
artifactsPath = await runFileSelectionStep(
defaultArtifactsPath,
'contract deployment artifacts',
defaultArtifactsNamePattern,
);
}
const artifacts = readDeploymentArtifacts(artifactsPath);
if (selectedChains) {
const artifactChains = Object.keys(artifacts).filter((c) =>
selectedChains.includes(c),
);
if (artifactChains.length === 0) {
log('No artifacts found for selected chains');
} else {
log(`Found existing artifacts for chains: ${artifactChains.join(', ')}`);
}
}
return artifacts;
}

@ -6,13 +6,6 @@ describe('readChainConfigs', () => {
const chainToMetadata = readChainConfigs('./examples/chain-config.yaml');
it('parses and validates correctly', () => {
expect(chainToMetadata.mychainname.chainId).to.equal(1234567890);
});
it('merges core configs', () => {
expect(chainToMetadata.sepolia.chainId).to.equal(11155111);
expect(chainToMetadata.sepolia.rpcUrls[0].http).to.equal(
'https://mycustomrpc.com',
);
expect(chainToMetadata.chainId).to.equal(1234567890);
});
});

@ -1,80 +1,41 @@
import { confirm, input } from '@inquirer/prompts';
import {
ChainMap,
ChainMetadata,
ChainMetadataSchema,
chainMetadata as coreChainMetadata,
} from '@hyperlane-xyz/sdk';
import { ProtocolType, objMerge } from '@hyperlane-xyz/utils';
import { ChainMetadata, ChainMetadataSchema } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { getMultiProvider } from '../context.js';
import { CommandContext } from '../context/types.js';
import { errorRed, log, logBlue, logGreen } from '../logger.js';
import {
FileFormat,
isFile,
mergeYamlOrJson,
readYamlOrJson,
} from '../utils/files.js';
import { readYamlOrJson } from '../utils/files.js';
export function readChainConfigs(filePath: string) {
log(`Reading file configs in ${filePath}`);
const chainToMetadata = readYamlOrJson<ChainMap<ChainMetadata>>(filePath);
const chainMetadata = readYamlOrJson<ChainMetadata>(filePath);
if (
!chainToMetadata ||
typeof chainToMetadata !== 'object' ||
!Object.keys(chainToMetadata).length
!chainMetadata ||
typeof chainMetadata !== 'object' ||
!Object.keys(chainMetadata).length
) {
errorRed(`No configs found in ${filePath}`);
process.exit(1);
}
// Validate configs from file and merge in core configs as needed
for (const chain of Object.keys(chainToMetadata)) {
if (coreChainMetadata[chain]) {
// For core chains, merge in the default config to allow users to override only some fields
chainToMetadata[chain] = objMerge(
coreChainMetadata[chain],
chainToMetadata[chain],
);
}
const parseResult = ChainMetadataSchema.safeParse(chainToMetadata[chain]);
if (!parseResult.success) {
errorRed(
`Chain config for ${chain} is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/chain-config.yaml for an example`,
);
errorRed(JSON.stringify(parseResult.error.errors));
process.exit(1);
}
if (chainToMetadata[chain].name !== chain) {
errorRed(`Chain ${chain} name does not match key`);
process.exit(1);
}
}
// Ensure MultiProvider accepts this metadata
getMultiProvider(chainToMetadata);
logGreen(`All chain configs in ${filePath} are valid`);
return chainToMetadata;
}
export function readChainConfigsIfExists(filePath?: string) {
if (!filePath || !isFile(filePath)) {
log('No chain config file provided');
return {};
} else {
return readChainConfigs(filePath);
const parseResult = ChainMetadataSchema.safeParse(chainMetadata);
if (!parseResult.success) {
errorRed(
`Chain config for ${filePath} is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/chain-config.yaml for an example`,
);
errorRed(JSON.stringify(parseResult.error.errors));
process.exit(1);
}
return chainMetadata;
}
export async function createChainConfig({
format,
outPath,
context,
}: {
format: FileFormat;
outPath: string;
context: CommandContext;
}) {
logBlue('Creating a new chain config');
const name = await input({
@ -149,8 +110,8 @@ export async function createChainConfig({
}
const parseResult = ChainMetadataSchema.safeParse(metadata);
if (parseResult.success) {
logGreen(`Chain config is valid, writing to file ${outPath}`);
mergeYamlOrJson(outPath, { [name]: metadata }, format);
logGreen(`Chain config is valid, writing to registry`);
await context.registry.updateChain({ chainName: metadata.name, metadata });
} else {
errorRed(
`Chain config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/chain-config.yaml for an example`,

@ -9,7 +9,6 @@ import {
GasOracleContractType,
HookType,
HooksConfig,
chainMetadata,
} from '@hyperlane-xyz/sdk';
import {
Address,
@ -18,11 +17,10 @@ import {
toWei,
} from '@hyperlane-xyz/utils';
import { CommandContext } from '../context/types.js';
import { errorRed, log, logBlue, logGreen, logRed } from '../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
import { FileFormat, mergeYamlOrJson, readYamlOrJson } from '../utils/files.js';
import { readChainConfigsIfExists } from './chain.js';
import { mergeYamlOrJson, readYamlOrJson } from '../utils/files.js';
const ProtocolFeeSchema = z.object({
type: z.literal(HookType.PROTOCOL_FEE),
@ -118,17 +116,14 @@ export function readHooksConfigMap(filePath: string) {
}
export async function createHooksConfigMap({
format,
context,
outPath,
chainConfigPath,
}: {
format: FileFormat;
context: CommandContext;
outPath: string;
chainConfigPath: string;
}) {
logBlue('Creating a new hook config');
const customChains = readChainConfigsIfExists(chainConfigPath);
const chains = await runMultiChainSelectionStep(customChains);
const chains = await runMultiChainSelectionStep(context.chainMetadata);
const result: HooksConfigMap = {};
for (const chain of chains) {
@ -137,12 +132,12 @@ export async function createHooksConfigMap({
const remotes = chains.filter((c) => c !== chain);
result[chain] = {
...result[chain],
[hookRequirements]: await createHookConfig(chain, remotes),
[hookRequirements]: await createHookConfig(context, chain, remotes),
};
}
if (isValidHookConfigMap(result)) {
logGreen(`Hook config is valid, writing to file ${outPath}`);
mergeYamlOrJson(outPath, result, format);
mergeYamlOrJson(outPath, result);
} else {
errorRed(
`Hook config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/hooks.yaml for an example`,
@ -153,6 +148,7 @@ export async function createHooksConfigMap({
}
export async function createHookConfig(
context: CommandContext,
chain: ChainName,
remotes: ChainName[],
): Promise<HookConfig> {
@ -195,13 +191,13 @@ export async function createHookConfig(
if (hookType === HookType.MERKLE_TREE) {
lastConfig = { type: HookType.MERKLE_TREE };
} else if (hookType === HookType.PROTOCOL_FEE) {
lastConfig = await createProtocolFeeConfig(chain);
lastConfig = await createProtocolFeeConfig(context, chain);
} else if (hookType === HookType.INTERCHAIN_GAS_PAYMASTER) {
lastConfig = await createIGPConfig(remotes);
} else if (hookType === HookType.AGGREGATION) {
lastConfig = await createAggregationConfig(chain, remotes);
lastConfig = await createAggregationConfig(context, chain, remotes);
} else if (hookType === HookType.ROUTING) {
lastConfig = await createRoutingConfig(chain, remotes);
lastConfig = await createRoutingConfig(context, chain, remotes);
} else {
throw new Error(`Invalid hook type: ${hookType}`);
}
@ -209,6 +205,7 @@ export async function createHookConfig(
}
export async function createProtocolFeeConfig(
context: CommandContext,
chain: ChainName,
): Promise<HookConfig> {
const owner = await input({
@ -232,6 +229,7 @@ export async function createProtocolFeeConfig(
const maxProtocolFee = toWei(
await input({
message: `Enter max protocol fee ${nativeTokenAndDecimals(
context,
chain,
)} e.g. 1.0)`,
}),
@ -239,6 +237,7 @@ export async function createProtocolFeeConfig(
const protocolFee = toWei(
await input({
message: `Enter protocol fee in ${nativeTokenAndDecimals(
context,
chain,
)} e.g. 0.01)`,
}),
@ -305,6 +304,7 @@ export async function createIGPConfig(
}
export async function createAggregationConfig(
context: CommandContext,
chain: ChainName,
remotes: ChainName[],
): Promise<HookConfig> {
@ -317,7 +317,7 @@ export async function createAggregationConfig(
const hooks: Array<HookConfig> = [];
for (let i = 0; i < hooksNum; i++) {
logBlue(`Creating hook ${i + 1} of ${hooksNum} ...`);
hooks.push(await createHookConfig(chain, remotes));
hooks.push(await createHookConfig(context, chain, remotes));
}
return {
type: HookType.AGGREGATION,
@ -326,6 +326,7 @@ export async function createAggregationConfig(
}
export async function createRoutingConfig(
context: CommandContext,
origin: ChainName,
remotes: ChainName[],
): Promise<HookConfig> {
@ -339,7 +340,7 @@ export async function createRoutingConfig(
await confirm({
message: `You are about to configure hook for remote chain ${chain}. Continue?`,
});
const config = await createHookConfig(origin, remotes);
const config = await createHookConfig(context, origin, remotes);
domainsMap[chain] = config;
}
return {
@ -349,10 +350,10 @@ export async function createRoutingConfig(
};
}
function nativeTokenAndDecimals(chain: ChainName) {
function nativeTokenAndDecimals(context: CommandContext, chain: ChainName) {
return `10^${
chainMetadata[chain].nativeToken?.decimals ?? '18'
context.chainMetadata[chain].nativeToken?.decimals ?? '18'
} which you cannot exceed (in ${
chainMetadata[chain].nativeToken?.symbol ?? 'eth'
context.chainMetadata[chain].nativeToken?.symbol ?? 'eth'
}`;
}

@ -3,6 +3,7 @@ import { z } from 'zod';
import { ChainMap, ChainName, IsmType, ZHash } from '@hyperlane-xyz/sdk';
import { CommandContext } from '../context/types.js';
import {
errorRed,
log,
@ -12,9 +13,7 @@ import {
logRed,
} from '../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
import { FileFormat, mergeYamlOrJson, readYamlOrJson } from '../utils/files.js';
import { readChainConfigsIfExists } from './chain.js';
import { mergeYamlOrJson, readYamlOrJson } from '../utils/files.js';
const MultisigIsmConfigSchema = z.object({
type: z.union([
@ -113,22 +112,19 @@ export function isValildIsmConfig(config: any) {
}
export async function createIsmConfigMap({
format,
context,
outPath,
chainConfigPath,
}: {
format: FileFormat;
context: CommandContext;
outPath: string;
chainConfigPath: string;
}) {
logBlue('Creating a new advanced ISM config');
logBoldUnderlinedRed('WARNING: USE AT YOUR RISK.');
logRed(
'Advanced ISM configs require knowledge of different ISM types and how they work together topologically. If possible, use the basic ISM configs are recommended.',
);
const customChains = readChainConfigsIfExists(chainConfigPath);
const chains = await runMultiChainSelectionStep(
customChains,
context.chainMetadata,
'Select chains to configure ISM for',
true,
);
@ -146,7 +142,7 @@ export async function createIsmConfigMap({
if (isValildIsmConfig(result)) {
logGreen(`ISM config is valid, writing to file ${outPath}`);
mergeYamlOrJson(outPath, result, format);
mergeYamlOrJson(outPath, result);
} else {
errorRed(
`ISM config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/ism.yaml for an example`,

@ -9,12 +9,10 @@ import {
objMap,
} from '@hyperlane-xyz/utils';
import { sdkContractAddressesMap } from '../context.js';
import { CommandContext } from '../context/types.js';
import { errorRed, log, logBlue, logGreen } from '../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
import { FileFormat, mergeYamlOrJson, readYamlOrJson } from '../utils/files.js';
import { readChainConfigsIfExists } from './chain.js';
import { mergeYamlOrJson, readYamlOrJson } from '../utils/files.js';
const MultisigConfigMapSchema = z.object({}).catchall(
z.object({
@ -64,21 +62,19 @@ export function isValidMultisigConfig(config: any) {
}
export async function createMultisigConfig({
format,
context,
outPath,
chainConfigPath,
}: {
format: FileFormat;
context: CommandContext;
outPath: string;
chainConfigPath: string;
}) {
logBlue('Creating a new multisig config');
log(
'Select your own chain below to run your own validators. If you want to reuse existing Hyperlane validators instead of running your own, do not select additional mainnet or testnet chains.',
);
const customChains = readChainConfigsIfExists(chainConfigPath);
const chains = await runMultiChainSelectionStep(customChains);
const chains = await runMultiChainSelectionStep(context.chainMetadata);
const chainAddresses = await context.registry.getAddresses();
const result: MultisigConfigMap = {};
let lastConfig: MultisigConfigMap['string'] | undefined = undefined;
const repeat = false;
@ -88,7 +84,7 @@ export async function createMultisigConfig({
result[chain] = lastConfig;
continue;
}
if (Object.keys(sdkContractAddressesMap).includes(chain)) {
if (Object.keys(chainAddresses).includes(chain)) {
const reuseCoreConfig = await confirm({
message: 'Use existing Hyperlane validators for this chain?',
});
@ -118,7 +114,7 @@ export async function createMultisigConfig({
if (isValidMultisigConfig(result)) {
logGreen(`Multisig config is valid, writing to file ${outPath}`);
mergeYamlOrJson(outPath, result, format);
mergeYamlOrJson(outPath, result);
} else {
errorRed(
`Multisig config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/ism.yaml for an example`,

@ -3,31 +3,27 @@ import { ethers } from 'ethers';
import {
TokenType,
WarpCoreConfig,
WarpCoreConfigSchema,
WarpRouteDeployConfig,
WarpRouteDeployConfigSchema,
} from '@hyperlane-xyz/sdk';
import { CommandContext } from '../context/types.js';
import { errorRed, logBlue, logGreen } from '../logger.js';
import {
runMultiChainSelectionStep,
runSingleChainSelectionStep,
} from '../utils/chains.js';
import { FileFormat, readYamlOrJson, writeYamlOrJson } from '../utils/files.js';
import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js';
import { readChainConfigsIfExists } from './chain.js';
export function readWarpRouteDeployConfig(filePath: string) {
export function readWarpRouteDeployConfig(
filePath: string,
): WarpRouteDeployConfig {
const config = readYamlOrJson(filePath);
if (!config)
throw new Error(`No warp route deploy config found at ${filePath}`);
const result = WarpRouteDeployConfigSchema.safeParse(config);
if (!result.success) {
const firstIssue = result.error.issues[0];
throw new Error(
`Invalid warp config: ${firstIssue.path} => ${firstIssue.message}`,
);
}
return result.data;
return WarpRouteDeployConfigSchema.parse(config);
}
export function isValidWarpRouteDeployConfig(config: any) {
@ -35,18 +31,15 @@ export function isValidWarpRouteDeployConfig(config: any) {
}
export async function createWarpRouteDeployConfig({
format,
context,
outPath,
chainConfigPath,
}: {
format: FileFormat;
context: CommandContext;
outPath: string;
chainConfigPath: string;
}) {
logBlue('Creating a new warp route deployment config');
const customChains = readChainConfigsIfExists(chainConfigPath);
const baseChain = await runSingleChainSelectionStep(
customChains,
context.chainMetadata,
'Select base chain with the original token to warp',
);
@ -74,7 +67,7 @@ export async function createWarpRouteDeployConfig({
: await input({ message: addressMessage });
const syntheticChains = await runMultiChainSelectionStep(
customChains,
context.chainMetadata,
'Select chains to which the base token will be connected',
);
@ -104,7 +97,7 @@ export async function createWarpRouteDeployConfig({
if (isValidWarpRouteDeployConfig(result)) {
logGreen(`Warp Route config is valid, writing to file ${outPath}`);
writeYamlOrJson(outPath, result, format);
writeYamlOrJson(outPath, result);
} else {
errorRed(
`Warp route deployment config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/warp-route-deployment.yaml for an example`,
@ -112,3 +105,11 @@ export async function createWarpRouteDeployConfig({
throw new Error('Invalid multisig config');
}
}
// Note, this is different than the function above which reads a config
// for a DEPLOYMENT. This gets a config for using a warp route (aka WarpCoreConfig)
export function readWarpRouteConfig(filePath: string): WarpCoreConfig {
const config = readYamlOrJson(filePath);
if (!config) throw new Error(`No warp route config found at ${filePath}`);
return WarpCoreConfigSchema.parse(config);
}

@ -1,23 +0,0 @@
import { expect } from 'chai';
import { ethers } from 'ethers';
import { getContext } from './context.js';
describe('context', () => {
it('Gets minimal read-only context correctly', async () => {
const context = await getContext({ chainConfigPath: './fakePath' });
expect(!!context.multiProvider).to.be.true;
expect(context.customChains).to.eql({});
});
it('Handles conditional type correctly', async () => {
const randomWallet = ethers.Wallet.createRandom();
const context = await getContext({
chainConfigPath: './fakePath',
keyConfig: { key: randomWallet.privateKey },
});
expect(!!context.multiProvider).to.be.true;
expect(context.customChains).to.eql({});
expect(await context.signer.getAddress()).to.eql(randomWallet.address);
});
});

@ -1,213 +0,0 @@
import { input } from '@inquirer/prompts';
import { ethers } from 'ethers';
import {
ChainMap,
ChainMetadata,
ChainName,
HyperlaneContractsMap,
MultiProvider,
WarpCoreConfig,
chainMetadata,
hyperlaneEnvironments,
} from '@hyperlane-xyz/sdk';
import { objFilter, objMap, objMerge } from '@hyperlane-xyz/utils';
import { runDeploymentArtifactStep } from './config/artifacts.js';
import { readChainConfigsIfExists } from './config/chain.js';
import { forkNetworkToMultiProvider } from './deploy/dry-run.js';
import { runSingleChainSelectionStep } from './utils/chains.js';
import { readYamlOrJson } from './utils/files.js';
import { getImpersonatedSigner, getSigner } from './utils/keys.js';
export const sdkContractAddressesMap: HyperlaneContractsMap<any> = {
...hyperlaneEnvironments.testnet,
...hyperlaneEnvironments.mainnet,
};
export function getMergedContractAddresses(
artifacts?: HyperlaneContractsMap<any>,
chains?: ChainName[],
) {
// if chains include non sdkContractAddressesMap chains, don't recover interchainGasPaymaster
let sdkContractsAddressesToRecover = sdkContractAddressesMap;
if (
chains?.some(
(chain) => !Object.keys(sdkContractAddressesMap).includes(chain),
)
) {
sdkContractsAddressesToRecover = objMap(sdkContractAddressesMap, (_, v) =>
objFilter(
v as ChainMap<any>,
(key, v): v is any => key !== 'interchainGasPaymaster',
),
);
}
return objMerge(
sdkContractsAddressesToRecover,
artifacts || {},
) as HyperlaneContractsMap<any>;
}
export type KeyConfig = {
key?: string;
promptMessage?: string;
};
export interface ContextSettings {
chainConfigPath?: string;
chains?: ChainName[];
coreConfig?: {
coreArtifactsPath?: string;
promptMessage?: string;
};
keyConfig?: KeyConfig;
skipConfirmation?: boolean;
warpConfig?: {
warpConfigPath?: string;
promptMessage?: string;
};
}
interface CommandContextBase {
chains: ChainName[];
customChains: ChainMap<ChainMetadata>;
multiProvider: MultiProvider;
}
// This makes return type dynamic based on the input settings
type CommandContext<P extends ContextSettings> = CommandContextBase &
(P extends { keyConfig: object }
? { signer: ethers.Signer }
: { signer: undefined }) &
(P extends { coreConfig: object }
? { coreArtifacts: HyperlaneContractsMap<any> }
: { coreArtifacts: undefined }) &
(P extends { warpConfig: object }
? { warpCoreConfig: WarpCoreConfig }
: { warpCoreConfig: undefined });
/**
* Retrieves context for the user-selected command
* @returns context for the current command
*/
export async function getContext<P extends ContextSettings>({
chainConfigPath,
coreConfig,
keyConfig,
skipConfirmation,
warpConfig,
}: P): Promise<CommandContext<P>> {
const customChains = readChainConfigsIfExists(chainConfigPath);
const signer = await getSigner({
keyConfig,
skipConfirmation,
});
let coreArtifacts = undefined;
if (coreConfig) {
coreArtifacts =
(await runDeploymentArtifactStep({
artifactsPath: coreConfig.coreArtifactsPath,
message:
coreConfig.promptMessage ||
'Do you want to use some core deployment address artifacts? This is required for PI chains (non-core chains).',
skipConfirmation,
})) || {};
}
let warpCoreConfig = undefined;
if (warpConfig) {
let warpConfigPath = warpConfig.warpConfigPath;
if (!warpConfigPath) {
// prompt for path to token config
warpConfigPath = await input({
message:
warpConfig.promptMessage ||
'Please provide a path to the Warp config',
});
}
warpCoreConfig = readYamlOrJson<WarpCoreConfig>(warpConfigPath);
}
const multiProvider = getMultiProvider(customChains, signer);
return {
customChains,
signer,
multiProvider,
coreArtifacts,
warpCoreConfig,
} as CommandContext<P>;
}
/**
* Retrieves dry-run context for the user-selected command
* @returns dry-run context for the current command
*/
export async function getDryRunContext<P extends ContextSettings>({
chainConfigPath,
chains,
coreConfig,
keyConfig,
skipConfirmation,
}: P): Promise<CommandContext<P>> {
const customChains = readChainConfigsIfExists(chainConfigPath);
let coreArtifacts = undefined;
if (coreConfig) {
coreArtifacts =
(await runDeploymentArtifactStep({
artifactsPath: coreConfig.coreArtifactsPath,
message:
coreConfig.promptMessage ||
'Do you want to use some core deployment address artifacts? This is required for PI chains (non-core chains).',
skipConfirmation,
})) || {};
}
const multiProvider = getMultiProvider(customChains);
if (!chains?.length) {
if (skipConfirmation) throw new Error('No chains provided');
chains = [
await runSingleChainSelectionStep(
customChains,
'Select chain to dry-run against:',
),
];
}
await forkNetworkToMultiProvider(multiProvider, chains[0]);
const impersonatedSigner = await getImpersonatedSigner({
keyConfig,
skipConfirmation,
});
if (impersonatedSigner) multiProvider.setSharedSigner(impersonatedSigner);
return {
chains,
signer: impersonatedSigner,
multiProvider,
coreArtifacts,
} as CommandContext<P>;
}
/**
* Retrieves a new MultiProvider based on all known chain metadata & custom user chains
* @param customChains Custom chains specified by the user
* @returns a new MultiProvider
*/
export function getMultiProvider(
customChains: ChainMap<ChainMetadata>,
signer?: ethers.Signer,
) {
const chainConfigs = { ...chainMetadata, ...customChains };
const multiProvider = new MultiProvider(chainConfigs);
if (signer) multiProvider.setSharedSigner(signer);
return multiProvider;
}

@ -0,0 +1,134 @@
import { ethers } from 'ethers';
import { IRegistry } from '@hyperlane-xyz/registry';
import { ChainName, MultiProvider } from '@hyperlane-xyz/sdk';
import { isSignCommand } from '../commands/signCommands.js';
import { forkNetworkToMultiProvider } from '../deploy/dry-run.js';
import { MergedRegistry } from '../registry/MergedRegistry.js';
import { runSingleChainSelectionStep } from '../utils/chains.js';
import { getImpersonatedSigner, getSigner } from '../utils/keys.js';
import {
CommandContext,
ContextSettings,
WriteCommandContext,
} from './types.js';
export async function contextMiddleware(argv: Record<string, any>) {
const isDryRun = !!argv.dryRun;
const requiresKey = isSignCommand(argv);
const settings: ContextSettings = {
registryUri: argv.registry,
registryOverrideUri: argv.overrides,
key: argv.key,
requiresKey,
skipConfirmation: argv.yes,
};
const context = isDryRun
? await getDryRunContext(settings, argv.dryRun)
: await getContext(settings);
argv.context = context;
}
/**
* Retrieves context for the user-selected command
* @returns context for the current command
*/
export async function getContext({
registryUri,
registryOverrideUri,
key,
requiresKey,
skipConfirmation,
}: ContextSettings): Promise<CommandContext> {
const registry = getRegistry(registryUri, registryOverrideUri);
let signer: ethers.Wallet | undefined = undefined;
if (requiresKey) {
({ key, signer } = await getSigner({ key, skipConfirmation }));
}
const multiProvider = await getMultiProvider(registry, signer);
return {
registry,
chainMetadata: multiProvider.metadata,
multiProvider,
key,
signer,
skipConfirmation: !!skipConfirmation,
} as CommandContext;
}
/**
* Retrieves dry-run context for the user-selected command
* @returns dry-run context for the current command
*/
export async function getDryRunContext(
{ registryUri, registryOverrideUri, key, skipConfirmation }: ContextSettings,
chain?: ChainName,
): Promise<CommandContext> {
const registry = getRegistry(registryUri, registryOverrideUri, true);
const chainMetadata = await registry.getMetadata();
if (!chain) {
if (skipConfirmation) throw new Error('No chains provided');
chain = await runSingleChainSelectionStep(
chainMetadata,
'Select chain to dry-run against:',
);
}
const multiProvider = await getMultiProvider(registry);
await forkNetworkToMultiProvider(multiProvider, chain);
const { key: impersonatedKey, signer: impersonatedSigner } =
await getImpersonatedSigner({
key,
skipConfirmation,
});
multiProvider.setSharedSigner(impersonatedSigner);
return {
registry,
chainMetadata: multiProvider.metadata,
key: impersonatedKey,
signer: impersonatedSigner,
multiProvider: multiProvider,
skipConfirmation: !!skipConfirmation,
isDryRun: true,
dryRunChain: chain,
} as WriteCommandContext;
}
/**
* Creates a new MergedRegistry using the provided URIs
* The intention of the MergedRegistry is to join the common data
* from a primary URI (such as the Hyperlane default Github repo)
* and an override one (such as a local directory)
* @returns a new MergedRegistry
*/
function getRegistry(
primaryRegistryUri: string,
overrideRegistryUri: string,
isDryRun?: boolean,
): IRegistry {
const registryUris = [primaryRegistryUri, overrideRegistryUri]
.map((r) => r.trim())
.filter((r) => !!r);
return new MergedRegistry({
registryUris,
isDryRun,
});
}
/**
* Retrieves a new MultiProvider based on all known chain metadata & custom user chains
* @param customChains Custom chains specified by the user
* @returns a new MultiProvider
*/
async function getMultiProvider(registry: IRegistry, signer?: ethers.Signer) {
const chainMetadata = await registry.getMetadata();
const multiProvider = new MultiProvider(chainMetadata);
if (signer) multiProvider.setSharedSigner(signer);
return multiProvider;
}

@ -0,0 +1,43 @@
import type { ethers } from 'ethers';
import type { CommandModule } from 'yargs';
import type { IRegistry } from '@hyperlane-xyz/registry';
import type {
ChainMap,
ChainMetadata,
MultiProvider,
} from '@hyperlane-xyz/sdk';
export interface ContextSettings {
registryUri: string;
registryOverrideUri: string;
key?: string;
requiresKey?: boolean;
skipConfirmation?: boolean;
}
export interface CommandContext {
registry: IRegistry;
chainMetadata: ChainMap<ChainMetadata>;
multiProvider: MultiProvider;
skipConfirmation: boolean;
key?: string;
signer?: ethers.Signer;
}
export interface WriteCommandContext extends CommandContext {
key: string;
signer: ethers.Signer;
isDryRun?: boolean;
dryRunChain?: string;
}
export type CommandModuleWithContext<Args> = CommandModule<
{},
Args & { context: CommandContext }
>;
export type CommandModuleWithWriteContext<Args> = CommandModule<
{},
Args & { context: WriteCommandContext }
>;

@ -2,7 +2,7 @@ import terminalLink from 'terminal-link';
import { toBase64 } from '@hyperlane-xyz/utils';
import { getContext } from '../context.js';
import { CommandContext } from '../context/types.js';
import { logBlue, logGreen } from '../logger.js';
import {
runMultiChainSelectionStep,
@ -11,27 +11,25 @@ import {
import { readJson, runFileSelectionStep } from '../utils/files.js';
export async function runKurtosisAgentDeploy({
context,
originChain,
relayChains,
chainConfigPath,
agentConfigurationPath,
}: {
originChain: string;
relayChains: string;
chainConfigPath: string;
agentConfigurationPath: string;
context: CommandContext;
originChain?: string;
relayChains?: string;
agentConfigurationPath?: string;
}) {
const { customChains } = await getContext({ chainConfigPath });
if (!originChain) {
originChain = await runSingleChainSelectionStep(
customChains,
context.chainMetadata,
'Select the origin chain',
);
}
if (!relayChains) {
const selectedRelayChains = await runMultiChainSelectionStep(
customChains,
context.chainMetadata,
'Select chains to relay between',
true,
);
@ -44,7 +42,7 @@ export async function runKurtosisAgentDeploy({
'No agent config json was provided. Please specify the agent config json filepath.',
);
agentConfigurationPath = await runFileSelectionStep(
'./artifacts',
'./configs',
'agent config json',
'agent-config',
);

@ -1,24 +1,22 @@
import { confirm } from '@inquirer/prompts';
import { ethers } from 'ethers';
import { ChainAddresses, IRegistry } from '@hyperlane-xyz/registry';
import {
ChainMap,
ChainName,
CoreConfig,
DeployedIsm,
GasOracleContractType,
HooksConfig,
HyperlaneAddressesMap,
HyperlaneContractsMap,
HyperlaneCore,
HyperlaneCoreDeployer,
HyperlaneDeploymentArtifacts,
HyperlaneIsmFactory,
HyperlaneProxyFactoryDeployer,
IgpConfig,
IsmConfig,
IsmType,
MultiProvider,
MultisigConfig,
RoutingIsmConfig,
buildAgentConfig,
@ -27,20 +25,13 @@ import {
multisigIsmVerificationCost,
serializeContractsMap,
} from '@hyperlane-xyz/sdk';
import { Address, objFilter, objMerge } from '@hyperlane-xyz/utils';
import { Address, objFilter, objMap, objMerge } from '@hyperlane-xyz/utils';
import { Command } from '../commands/deploy.js';
import { runDeploymentArtifactStep } from '../config/artifacts.js';
import { presetHookConfigs, readHooksConfigMap } from '../config/hooks.js';
import { readIsmConfig } from '../config/ism.js';
import { readMultisigConfig } from '../config/multisig.js';
import { MINIMUM_CORE_DEPLOY_GAS } from '../consts.js';
import {
getContext,
getDryRunContext,
getMergedContractAddresses,
sdkContractAddressesMap,
} from '../context.js';
import { WriteCommandContext } from '../context/types.js';
import {
log,
logBlue,
@ -50,12 +41,7 @@ import {
logRed,
} from '../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
import {
getArtifactsFiles,
prepNewArtifactsFiles,
runFileSelectionStep,
writeJson,
} from '../utils/files.js';
import { runFileSelectionStep, writeJson } from '../utils/files.js';
import {
completeDeploy,
@ -65,62 +51,36 @@ import {
runPreflightChecksForChains,
} from './utils.js';
const CONTRACT_CACHE_EXCLUSIONS = ['interchainGasPaymaster'];
/**
* Executes the core deploy command.
*/
export async function runCoreDeploy({
key,
chainConfigPath,
context,
chains,
ismConfigPath,
hookConfigPath,
artifactsPath,
outPath,
skipConfirmation,
dryRun,
agentOutPath,
}: {
key?: string;
chainConfigPath: string;
context: WriteCommandContext;
chains?: ChainName[];
ismConfigPath?: string;
hookConfigPath?: string;
artifactsPath?: string;
outPath: string;
skipConfirmation: boolean;
dryRun: string;
agentOutPath: string;
}) {
const context = dryRun
? await getDryRunContext({
chainConfigPath,
chains: [dryRun],
keyConfig: { key },
skipConfirmation,
})
: await getContext({
chainConfigPath,
keyConfig: { key },
skipConfirmation,
});
const customChains = context.customChains;
const multiProvider = context.multiProvider;
const signer = context.signer;
if (dryRun) chains = context.chains;
const { chainMetadata, signer, dryRunChain, skipConfirmation } = context;
if (dryRunChain) chains = [dryRunChain];
else if (!chains?.length) {
if (skipConfirmation) throw new Error('No chains provided');
chains = await runMultiChainSelectionStep(
customChains,
chainMetadata,
'Select chains to connect:',
true,
);
}
const artifacts = await runArtifactStep(
chains,
skipConfirmation,
artifactsPath,
);
const result = await runIsmStep(chains, skipConfirmation, ismConfigPath);
// we can either specify the full ISM config or just the multisig config
const isIsmConfig = isISMConfig(result);
@ -131,16 +91,12 @@ export async function runCoreDeploy({
const hooksConfig = await runHookStep(chains, hookConfigPath);
const deploymentParams: DeployParams = {
context,
chains,
signer,
multiProvider,
artifacts,
ismConfigs,
multisigConfigs,
hooksConfig,
outPath,
skipConfirmation,
dryRun,
agentOutPath,
};
await runDeployPlanStep(deploymentParams);
@ -149,41 +105,13 @@ export async function runCoreDeploy({
minGas: MINIMUM_CORE_DEPLOY_GAS,
});
const userAddress = dryRun ? key! : await signer.getAddress();
const userAddress = await signer.getAddress();
const initialBalances = await prepareDeploy(
multiProvider,
userAddress,
chains,
);
const initialBalances = await prepareDeploy(context, userAddress, chains);
await executeDeploy(deploymentParams);
await completeDeploy(
Command.CORE,
initialBalances,
multiProvider,
userAddress,
chains,
dryRun,
);
}
function runArtifactStep(
selectedChains: ChainName[],
skipConfirmation: boolean,
artifactsPath?: string,
dryRun?: boolean,
) {
logBlue(
'\nDeployments can be totally new or can use some existing contract addresses.',
);
return runDeploymentArtifactStep({
artifactsPath,
selectedChains,
skipConfirmation,
dryRun,
});
await completeDeploy(context, 'core', initialBalances, userAddress, chains);
}
async function runIsmStep(
@ -271,24 +199,16 @@ async function runHookStep(
}
interface DeployParams {
context: WriteCommandContext;
chains: ChainName[];
signer: ethers.Signer;
multiProvider: MultiProvider;
artifacts?: HyperlaneAddressesMap<any>;
ismConfigs?: ChainMap<IsmConfig>;
multisigConfigs?: ChainMap<MultisigConfig>;
hooksConfig?: ChainMap<HooksConfig>;
outPath: string;
skipConfirmation: boolean;
dryRun: string;
agentOutPath: string;
}
async function runDeployPlanStep({
chains,
signer,
artifacts,
skipConfirmation,
}: DeployParams) {
async function runDeployPlanStep({ context, chains }: DeployParams) {
const { signer, skipConfirmation } = context;
const address = await signer.getAddress();
logBlue('\nDeployment plan');
@ -296,9 +216,7 @@ async function runDeployPlanStep({
log(`Transaction signer and owner of new contracts will be ${address}`);
log(`Deploying to ${chains.join(', ')}`);
log(
`There are several contracts required for each chain but contracts in the Hyperlane SDK ${
artifacts ? 'or your artifacts ' : ''
}will be skipped`,
`There are several contracts required for each chain but contracts in your provided registries will be skipped`,
);
if (skipConfirmation) return;
@ -309,36 +227,26 @@ async function runDeployPlanStep({
}
async function executeDeploy({
context,
chains,
signer,
multiProvider,
outPath,
artifacts = {},
ismConfigs = {},
multisigConfigs = {},
hooksConfig = {},
dryRun,
agentOutPath,
}: DeployParams) {
logBlue('All systems ready, captain! Beginning deployment...');
const { signer, multiProvider, registry } = context;
const [contractsFilePath, agentFilePath] = prepNewArtifactsFiles(
outPath,
getArtifactsFiles(
[
{ filename: 'core-deployment', description: 'Contract addresses' },
{ filename: 'agent-config', description: 'Agent configs' },
],
dryRun,
),
);
let chainAddresses = await registry.getAddresses();
chainAddresses = filterAddressesToCache(chainAddresses);
const owner = await signer.getAddress();
const mergedContractAddrs = getMergedContractAddresses(artifacts, chains);
let artifacts: HyperlaneAddressesMap<any> = {};
// 1. Deploy ISM factories to all deployable chains that don't have them.
logBlue('Deploying ISM factory contracts');
const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
ismFactoryDeployer.cacheAddressesMap(mergedContractAddrs);
ismFactoryDeployer.cacheAddressesMap(chainAddresses);
const ismFactoryConfig = chains.reduce((chainMap, curr) => {
chainMap[curr] = {};
@ -346,33 +254,32 @@ async function executeDeploy({
}, {} as ChainMap<{}>);
const ismFactoryContracts = await ismFactoryDeployer.deploy(ismFactoryConfig);
artifacts = writeMergedAddresses(
contractsFilePath,
artifacts,
artifacts = await updateChainAddresses(
registry,
ismFactoryContracts,
artifacts,
);
logGreen('ISM factory contracts deployed');
// Build an IsmFactory that covers all chains so that we can
// use it to deploy ISMs to remote chains.
const ismFactory = HyperlaneIsmFactory.fromAddressesMap(
mergedContractAddrs,
chainAddresses,
multiProvider,
);
// 3. Construct ISM configs for all deployable chains
const ismContracts: ChainMap<{ interchainSecurityModule: DeployedIsm }> = {};
const defaultIsms: ChainMap<IsmConfig> = {};
for (const ismOrigin of chains) {
defaultIsms[ismOrigin] =
ismConfigs[ismOrigin] ??
buildIsmConfig(owner, ismOrigin, chains, multisigConfigs);
}
artifacts = writeMergedAddresses(contractsFilePath, artifacts, ismContracts);
// 4. Deploy core contracts to chains
logBlue(`Deploying core contracts to ${chains.join(', ')}`);
const coreDeployer = new HyperlaneCoreDeployer(multiProvider, ismFactory);
coreDeployer.cacheAddressesMap(mergedContractAddrs as any);
coreDeployer.cacheAddressesMap(chainAddresses as any);
const coreConfigs = buildCoreConfigMap(
owner,
chains,
@ -390,16 +297,27 @@ async function executeDeploy({
};
}
artifacts = objMerge(artifacts, isms);
artifacts = writeMergedAddresses(contractsFilePath, artifacts, coreContracts);
artifacts = await updateChainAddresses(registry, coreContracts, artifacts);
logGreen('✅ Core contracts deployed');
log(JSON.stringify(artifacts, null, 2));
log('Writing agent configs');
await writeAgentConfig(agentFilePath, artifacts, chains, multiProvider);
logGreen('Agent configs written');
await writeAgentConfig(context, artifacts, chains, agentOutPath);
logBlue('Deployment is complete!');
logBlue(`Contract address artifacts are in ${contractsFilePath}`);
logBlue(`Agent configs are in ${agentFilePath}`);
}
function filterAddressesToCache(addressesMap: ChainMap<ChainAddresses>) {
// Filter out the certain addresses that must always be
// deployed when deploying to a PI chain.
// See https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/2983
// And https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/3183
return objMap(addressesMap, (_chain, addresses) =>
objFilter(
addresses,
(contract, _address): _address is string =>
!CONTRACT_CACHE_EXCLUSIONS.includes(contract),
),
);
}
function buildIsmConfig(
@ -473,23 +391,40 @@ export function buildIgpConfigMap(
return configMap;
}
function writeMergedAddresses(
filePath: string,
aAddresses: HyperlaneAddressesMap<any>,
bContracts: HyperlaneContractsMap<any>,
): HyperlaneAddressesMap<any> {
const bAddresses = serializeContractsMap(bContracts);
const mergedAddresses = objMerge(aAddresses, bAddresses);
writeJson(filePath, mergedAddresses);
async function updateChainAddresses(
registry: IRegistry,
newContracts: HyperlaneContractsMap<any>,
otherAddresses: HyperlaneAddressesMap<any>,
) {
let newAddresses = serializeContractsMap(newContracts);
// The HyperlaneCoreDeployer is returning a nested object with ISM addresses
// from other chains, which don't need to be in the artifacts atm.
newAddresses = objMap(newAddresses, (_, newChainAddresses) => {
// For each chain in the addresses chainmap, filter the values to those that are just strings
return objFilter(
newChainAddresses,
(_, value): value is string => typeof value === 'string',
);
});
const mergedAddresses = objMerge(otherAddresses, newAddresses);
for (const chainName of Object.keys(newContracts)) {
await registry.updateChain({
chainName,
addresses: mergedAddresses[chainName],
});
}
return mergedAddresses;
}
async function writeAgentConfig(
filePath: string,
context: WriteCommandContext,
artifacts: HyperlaneAddressesMap<any>,
chains: ChainName[],
multiProvider: MultiProvider,
outPath: string,
) {
if (context.isDryRun) return;
log('Writing agent configs');
const { multiProvider, registry } = context;
const startBlocks: ChainMap<number> = {};
const core = HyperlaneCore.fromAddressesMap(artifacts, multiProvider);
@ -498,22 +433,19 @@ async function writeAgentConfig(
startBlocks[chain] = (await mailbox.deployedBlock()).toNumber();
}
const mergedAddressesMap = objMerge(
sdkContractAddressesMap,
artifacts,
) as ChainMap<HyperlaneDeploymentArtifacts>;
const chainAddresses = await registry.getAddresses();
for (const chain of chains) {
if (!mergedAddressesMap[chain].interchainGasPaymaster) {
mergedAddressesMap[chain].interchainGasPaymaster =
if (!chainAddresses[chain].interchainGasPaymaster) {
chainAddresses[chain].interchainGasPaymaster =
ethers.constants.AddressZero;
}
}
const agentConfig = buildAgentConfig(
chains, // Use only the chains that were deployed to
multiProvider,
mergedAddressesMap,
chainAddresses as any,
startBlocks,
);
writeJson(filePath, agentConfig);
writeJson(outPath, agentConfig);
logGreen('Agent configs written');
}

@ -6,7 +6,6 @@ import {
setFork,
} from '@hyperlane-xyz/sdk';
import { Command } from '../commands/deploy.js';
import { logGray, logGreen, warnYellow } from '../logger.js';
import { ENV } from '../utils/env.js';
@ -51,14 +50,14 @@ export async function verifyAnvil() {
* @param error the thrown error
* @param dryRun the chain name to execute the dry-run on
*/
export function evaluateIfDryRunFailure(error: any, dryRun: string) {
export function evaluateIfDryRunFailure(error: any, dryRun: boolean) {
if (dryRun && error.message.includes('call revert exception'))
warnYellow(
'⛔ [dry-run] The current RPC may not support forking. Please consider using a different RPC provider.',
);
}
export async function completeDryRun(command: Command) {
export async function completeDryRun(command: string) {
await resetFork();
logGreen(`${toUpperCamelCase(command)} dry-run completed successfully`);

@ -4,14 +4,13 @@ import {
ChainMap,
ChainName,
IsmConfig,
MultiProvider,
MultisigConfig,
getLocalProvider,
} from '@hyperlane-xyz/sdk';
import { Address, ProtocolType } from '@hyperlane-xyz/utils';
import { Command } from '../commands/deploy.js';
import { parseIsmConfig } from '../config/ism.js';
import { WriteCommandContext } from '../context/types.js';
import { log, logGreen, logPink } from '../logger.js';
import { assertGasBalances } from '../utils/balances.js';
import { ENV } from '../utils/env.js';
@ -20,17 +19,15 @@ import { assertSigner } from '../utils/keys.js';
import { completeDryRun } from './dry-run.js';
export async function runPreflightChecks({
context,
origin,
remotes,
signer,
multiProvider,
minGas,
chainsToGasCheck,
}: {
context: WriteCommandContext;
origin: ChainName;
remotes: ChainName[];
signer: ethers.Signer;
multiProvider: MultiProvider;
minGas: string;
chainsToGasCheck?: ChainName[];
}) {
@ -44,30 +41,28 @@ export async function runPreflightChecks({
logGreen('✅ Origin and remote are distinct');
return runPreflightChecksForChains({
context,
chains: [origin, ...remotes],
signer,
multiProvider,
minGas,
chainsToGasCheck,
});
}
export async function runPreflightChecksForChains({
context,
chains,
signer,
multiProvider,
minGas,
chainsToGasCheck,
}: {
context: WriteCommandContext;
chains: ChainName[];
signer: ethers.Signer;
multiProvider: MultiProvider;
minGas: string;
// Chains for which to assert a native balance
// Defaults to all chains if not specified
chainsToGasCheck?: ChainName[];
}) {
log('Running pre-flight checks for chains...');
const { signer, multiProvider } = context;
if (!chains?.length) throw new Error('Empty chain selection');
for (const chain of chains) {
@ -103,15 +98,15 @@ export function isZODISMConfig(filepath: string): boolean {
}
export async function prepareDeploy(
multiProvider: MultiProvider,
context: WriteCommandContext,
userAddress: Address,
chains: ChainName[],
dryRun: boolean = false,
): Promise<Record<string, BigNumber>> {
const { multiProvider, isDryRun } = context;
const initialBalances: Record<string, BigNumber> = {};
await Promise.all(
chains.map(async (chain: ChainName) => {
const provider = dryRun
const provider = isDryRun
? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT)
: multiProvider.getProvider(chain);
const currentBalance = await provider.getBalance(userAddress);
@ -122,31 +117,31 @@ export async function prepareDeploy(
}
export async function completeDeploy(
command: Command,
context: WriteCommandContext,
command: string,
initialBalances: Record<string, BigNumber>,
multiProvider: MultiProvider,
userAddress: Address,
chains: ChainName[],
dryRun: string,
) {
const { multiProvider, isDryRun } = context;
if (chains.length > 0) logPink(` Gas Usage Statistics`);
for (const chain of chains) {
const provider = dryRun
const provider = isDryRun
? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT)
: multiProvider.getProvider(chain);
const currentBalance = await provider.getBalance(userAddress);
const balanceDelta = initialBalances[chain].sub(currentBalance);
if (dryRun && balanceDelta.lt(0)) break;
if (isDryRun && balanceDelta.lt(0)) break;
logPink(
`\t- Gas required for ${command} ${
dryRun ? 'dry-run' : 'deploy'
isDryRun ? 'dry-run' : 'deploy'
} on ${chain}: ${ethers.utils.formatEther(balanceDelta)} ${
multiProvider.getChainMetadata(chain).nativeToken?.symbol
}`,
);
}
if (dryRun) await completeDryRun(command);
if (isDryRun) await completeDryRun(command);
}
export function toUpperCamelCase(string: string) {

@ -1,5 +1,4 @@
import { confirm, input } from '@inquirer/prompts';
import { ethers } from 'ethers';
import {
ChainMap,
@ -12,6 +11,7 @@ import {
MinimalTokenMetadata,
MultiProtocolProvider,
MultiProvider,
RouterConfig,
TOKEN_TYPE_TO_STANDARD,
TokenConfig,
TokenFactories,
@ -24,45 +24,25 @@ import {
isNativeConfig,
isSyntheticConfig,
} from '@hyperlane-xyz/sdk';
import { RouterConfig } from '@hyperlane-xyz/sdk';
import { Address, ProtocolType, objMap } from '@hyperlane-xyz/utils';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { Command } from '../commands/deploy.js';
import { readWarpRouteDeployConfig } from '../config/warp.js';
import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js';
import {
getContext,
getDryRunContext,
getMergedContractAddresses,
} from '../context.js';
import { WriteCommandContext } from '../context/types.js';
import { log, logBlue, logGray, logGreen } from '../logger.js';
import {
getArtifactsFiles,
isFile,
prepNewArtifactsFiles,
runFileSelectionStep,
writeJson,
} from '../utils/files.js';
import { isFile, runFileSelectionStep } from '../utils/files.js';
import { completeDeploy, prepareDeploy, runPreflightChecks } from './utils.js';
export async function runWarpRouteDeploy({
key,
chainConfigPath,
context,
warpRouteDeploymentConfigPath,
coreArtifactsPath,
outPath,
skipConfirmation,
dryRun,
}: {
key?: string;
chainConfigPath: string;
context: WriteCommandContext;
warpRouteDeploymentConfigPath?: string;
coreArtifactsPath?: string;
outPath: string;
skipConfirmation: boolean;
dryRun: string;
}) {
const { signer, skipConfirmation } = context;
if (
!warpRouteDeploymentConfigPath ||
!isFile(warpRouteDeploymentConfigPath)
@ -83,36 +63,14 @@ export async function runWarpRouteDeploy({
warpRouteDeploymentConfigPath,
);
const { multiProvider, signer, coreArtifacts } = dryRun
? await getDryRunContext({
chainConfigPath,
chains: [dryRun],
coreConfig: { coreArtifactsPath },
keyConfig: { key },
skipConfirmation,
})
: await getContext({
chainConfigPath,
coreConfig: { coreArtifactsPath },
keyConfig: { key },
skipConfirmation,
});
const configs = await runBuildConfigStep({
context,
warpRouteConfig,
coreArtifacts,
multiProvider,
signer,
skipConfirmation,
});
const deploymentParams = {
context,
...configs,
signer,
multiProvider,
outPath,
skipConfirmation,
dryRun,
};
logBlue('Warp route deployment plan');
@ -123,41 +81,26 @@ export async function runWarpRouteDeploy({
minGas: MINIMUM_WARP_DEPLOY_GAS,
});
const userAddress = dryRun ? key! : await signer.getAddress();
const userAddress = await signer.getAddress();
const chains = [deploymentParams.origin, ...configs.remotes];
const initialBalances = await prepareDeploy(
multiProvider,
userAddress,
chains,
);
const initialBalances = await prepareDeploy(context, userAddress, chains);
await executeDeploy(deploymentParams);
await completeDeploy(
Command.WARP,
initialBalances,
multiProvider,
userAddress,
chains,
dryRun,
);
await completeDeploy(context, 'warp', initialBalances, userAddress, chains);
}
async function runBuildConfigStep({
context,
warpRouteConfig,
multiProvider,
signer,
coreArtifacts,
skipConfirmation,
}: {
context: WriteCommandContext;
warpRouteConfig: WarpRouteDeployConfig;
multiProvider: MultiProvider;
signer: ethers.Signer;
coreArtifacts?: HyperlaneContractsMap<any>;
skipConfirmation: boolean;
}) {
const { registry, signer, multiProvider, skipConfirmation } = context;
log('Assembling token configs');
const chainAddresses = await registry.getAddresses();
const owner = await signer.getAddress();
const requiredRouterFields: Array<keyof ConnectionClientConfig> = ['mailbox'];
const remotes: string[] = [];
@ -167,20 +110,16 @@ async function runBuildConfigStep({
/// @todo Remove this artifact when multi-collateral is enabled
let baseChainName = '';
let baseMetadata = {} as MinimalTokenMetadata;
// Create config that coalesce together values from the config file,
// Define configs that coalesce together values from the config file
for (const [chain, config] of Object.entries(warpRouteConfig)) {
const mergedContractAddrs = getMergedContractAddresses(
coreArtifacts,
Object.keys(warpRouteConfig),
);
// the artifacts, and the SDK as a fallback
config.owner = owner;
config.mailbox = config.mailbox || mergedContractAddrs[chain]?.mailbox;
config.mailbox = config.mailbox || chainAddresses[chain]?.mailbox;
config.interchainSecurityModule =
config.interchainSecurityModule ||
mergedContractAddrs[chain]?.interchainSecurityModule ||
mergedContractAddrs[chain]?.multisigIsm;
// config.ismFactory: mergedContractAddrs[baseChainName].domainRoutingIsmFactory, // TODO fix when updating from routingIsm
chainAddresses[chain]?.interchainSecurityModule ||
chainAddresses[chain]?.multisigIsm;
// config.ismFactory: chainAddresses[baseChainName].domainRoutingIsmFactory, // TODO fix when updating from routingIsm
if (isCollateralConfig(config) || isNativeConfig(config)) {
// Store the base metadata
@ -232,24 +171,20 @@ async function runBuildConfigStep({
}
interface DeployParams {
context: WriteCommandContext;
configMap: WarpRouteDeployConfig;
metadata: MinimalTokenMetadata;
origin: ChainName;
remotes: ChainName[];
signer: ethers.Signer;
multiProvider: MultiProvider;
outPath: string;
skipConfirmation: boolean;
dryRun: string;
}
async function runDeployPlanStep({
context,
configMap,
origin,
remotes,
signer,
skipConfirmation,
}: DeployParams) {
const { signer, skipConfirmation } = context;
const address = await signer.getAddress();
const baseToken = configMap[origin];
@ -273,27 +208,16 @@ async function runDeployPlanStep({
async function executeDeploy(params: DeployParams) {
logBlue('All systems ready, captain! Beginning deployment...');
const { configMap, multiProvider, outPath } = params;
const [contractsFilePath, tokenConfigPath] = prepNewArtifactsFiles(
outPath,
getArtifactsFiles(
[
{
filename: 'warp-route-deployment',
description: 'Contract addresses',
},
{ filename: 'warp-config', description: 'Warp config' },
],
params.dryRun,
),
);
const {
configMap,
context: { registry, multiProvider, isDryRun },
} = params;
const deployer = configMap.isNft
? new HypERC721Deployer(multiProvider)
: new HypERC20Deployer(multiProvider);
const config = params.dryRun
const config = isDryRun
? { [params.origin]: configMap[params.origin] }
: configMap;
@ -304,12 +228,10 @@ async function executeDeploy(params: DeployParams) {
logGreen('✅ Hyp token deployments complete');
log('Writing deployment artifacts');
writeTokenDeploymentArtifacts(contractsFilePath, deployedContracts, params);
writeWarpConfig(tokenConfigPath, deployedContracts, params);
const warpCoreConfig = getWarpCoreConfig(params, deployedContracts);
await registry.addWarpRoute(warpCoreConfig);
log(JSON.stringify(warpCoreConfig, null, 2));
logBlue('Deployment is complete!');
logBlue(`Contract address artifacts are in ${contractsFilePath}`);
logBlue(`Warp config is in ${tokenConfigPath}`);
}
async function fetchBaseTokenMetadata(
@ -344,28 +266,11 @@ async function fetchBaseTokenMetadata(
function getTokenName(token: TokenConfig) {
return token.type === TokenType.native ? 'native' : token.name;
}
function writeTokenDeploymentArtifacts(
filePath: string,
contracts: HyperlaneContractsMap<TokenFactories>,
{ configMap }: DeployParams,
) {
const artifacts: ChainMap<{
router: Address;
tokenType: TokenType;
}> = objMap(contracts, (chain, contract) => {
return {
router: contract[configMap[chain].type as keyof TokenFactories].address,
tokenType: configMap[chain].type,
};
});
writeJson(filePath, artifacts);
}
function writeWarpConfig(
filePath: string,
contracts: HyperlaneContractsMap<TokenFactories>,
function getWarpCoreConfig(
{ configMap, metadata }: DeployParams,
) {
contracts: HyperlaneContractsMap<TokenFactories>,
): WarpCoreConfig {
const warpCoreConfig: WarpCoreConfig = { tokens: [] };
// First pass, create token configs
@ -406,5 +311,5 @@ function writeWarpConfig(
}
}
writeJson(filePath, warpCoreConfig);
return warpCoreConfig;
}

@ -1,48 +1,34 @@
import { ChainName, EvmHookReader } from '@hyperlane-xyz/sdk';
import { Address, ProtocolType, stringifyObject } from '@hyperlane-xyz/utils';
import { readChainConfigsIfExists } from '../config/chain.js';
import { getMultiProvider } from '../context.js';
import { CommandContext } from '../context/types.js';
import { log, logBlue, logRed } from '../logger.js';
import {
FileFormat,
resolveFileFormat,
writeFileAtPath,
} from '../utils/files.js';
import { resolveFileFormat, writeFileAtPath } from '../utils/files.js';
/**
* Read Hook config for a specified chain and address, logging or writing result to file.
*/
export async function readHookConfig({
context,
chain,
address,
chainConfigPath,
format,
output,
out,
}: {
context: CommandContext;
chain: ChainName;
address: Address;
chainConfigPath: string;
format: FileFormat;
output?: string;
out?: string;
}): Promise<void> {
const customChains = readChainConfigsIfExists(chainConfigPath);
const multiProvider = getMultiProvider(customChains);
if (multiProvider.getProtocol(chain) === ProtocolType.Ethereum) {
const hookReader = new EvmHookReader(multiProvider, chain);
if (context.multiProvider.getProtocol(chain) === ProtocolType.Ethereum) {
const hookReader = new EvmHookReader(context.multiProvider, chain);
const config = await hookReader.deriveHookConfig(address);
const stringConfig = stringifyObject(
config,
resolveFileFormat(output, format),
2,
);
if (!output) {
const stringConfig = stringifyObject(config, resolveFileFormat(out), 2);
if (!out) {
logBlue(`Hook Config at ${address} on ${chain}:`);
log(stringConfig);
} else {
writeFileAtPath(output, stringConfig + '\n');
logBlue(`Hook Config written to ${output}.`);
writeFileAtPath(out, stringConfig + '\n');
logBlue(`Hook Config written to ${out}.`);
}
return;
}

@ -1,48 +1,34 @@
import { ChainName, EvmIsmReader } from '@hyperlane-xyz/sdk';
import { Address, ProtocolType, stringifyObject } from '@hyperlane-xyz/utils';
import { readChainConfigsIfExists } from '../config/chain.js';
import { getMultiProvider } from '../context.js';
import { CommandContext } from '../context/types.js';
import { log, logBlue, logRed } from '../logger.js';
import {
FileFormat,
resolveFileFormat,
writeFileAtPath,
} from '../utils/files.js';
import { resolveFileFormat, writeFileAtPath } from '../utils/files.js';
/**
* Read ISM config for a specified chain and address, logging or writing result to file.
*/
export async function readIsmConfig({
context,
chain,
address,
chainConfigPath,
format,
output,
out,
}: {
context: CommandContext;
chain: ChainName;
address: Address;
chainConfigPath: string;
format: FileFormat;
output?: string;
out?: string;
}): Promise<void> {
const customChains = readChainConfigsIfExists(chainConfigPath);
const multiProvider = getMultiProvider(customChains);
if (multiProvider.getProtocol(chain) === ProtocolType.Ethereum) {
const ismReader = new EvmIsmReader(multiProvider, chain);
if (context.multiProvider.getProtocol(chain) === ProtocolType.Ethereum) {
const ismReader = new EvmIsmReader(context.multiProvider, chain);
const config = await ismReader.deriveIsmConfig(address);
const stringConfig = stringifyObject(
config,
resolveFileFormat(output, format),
2,
);
if (!output) {
const stringConfig = stringifyObject(config, resolveFileFormat(out), 2);
if (!out) {
logBlue(`ISM Config at ${address} on ${chain}:`);
log(stringConfig);
} else {
writeFileAtPath(output, stringConfig + '\n');
logBlue(`ISM Config written to ${output}.`);
writeFileAtPath(out, stringConfig + '\n');
logBlue(`ISM Config written to ${out}.`);
}
return;
}

@ -0,0 +1,156 @@
import { Logger } from 'pino';
import {
BaseRegistry,
ChainAddresses,
GithubRegistry,
IRegistry,
RegistryContent,
RegistryType,
} from '@hyperlane-xyz/registry';
import { LocalRegistry } from '@hyperlane-xyz/registry/local';
import {
ChainMap,
ChainMetadata,
ChainName,
WarpCoreConfig,
} from '@hyperlane-xyz/sdk';
import {
isHttpsUrl,
objKeys,
objMerge,
rootLogger,
} from '@hyperlane-xyz/utils';
export interface MergedRegistryOptions {
registryUris: Array<string>;
isDryRun?: boolean;
logger?: Logger;
}
export class MergedRegistry extends BaseRegistry implements IRegistry {
public readonly type = RegistryType.Local;
public readonly registries: Array<IRegistry>;
public readonly isDryRun: boolean;
constructor({ registryUris, logger, isDryRun }: MergedRegistryOptions) {
logger ||= rootLogger.child({ module: 'MergedRegistry' });
super({ uri: '__merged_registry__', logger });
if (!registryUris.length)
throw new Error('At least one registry URI is required');
this.registries = registryUris.map((uri, index) => {
if (isHttpsUrl(uri)) {
return new GithubRegistry({ uri, logger: logger!.child({ index }) });
} else {
return new LocalRegistry({ uri, logger: logger!.child({ index }) });
}
});
this.isDryRun = !!isDryRun;
}
async listRegistryContent(): Promise<RegistryContent> {
const results = await this.multiRegistryRead((r) =>
r.listRegistryContent(),
);
return results.reduce((acc, content) => objMerge(acc, content), {
chains: {},
deployments: {},
});
}
async getChains(): Promise<Array<ChainName>> {
return objKeys(await this.getMetadata);
}
async getMetadata(): Promise<ChainMap<ChainMetadata>> {
const results = await this.multiRegistryRead((r) => r.getMetadata());
return results.reduce((acc, content) => objMerge(acc, content), {});
}
async getChainMetadata(chainName: ChainName): Promise<ChainMetadata | null> {
return (await this.getMetadata())[chainName] || null;
}
async getAddresses(): Promise<ChainMap<ChainAddresses>> {
const results = await this.multiRegistryRead((r) => r.getAddresses());
return results.reduce((acc, content) => objMerge(acc, content), {});
}
async getChainAddresses(
chainName: ChainName,
): Promise<ChainAddresses | null> {
return (await this.getAddresses())[chainName] || null;
}
async addChain(chain: {
chainName: ChainName;
metadata?: ChainMetadata;
addresses?: ChainAddresses;
}): Promise<void> {
return this.multiRegistryWrite(
async (registry) => await registry.addChain(chain),
`adding chain ${chain.chainName}`,
);
}
async updateChain(chain: {
chainName: ChainName;
metadata?: ChainMetadata;
addresses?: ChainAddresses;
}): Promise<void> {
return this.multiRegistryWrite(
async (registry) => await registry.updateChain(chain),
`updating chain ${chain.chainName}`,
);
}
async removeChain(chain: ChainName): Promise<void> {
return this.multiRegistryWrite(
async (registry) => await registry.removeChain(chain),
`removing chain ${chain}`,
);
}
async addWarpRoute(config: WarpCoreConfig): Promise<void> {
return this.multiRegistryWrite(
async (registry) => await registry.addWarpRoute(config),
'adding warp route',
);
}
protected multiRegistryRead<R>(
readFn: (registry: IRegistry) => Promise<R> | R,
) {
return Promise.all(this.registries.map(readFn));
}
protected async multiRegistryWrite(
writeFn: (registry: IRegistry) => Promise<void>,
logMsg: string,
): Promise<void> {
if (this.isDryRun) return;
for (const registry of this.registries) {
// TODO remove this when GithubRegistry supports write methods
if (registry.type === RegistryType.Github) {
this.logger.warn(`skipping ${logMsg} at ${registry.type} registry`);
continue;
}
try {
this.logger.info(
`${logMsg} at ${registry.type} registry at ${registry.uri}`,
);
await writeFn(registry);
this.logger.info(`done ${logMsg} at ${registry.type} registry`);
} catch (error) {
// To prevent loss of artifacts, MergedRegistry write methods are failure tolerant
this.logger.error(
`failure ${logMsg} at ${registry.type} registry`,
error,
);
}
}
}
}

@ -1,23 +1,16 @@
import { ethers } from 'ethers';
import {
ChainName,
HyperlaneContractsMap,
HyperlaneCore,
MultiProvider,
} from '@hyperlane-xyz/sdk';
import { ChainName, HyperlaneCore } from '@hyperlane-xyz/sdk';
import { addressToBytes32, timeout } from '@hyperlane-xyz/utils';
import { MINIMUM_TEST_SEND_GAS } from '../consts.js';
import { getContext, getMergedContractAddresses } from '../context.js';
import { CommandContext, WriteCommandContext } from '../context/types.js';
import { runPreflightChecks } from '../deploy/utils.js';
import { errorRed, log, logBlue, logGreen } from '../logger.js';
import { runSingleChainSelectionStep } from '../utils/chains.js';
export async function sendTestMessage({
key,
chainConfigPath,
coreArtifactsPath,
context,
origin,
destination,
messageBody,
@ -25,9 +18,7 @@ export async function sendTestMessage({
skipWaitForDelivery,
selfRelay,
}: {
key?: string;
chainConfigPath: string;
coreArtifactsPath?: string;
context: WriteCommandContext;
origin?: ChainName;
destination?: ChainName;
messageBody: string;
@ -35,43 +26,36 @@ export async function sendTestMessage({
skipWaitForDelivery: boolean;
selfRelay?: boolean;
}) {
const { signer, multiProvider, customChains, coreArtifacts } =
await getContext({
chainConfigPath,
coreConfig: { coreArtifactsPath },
keyConfig: { key },
});
const { chainMetadata } = context;
if (!origin) {
origin = await runSingleChainSelectionStep(
customChains,
chainMetadata,
'Select the origin chain',
);
}
if (!destination) {
destination = await runSingleChainSelectionStep(
customChains,
chainMetadata,
'Select the destination chain',
);
}
await runPreflightChecks({
context,
origin,
remotes: [destination],
multiProvider,
signer,
minGas: MINIMUM_TEST_SEND_GAS,
chainsToGasCheck: [origin],
});
await timeout(
executeDelivery({
context,
origin,
destination,
messageBody,
multiProvider,
coreArtifacts,
skipWaitForDelivery,
selfRelay,
}),
@ -81,30 +65,26 @@ export async function sendTestMessage({
}
async function executeDelivery({
context,
origin,
destination,
messageBody,
multiProvider,
coreArtifacts,
skipWaitForDelivery,
selfRelay,
}: {
context: CommandContext;
origin: ChainName;
destination: ChainName;
messageBody: string;
multiProvider: MultiProvider;
coreArtifacts?: HyperlaneContractsMap<any>;
skipWaitForDelivery: boolean;
selfRelay?: boolean;
}) {
const mergedContractAddrs = getMergedContractAddresses(coreArtifacts);
const core = HyperlaneCore.fromAddressesMap(
mergedContractAddrs,
multiProvider,
);
const { registry, multiProvider } = context;
const chainAddresses = await registry.getAddresses();
const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider);
const mailbox = core.getContracts(origin).mailbox;
let hook = mergedContractAddrs[origin]?.customHook;
let hook = chainAddresses[origin]?.customHook;
if (hook) {
logBlue(`Using custom hook ${hook} for ${origin} -> ${destination}`);
} else {
@ -115,7 +95,7 @@ async function executeDelivery({
const destinationDomain = multiProvider.getDomainId(destination);
let txReceipt: ethers.ContractReceipt;
try {
const recipient = mergedContractAddrs[destination].testRecipient;
const recipient = chainAddresses[destination].testRecipient;
if (!recipient) {
throw new Error(`Unable to find TestRecipient for ${destination}`);
}
@ -153,6 +133,7 @@ async function executeDelivery({
log(`Message: ${JSON.stringify(message)}`);
if (selfRelay) {
log('Attempting self-relay of message');
await core.relayMessage(message);
logGreen('Message was self-relayed!');
return;

@ -1,94 +1,78 @@
import { select } from '@inquirer/prompts';
import { ethers } from 'ethers';
import {
ChainName,
HyperlaneContractsMap,
HyperlaneCore,
MultiProtocolProvider,
MultiProvider,
ProviderType,
Token,
TokenAmount,
WarpCore,
WarpCoreConfig,
} from '@hyperlane-xyz/sdk';
import { Address, timeout } from '@hyperlane-xyz/utils';
import { timeout } from '@hyperlane-xyz/utils';
import { readWarpRouteConfig } from '../config/warp.js';
import { MINIMUM_TEST_SEND_GAS } from '../consts.js';
import { getContext, getMergedContractAddresses } from '../context.js';
import { WriteCommandContext } from '../context/types.js';
import { runPreflightChecks } from '../deploy/utils.js';
import { logBlue, logGreen, logRed } from '../logger.js';
import { runSingleChainSelectionStep } from '../utils/chains.js';
import { runTokenSelectionStep } from '../utils/tokens.js';
export async function sendTestTransfer({
key,
chainConfigPath,
coreArtifactsPath,
context,
warpConfigPath,
origin,
destination,
routerAddress,
wei,
recipient,
timeoutSec,
skipWaitForDelivery,
selfRelay,
}: {
key?: string;
chainConfigPath: string;
coreArtifactsPath?: string;
context: WriteCommandContext;
warpConfigPath: string;
origin?: ChainName;
destination?: ChainName;
routerAddress?: Address;
wei: string;
recipient?: string;
timeoutSec: number;
skipWaitForDelivery: boolean;
selfRelay?: boolean;
}) {
const { signer, multiProvider, customChains, coreArtifacts, warpCoreConfig } =
await getContext({
chainConfigPath,
coreConfig: { coreArtifactsPath },
keyConfig: { key },
warpConfig: { warpConfigPath },
});
const { chainMetadata } = context;
const warpCoreConfig = readWarpRouteConfig(warpConfigPath);
if (!origin) {
origin = await runSingleChainSelectionStep(
customChains,
chainMetadata,
'Select the origin chain',
);
}
if (!destination) {
destination = await runSingleChainSelectionStep(
customChains,
chainMetadata,
'Select the destination chain',
);
}
await runPreflightChecks({
context,
origin,
remotes: [destination],
multiProvider,
signer,
minGas: MINIMUM_TEST_SEND_GAS,
chainsToGasCheck: [origin],
});
await timeout(
executeDelivery({
context,
origin,
destination,
warpCoreConfig,
routerAddress,
wei,
recipient,
signer,
multiProvider,
coreArtifacts,
skipWaitForDelivery,
selfRelay,
}),
@ -98,39 +82,32 @@ export async function sendTestTransfer({
}
async function executeDelivery({
context,
origin,
destination,
warpCoreConfig,
routerAddress,
wei,
recipient,
multiProvider,
signer,
coreArtifacts,
skipWaitForDelivery,
selfRelay,
}: {
context: WriteCommandContext;
origin: ChainName;
destination: ChainName;
warpCoreConfig: WarpCoreConfig;
routerAddress?: Address;
wei: string;
recipient?: string;
multiProvider: MultiProvider;
signer: ethers.Signer;
coreArtifacts?: HyperlaneContractsMap<any>;
skipWaitForDelivery: boolean;
selfRelay?: boolean;
}) {
const { signer, multiProvider, registry } = context;
const signerAddress = await signer.getAddress();
recipient ||= signerAddress;
const mergedContractAddrs = getMergedContractAddresses(coreArtifacts);
const chainAddresses = await registry.getAddresses();
const core = HyperlaneCore.fromAddressesMap(
mergedContractAddrs,
multiProvider,
);
const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider);
const provider = multiProvider.getProvider(origin);
const connectedSigner = signer.connect(provider);
@ -140,31 +117,17 @@ async function executeDelivery({
warpCoreConfig,
);
if (!routerAddress) {
const tokensForRoute = warpCore.getTokensForRoute(origin, destination);
if (tokensForRoute.length === 0) {
logRed(`No Warp Routes found from ${origin} to ${destination}`);
throw new Error('Error finding warp route');
}
routerAddress = (await select({
message: `Select router address`,
choices: [
...tokensForRoute.map((t) => ({
value: t.addressOrDenom,
description: `${t.name} ($${t.symbol})`,
})),
],
pageSize: 10,
})) as string;
}
const token = warpCore.findToken(origin, routerAddress);
if (!token) {
logRed(
`No Warp Routes found from ${origin} to ${destination} with router address ${routerAddress}`,
);
let token: Token;
const tokensForRoute = warpCore.getTokensForRoute(origin, destination);
if (tokensForRoute.length === 0) {
logRed(`No Warp Routes found from ${origin} to ${destination}`);
throw new Error('Error finding warp route');
} else if (tokensForRoute.length === 1) {
token = tokensForRoute[0];
} else {
logBlue(`Please select a token from the Warp config`);
const routerAddress = await runTokenSelectionStep(tokensForRoute);
token = warpCore.findToken(origin, routerAddress)!;
}
const senderAddress = await signer.getAddress();

@ -2,38 +2,26 @@ import { input } from '@inquirer/prompts';
import { ChainName, HyperlaneCore } from '@hyperlane-xyz/sdk';
import { getContext, getMergedContractAddresses } from '../context.js';
import { CommandContext } from '../context/types.js';
import { log, logBlue, logGreen } from '../logger.js';
import { runSingleChainSelectionStep } from '../utils/chains.js';
export async function checkMessageStatus({
chainConfigPath,
coreArtifactsPath,
context,
messageId,
destination,
origin,
selfRelay,
key,
}: {
chainConfigPath: string;
coreArtifactsPath?: string;
context: CommandContext;
messageId?: string;
destination?: ChainName;
origin?: ChainName;
selfRelay?: boolean;
key?: string;
}) {
const keyConfig = selfRelay ? { key } : undefined;
const { multiProvider, customChains, coreArtifacts } = await getContext({
chainConfigPath,
coreConfig: { coreArtifactsPath },
keyConfig,
});
if (!destination) {
destination = await runSingleChainSelectionStep(
customChains,
context.chainMetadata,
'Select the destination chain',
);
}
@ -44,10 +32,10 @@ export async function checkMessageStatus({
});
}
const mergedContractAddrs = getMergedContractAddresses(coreArtifacts);
const chainAddresses = await context.registry.getAddresses();
const core = HyperlaneCore.fromAddressesMap(
mergedContractAddrs,
multiProvider,
chainAddresses,
context.multiProvider,
);
const mailbox = core.getContracts(destination).mailbox;
log(`Checking status of message ${messageId} on ${destination}`);
@ -62,7 +50,7 @@ export async function checkMessageStatus({
// TODO: implement option for tx receipt input
if (!origin) {
origin = await runSingleChainSelectionStep(
customChains,
context.chainMetadata,
'Select the origin chain',
);
}

@ -2,24 +2,19 @@ import { Separator, checkbox } from '@inquirer/prompts';
import select from '@inquirer/select';
import chalk from 'chalk';
import {
ChainMap,
ChainMetadata,
mainnetChainsMetadata,
testnetChainsMetadata,
} from '@hyperlane-xyz/sdk';
import { ChainMap, ChainMetadata } from '@hyperlane-xyz/sdk';
import { log, logBlue, logRed, logTip } from '../logger.js';
import { log, logRed, logTip } from '../logger.js';
// A special value marker to indicate user selected
// a new chain in the list
const NEW_CHAIN_MARKER = '__new__';
export async function runSingleChainSelectionStep(
customChains: ChainMap<ChainMetadata>,
chainMetadata: ChainMap<ChainMetadata>,
message = 'Select chain',
) {
const choices = getChainChoices(customChains);
const choices = getChainChoices(chainMetadata);
const chain = (await select({
message,
choices,
@ -30,11 +25,11 @@ export async function runSingleChainSelectionStep(
}
export async function runMultiChainSelectionStep(
customChains: ChainMap<ChainMetadata>,
chainMetadata: ChainMap<ChainMetadata>,
message = 'Select chains',
requireMultiple = false,
) {
const choices = getChainChoices(customChains);
const choices = getChainChoices(chainMetadata);
while (true) {
logTip('Use SPACE key to select chains, then press ENTER');
const chains = (await checkbox({
@ -51,26 +46,25 @@ export async function runMultiChainSelectionStep(
}
}
function getChainChoices(customChains: ChainMap<ChainMetadata>) {
function getChainChoices(chainMetadata: ChainMap<ChainMetadata>) {
const chainsToChoices = (chains: ChainMetadata[]) =>
chains.map((c) => ({ name: c.name, value: c.name }));
const chains = Object.values(chainMetadata);
const testnetChains = chains.filter((c) => !!c.isTestnet);
const mainnetChains = chains.filter((c) => !c.isTestnet);
const choices: Parameters<typeof select>['0']['choices'] = [
new Separator('--Custom Chains--'),
...chainsToChoices(Object.values(customChains)),
{ name: '(New custom chain)', value: NEW_CHAIN_MARKER },
new Separator('--Mainnet Chains--'),
...chainsToChoices(mainnetChainsMetadata),
...chainsToChoices(mainnetChains),
new Separator('--Testnet Chains--'),
...chainsToChoices(testnetChainsMetadata),
...chainsToChoices(testnetChains),
];
return choices;
}
function handleNewChain(chainNames: string[]) {
if (chainNames.includes(NEW_CHAIN_MARKER)) {
logBlue(
'To use a new chain, use the --config argument add them to that file',
);
log(
chalk.blue('Use the'),
chalk.magentaBright('hyperlane config create'),

@ -6,9 +6,7 @@ import { parse as yamlParse, stringify as yamlStringify } from 'yaml';
import { objMerge } from '@hyperlane-xyz/utils';
import { log, logBlue } from '../logger.js';
import { getTimestampForFilename } from './time.js';
import { log } from '../logger.js';
export type FileFormat = 'yaml' | 'json';
@ -118,7 +116,7 @@ export function writeYamlOrJson(
export function mergeYamlOrJson(
filepath: string,
obj: Record<string, any>,
format?: FileFormat,
format: FileFormat = 'yaml',
) {
return resolveYamlOrJsonFn(
filepath,
@ -170,38 +168,6 @@ export function resolveFileFormat(
return undefined;
}
export function prepNewArtifactsFiles(
outPath: string,
files: Array<ArtifactsFile>,
) {
const timestamp = getTimestampForFilename();
const newPaths: string[] = [];
for (const file of files) {
const filePath = path.join(outPath, `${file.filename}-${timestamp}.json`);
// Write empty object to ensure permissions are okay
writeJson(filePath, {});
newPaths.push(filePath);
logBlue(`${file.description} will be written to ${filePath}`);
}
return newPaths;
}
/**
* Retrieves artifacts file metadata for the current command.
* @param dryRun whether or not the current command is being dry-run
* @returns the artifacts files
*/
export function getArtifactsFiles(
defaultFiles: ArtifactsFile[],
dryRun: string = '',
): Array<ArtifactsFile> {
if (dryRun)
defaultFiles.map((defaultFile: ArtifactsFile) => {
defaultFile.filename = `dry-run_${defaultFile.filename}`;
});
return defaultFiles;
}
export async function runFileSelectionStep(
folderPath: string,
description: string,

@ -4,8 +4,6 @@ import { ethers, providers } from 'ethers';
import { impersonateAccount } from '@hyperlane-xyz/sdk';
import { Address, ensure0x } from '@hyperlane-xyz/utils';
import { ContextSettings, KeyConfig } from '../context.js';
const ETHEREUM_ADDRESS_LENGTH = 42;
const DEFAULT_KEY_TYPE = 'private key';
const IMPERSONATED_KEY_TYPE = 'address';
@ -14,34 +12,32 @@ const IMPERSONATED_KEY_TYPE = 'address';
* Retrieves a signer for the current command-context.
* @returns the signer
*/
export async function getSigner<P extends ContextSettings>({
keyConfig,
export async function getSigner({
key,
skipConfirmation,
}: P): Promise<providers.JsonRpcSigner | ethers.Wallet | undefined> {
if (!keyConfig) return undefined;
const key = await retrieveKey(DEFAULT_KEY_TYPE, keyConfig, skipConfirmation);
return privateKeyToSigner(key);
}: {
key?: string;
skipConfirmation?: boolean;
}) {
key ||= await retrieveKey(DEFAULT_KEY_TYPE, skipConfirmation);
const signer = privateKeyToSigner(key);
return { key, signer };
}
/**
* Retrieves an impersonated signer for the current command-context.
* @returns the impersonated signer
*/
export async function getImpersonatedSigner<P extends ContextSettings>({
keyConfig,
export async function getImpersonatedSigner({
key,
skipConfirmation,
}: P): Promise<providers.JsonRpcSigner | ethers.Wallet | undefined> {
if (!keyConfig) return undefined;
const key = await retrieveKey(
IMPERSONATED_KEY_TYPE,
keyConfig,
skipConfirmation,
);
return await addressToImpersonatedSigner(key);
}: {
key?: string;
skipConfirmation?: boolean;
}) {
key ||= await retrieveKey(IMPERSONATED_KEY_TYPE, skipConfirmation);
const signer = await addressToImpersonatedSigner(key);
return { key, signer };
}
/**
@ -91,15 +87,11 @@ function privateKeyToSigner(key: string): ethers.Wallet {
async function retrieveKey(
keyType: string,
keyConfig: KeyConfig,
skipConfirmation: boolean | undefined,
): Promise<string> {
if (keyConfig.key) return keyConfig.key;
else if (skipConfirmation) throw new Error(`No ${keyType} provided`);
if (skipConfirmation) throw new Error(`No ${keyType} provided`);
else
return await input({
message:
keyConfig.promptMessage ||
`Please enter ${keyType} or use the HYP_KEY environment variable.`,
message: `Please enter ${keyType} or use the HYP_KEY environment variable.`,
});
}

@ -0,0 +1,19 @@
import select from '@inquirer/select';
import { Token } from '@hyperlane-xyz/sdk';
export async function runTokenSelectionStep(
tokens: Token[],
message = 'Select token',
) {
const choices = tokens.map((t) => ({
name: `${t.symbol} - ${t.addressOrDenom}`,
value: t.addressOrDenom,
}));
const routerAddress = (await select({
message,
choices,
pageSize: 20,
})) as string;
return routerAddress;
}

@ -4,7 +4,12 @@ import { log } from '../logger.js';
import { VERSION } from '../version.js';
export async function checkVersion() {
const argv = process.argv;
// The latestVersion lib (or one of its deps) is confused by the --registry value
// in the CLI's args, so we need to clear the args before calling it
process.argv = [];
const currentVersion = await latestVersion('@hyperlane-xyz/cli');
process.argv = argv;
if (VERSION < currentVersion) {
log(`Your CLI version: ${VERSION}, latest version: ${currentVersion}`);
}

@ -0,0 +1,14 @@
# Configs for describing chain metadata for use in Hyperlane deployments or apps
# Consists of a map of chain names to metadata
# Schema here: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/metadata/chainMetadataTypes.ts
---
chainId: 31337
domainId: 31337
name: anvil1
protocol: ethereum
rpcUrls:
- http: http://127.0.0.1:8545
nativeToken:
name: Ether
symbol: ETH
decimals: 18

@ -0,0 +1,7 @@
---
chainId: 31338
domainId: 31338
name: anvil2
protocol: ethereum
rpcUrls:
- http: http://127.0.0.1:8555

@ -0,0 +1,13 @@
chainId: 44787
domainId: 44787
name: alfajores
nativeToken:
decimals: 18
name: CELO
symbol: CELO
protocol: ethereum
rpcUrls:
- http: https://alfajores-forno.celo-testnet.org
blocks:
confirmations: 1
estimateBlockTime: 1

@ -0,0 +1,10 @@
chainId: 31337
domainId: 31337
name: anvil
protocol: ethereum
rpcUrls:
- http: http://127.0.0.1:8545
nativeToken:
name: Ether
symbol: ETH
decimals: 18

@ -0,0 +1,6 @@
chainId: 43113
domainId: 43113
name: fuji
protocol: ethereum
rpcUrls:
- http: https://api.avax-test.network/ext/bc/C/rpc

@ -0,0 +1,11 @@
---
chainId: 31337
domainId: 31337
name: anvil1
protocol: ethereum
rpcUrls:
- http: http://127.0.0.1:8545
nativeToken:
name: Ether
symbol: ETH
decimals: 18

@ -0,0 +1,9 @@
chainId: 1
domainId: 1
name: ethereum
protocol: ethereum
rpcUrls:
- http: http://127.0.0.1:8555
blocks:
confirmations: 1
estimateBlockTime: 1

@ -1,9 +1,8 @@
# A config for a multisig Interchain Security Module (ISM)
# Schema: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/ism/types.ts
#
---
anvil:
anvil1:
threshold: 1 # Number: Signatures required to approve a message
validators: # Array: List of validator addresses
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'

@ -10,7 +10,7 @@
# fastCollateral
# fastSynthetic
---
anvil:
anvil1:
type: native
# token: "0x123" # Collateral/vault address. Required for collateral types
# owner: "0x123" # Optional owner address for synthetic token

@ -4,6 +4,7 @@
"version": "3.10.0",
"dependencies": {
"@hyperlane-xyz/core": "3.10.0",
"@hyperlane-xyz/registry": "^1.0.7",
"@hyperlane-xyz/sdk": "3.10.0",
"@openzeppelin/contracts-upgradeable": "^4.9.3",
"ethers": "^5.7.2"

@ -1,8 +1,9 @@
import { RouterConfig, chainMetadata } from '@hyperlane-xyz/sdk';
import { chainMetadata } from '@hyperlane-xyz/registry';
import { RouterConfig } from '@hyperlane-xyz/sdk';
export type HelloWorldConfig = RouterConfig;
// SET DESIRED NETWORKS HERE
// SET DESIRED NETWORKS HERE OR USE THE DEFAULT SET FROM THE REGISTRY
export const prodConfigs = {
alfajores: chainMetadata.alfajores,
...chainMetadata,
};

@ -1,8 +1,11 @@
import { chainAddresses } from '@hyperlane-xyz/registry';
import {
ChainMap,
HyperlaneCore,
MultiProvider,
attachContractsMap,
} from '@hyperlane-xyz/sdk';
import type { Address } from '@hyperlane-xyz/utils';
import { HelloWorldApp } from '../app/app.js';
import { helloWorldFactories } from '../app/contracts.js';
@ -10,7 +13,7 @@ import { HelloWorldChecker } from '../deploy/check.js';
import { prodConfigs } from '../deploy/config.js';
// COPY FROM OUTPUT OF DEPLOYMENT SCRIPT OR IMPORT FROM ELSEWHERE
const deploymentAddresses = {};
const deploymentAddresses: ChainMap<Record<string, Address>> = {};
// SET CONTRACT OWNER ADDRESS HERE
const ownerAddress = '0x123...';
@ -24,7 +27,9 @@ async function check() {
helloWorldFactories,
);
const core = HyperlaneCore.fromEnvironment('testnet', multiProvider);
// If the default registry does not contain the core contract addresses you need,
// Replace `chainAddresses` with a custom map of addresses
const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider);
const app = new HelloWorldApp(core, contractsMap, multiProvider);
const config = core.getRouterConfig(ownerAddress);

@ -1,5 +1,6 @@
import { Wallet } from 'ethers';
import { chainAddresses } from '@hyperlane-xyz/registry';
import {
HyperlaneCore,
MultiProvider,
@ -17,7 +18,9 @@ async function main() {
const multiProvider = new MultiProvider(prodConfigs);
multiProvider.setSharedSigner(signer);
const core = HyperlaneCore.fromEnvironment('testnet', multiProvider);
// If the default registry does not contain the core contract addresses you need,
// Replace `chainAddresses` with a custom map of addresses
const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider);
const config = core.getRouterConfig(signer.address);
const deployer = new HelloWorldDeployer(multiProvider);

@ -4,10 +4,10 @@ import hre from 'hardhat';
import {
ChainMap,
Chains,
HyperlaneIsmFactory,
HyperlaneProxyFactoryDeployer,
MultiProvider,
TestChainName,
TestCoreApp,
TestCoreDeployer,
} from '@hyperlane-xyz/sdk';
@ -17,8 +17,8 @@ import { HelloWorldDeployer } from '../deploy/deploy.js';
import { HelloWorld } from '../types/index.js';
describe('HelloWorld', async () => {
const localChain = Chains.test1;
const remoteChain = Chains.test2;
const localChain = TestChainName.test1;
const remoteChain = TestChainName.test2;
let localDomain: number;
let remoteDomain: number;

@ -0,0 +1,8 @@
# @hyperlane-xyz/infra
Various scripts and utilities for managing and deploying Hyperlane infrastructure.
## Tips
- To enable more verbose logging, see the log env vars mentioned in the [root readme](../../README.md)
- To configure the local registry path, set the `REGISTRY_URI` env var

@ -1,10 +1,7 @@
import {
Chains,
GasPaymentEnforcement,
GasPaymentEnforcementPolicyType,
RpcConsensusType,
chainMetadata,
getDomainId,
} from '@hyperlane-xyz/sdk';
import {
@ -19,8 +16,9 @@ import {
import { ALL_KEY_ROLES, Role } from '../../../src/roles.js';
import { Contexts } from '../../contexts.js';
import { environment, supportedChainNames } from './chains.js';
import { environment } from './chains.js';
import { helloWorld } from './helloworld.js';
import { supportedChainNames } from './supportedChainNames.js';
import { validatorChainConfig } from './validators.js';
import ancient8EthereumUsdcAddresses from './warp/ancient8-USDC-addresses.json';
import arbitrumTIAAddresses from './warp/arbitrum-TIA-addresses.json';
@ -46,74 +44,74 @@ const repo = 'gcr.io/abacus-labs-dev/hyperlane-agent';
export const hyperlaneContextAgentChainConfig: AgentChainConfig = {
// Generally, we run all production validators in the Hyperlane context.
[Role.Validator]: {
[Chains.arbitrum]: true,
[Chains.ancient8]: true,
[Chains.avalanche]: true,
[Chains.base]: true,
[Chains.blast]: true,
[Chains.bsc]: true,
[Chains.celo]: true,
[Chains.ethereum]: true,
[Chains.neutron]: true,
[Chains.mantapacific]: true,
[Chains.mode]: true,
[Chains.moonbeam]: true,
[Chains.optimism]: true,
[Chains.polygon]: true,
[Chains.gnosis]: true,
[Chains.scroll]: true,
[Chains.polygonzkevm]: true,
[Chains.injective]: true,
[Chains.inevm]: true,
[Chains.viction]: true,
arbitrum: true,
ancient8: true,
avalanche: true,
base: true,
blast: true,
bsc: true,
celo: true,
ethereum: true,
neutron: true,
mantapacific: true,
mode: true,
moonbeam: true,
optimism: true,
polygon: true,
gnosis: true,
scroll: true,
polygonzkevm: true,
injective: true,
inevm: true,
viction: true,
},
[Role.Relayer]: {
[Chains.arbitrum]: true,
[Chains.ancient8]: true,
[Chains.avalanche]: true,
[Chains.base]: true,
[Chains.blast]: true,
[Chains.bsc]: true,
[Chains.celo]: true,
[Chains.ethereum]: true,
arbitrum: true,
ancient8: true,
avalanche: true,
base: true,
blast: true,
bsc: true,
celo: true,
ethereum: true,
// At the moment, we only relay between Neutron and Manta Pacific on the neutron context.
[Chains.neutron]: false,
[Chains.mantapacific]: true,
[Chains.mode]: true,
[Chains.moonbeam]: true,
[Chains.optimism]: true,
[Chains.polygon]: true,
[Chains.gnosis]: true,
[Chains.scroll]: true,
[Chains.polygonzkevm]: true,
[Chains.injective]: true,
[Chains.inevm]: true,
[Chains.viction]: true,
neutron: false,
mantapacific: false,
mode: true,
moonbeam: true,
optimism: true,
polygon: true,
gnosis: true,
scroll: true,
polygonzkevm: true,
injective: true,
inevm: true,
viction: true,
},
[Role.Scraper]: {
[Chains.arbitrum]: true,
[Chains.ancient8]: true,
[Chains.avalanche]: true,
[Chains.base]: true,
[Chains.blast]: true,
[Chains.bsc]: true,
[Chains.celo]: true,
[Chains.ethereum]: true,
arbitrum: true,
ancient8: true,
avalanche: true,
base: true,
blast: true,
bsc: true,
celo: true,
ethereum: true,
// Cannot scrape non-EVM chains
[Chains.neutron]: false,
[Chains.mantapacific]: true,
[Chains.mode]: true,
[Chains.moonbeam]: true,
[Chains.optimism]: true,
[Chains.polygon]: true,
[Chains.gnosis]: true,
[Chains.scroll]: true,
[Chains.polygonzkevm]: true,
neutron: false,
mantapacific: true,
mode: true,
moonbeam: true,
optimism: true,
polygon: true,
gnosis: true,
scroll: true,
polygonzkevm: true,
// Cannot scrape non-EVM chains
[Chains.injective]: false,
[Chains.inevm]: true,
injective: false,
inevm: true,
// Has RPC non-compliance that breaks scraping.
[Chains.viction]: false,
viction: false,
},
};
@ -257,11 +255,7 @@ const neutron: RootAgentConfig = {
...contextBase,
contextChainNames: {
validator: [],
relayer: [
chainMetadata.neutron.name,
chainMetadata.mantapacific.name,
chainMetadata.arbitrum.name,
],
relayer: ['neutron', 'mantapacific', 'arbitrum'],
scraper: [],
},
context: Contexts.Neutron,

@ -1,17 +1,11 @@
import {
ChainMap,
ChainMetadata,
Mainnets,
chainMetadata,
} from '@hyperlane-xyz/sdk';
import { ChainMap, ChainMetadata } from '@hyperlane-xyz/sdk';
import { objKeys } from '@hyperlane-xyz/utils';
import { getChainMetadatas } from '../../../src/config/chain.js';
import { getChain } from '../../registry.js';
// The `Mainnets` from the SDK are all supported chains for the mainnet3 environment.
// These chains may be any protocol type.
export const supportedChainNames = Mainnets;
import { supportedChainNames } from './supportedChainNames.js';
export type MainnetChains = (typeof supportedChainNames)[number];
export const environment = 'mainnet3';
const {
@ -22,15 +16,15 @@ const {
export const ethereumMainnetConfigs: ChainMap<ChainMetadata> = {
...defaultEthereumMainnetConfigs,
bsc: {
...chainMetadata.bsc,
...getChain('bsc'),
transactionOverrides: {
gasPrice: 3 * 10 ** 9, // 3 gwei
},
},
polygon: {
...chainMetadata.polygon,
...getChain('polygon'),
blocks: {
...chainMetadata.polygon.blocks,
...getChain('polygon').blocks,
confirmations: 3,
},
transactionOverrides: {
@ -41,9 +35,9 @@ export const ethereumMainnetConfigs: ChainMap<ChainMetadata> = {
},
},
ethereum: {
...chainMetadata.ethereum,
...getChain('ethereum'),
blocks: {
...chainMetadata.ethereum.blocks,
...getChain('ethereum').blocks,
confirmations: 3,
},
transactionOverrides: {
@ -52,7 +46,7 @@ export const ethereumMainnetConfigs: ChainMap<ChainMetadata> = {
},
},
scroll: {
...chainMetadata.scroll,
...getChain('scroll'),
transactionOverrides: {
// Scroll doesn't use EIP 1559 and the gas price that's returned is sometimes
// too low for the transaction to be included in a reasonable amount of time -
@ -61,7 +55,7 @@ export const ethereumMainnetConfigs: ChainMap<ChainMetadata> = {
},
},
moonbeam: {
...chainMetadata.moonbeam,
...getChain('moonbeam'),
transactionOverrides: {
maxFeePerGas: 350 * 10 ** 9, // 350 gwei
maxPriorityFeePerGas: 50 * 10 ** 9, // 50 gwei
@ -74,6 +68,4 @@ export const mainnetConfigs: ChainMap<ChainMetadata> = {
...nonEthereumMainnetConfigs,
};
export const ethereumChainNames = Object.keys(
ethereumMainnetConfigs,
) as MainnetChains[];
export const ethereumChainNames = objKeys(ethereumMainnetConfigs);

@ -18,15 +18,18 @@ import {
RoutingIsmConfig,
defaultMultisigConfigs,
} from '@hyperlane-xyz/sdk';
import { Address, objMap } from '@hyperlane-xyz/utils';
import { Address, ProtocolType, objMap } from '@hyperlane-xyz/utils';
import { getChain } from '../../registry.js';
import { supportedChainNames } from './chains.js';
import { igp } from './igp.js';
import { DEPLOYER, owners } from './owners.js';
import { supportedChainNames } from './supportedChainNames.js';
export const core: ChainMap<CoreConfig> = objMap(owners, (local, owner) => {
const originMultisigs: ChainMap<MultisigConfig> = Object.fromEntries(
supportedChainNames
.filter((chain) => getChain(chain).protocol === ProtocolType.Ethereum)
.filter((chain) => chain !== local)
.map((origin) => [origin, defaultMultisigConfigs[origin]]),
);

@ -16,20 +16,17 @@ import {
getTokenExchangeRateFromValues,
} from '../../../src/config/gas-oracle.js';
import {
MainnetChains,
ethereumChainNames,
supportedChainNames,
} from './chains.js';
import { ethereumChainNames } from './chains.js';
import gasPrices from './gasPrices.json';
import { DEPLOYER, owners } from './owners.js';
import { supportedChainNames } from './supportedChainNames.js';
import rawTokenPrices from './tokenPrices.json';
const tokenPrices: ChainMap<string> = rawTokenPrices;
const FOREIGN_DEFAULT_OVERHEAD = 600_000; // cosmwasm warp route somewhat arbitrarily chosen
const remoteOverhead = (remote: MainnetChains) =>
const remoteOverhead = (remote: ChainName) =>
ethereumChainNames.includes(remote)
? multisigIsmVerificationCost(
defaultMultisigConfigs[remote].threshold,
@ -53,11 +50,11 @@ function getTokenExchangeRate(local: ChainName, remote: ChainName): BigNumber {
const storageGasOracleConfig: AllStorageGasOracleConfigs =
getAllStorageGasOracleConfigs(
supportedChainNames,
ethereumChainNames,
gasPrices,
getTokenExchangeRate,
(local) => parseFloat(tokenPrices[local]),
(local) => remoteOverhead(local as MainnetChains),
(local) => remoteOverhead(local),
);
export const igp: ChainMap<IgpConfig> = objMap(owners, (local, owner) => ({
@ -72,7 +69,7 @@ export const igp: ChainMap<IgpConfig> = objMap(owners, (local, owner) => ({
overhead: Object.fromEntries(
exclude(local, supportedChainNames).map((remote) => [
remote,
remoteOverhead(remote as MainnetChains),
remoteOverhead(remote),
]),
),
oracleConfig: storageGasOracleConfig[local],

@ -2,29 +2,27 @@ import {
BridgeAdapterConfig,
BridgeAdapterType,
ChainMap,
Chains,
RpcConsensusType,
chainMetadata,
getDomainId,
} from '@hyperlane-xyz/sdk';
import { LiquidityLayerRelayerConfig } from '../../../src/config/middleware.js';
import { getDomainId } from '../../registry.js';
import { environment } from './chains.js';
const circleDomainMapping = [
{
hyperlaneDomain: getDomainId(chainMetadata[Chains.ethereum]),
hyperlaneDomain: getDomainId('ethereum'),
circleDomain: 0,
},
{
hyperlaneDomain: getDomainId(chainMetadata[Chains.avalanche]),
hyperlaneDomain: getDomainId('avalanche'),
circleDomain: 1,
},
];
export const bridgeAdapterConfigs: ChainMap<BridgeAdapterConfig> = {
[Chains.ethereum]: {
ethereum: {
circle: {
type: BridgeAdapterType.Circle,
tokenMessengerAddress: '0xBd3fa81B58Ba92a82136038B25aDec7066af3155',
@ -33,7 +31,7 @@ export const bridgeAdapterConfigs: ChainMap<BridgeAdapterConfig> = {
circleDomainMapping,
},
},
[Chains.avalanche]: {
avalanche: {
circle: {
type: BridgeAdapterType.Circle,
tokenMessengerAddress: '0x6B25532e1060CE10cc3B0A99e5683b91BFDe6982',

@ -1,11 +1,8 @@
import {
AddressesMap,
ChainMap,
OwnableConfig,
hyperlaneEnvironments,
} from '@hyperlane-xyz/sdk';
import { AddressesMap, ChainMap, OwnableConfig } from '@hyperlane-xyz/sdk';
import { Address, objFilter, objMap } from '@hyperlane-xyz/utils';
import { getMainnetAddresses } from '../../registry.js';
import { ethereumChainNames } from './chains.js';
export const timelocks: ChainMap<Address | undefined> = {
@ -13,8 +10,7 @@ export const timelocks: ChainMap<Address | undefined> = {
};
export function localAccountRouters(): ChainMap<Address> {
const coreAddresses: ChainMap<AddressesMap> =
hyperlaneEnvironments['mainnet'];
const coreAddresses: ChainMap<AddressesMap> = getMainnetAddresses();
const filteredAddresses = objFilter(
coreAddresses,
(local, addressMap): addressMap is AddressesMap =>

@ -0,0 +1,24 @@
// These chains may be any protocol type.
// Placing them here instead of adjacent chains file to avoid circular dep
export const supportedChainNames = [
'arbitrum',
'ancient8',
'avalanche',
'blast',
'bsc',
'celo',
'ethereum',
'neutron',
'mantapacific',
'mode',
'moonbeam',
'optimism',
'polygon',
'gnosis',
'base',
'scroll',
'polygonzkevm',
'injective',
'inevm',
'viction',
];

@ -1,19 +1,18 @@
import {
BridgeAdapterType,
ChainMap,
Chains,
CircleBridgeAdapterConfig,
chainMetadata,
getDomainId,
} from '@hyperlane-xyz/sdk';
import { getDomainId } from '../../registry.js';
const circleDomainMapping = [
{ hyperlaneDomain: getDomainId(chainMetadata[Chains.fuji]), circleDomain: 1 },
{ hyperlaneDomain: getDomainId('fuji'), circleDomain: 1 },
];
// Circle deployed contracts
export const circleBridgeAdapterConfig: ChainMap<CircleBridgeAdapterConfig> = {
[Chains.fuji]: {
fuji: {
type: BridgeAdapterType.Circle,
tokenMessengerAddress: '0x0fc1103927af27af808d03135214718bcedbe9ad',
messageTransmitterAddress: '0x52fffb3ee8fa7838e9858a2d5e454007b9027c3c',

@ -1,7 +1,6 @@
import { chainMetadata, getReorgPeriod } from '@hyperlane-xyz/sdk';
import { ValidatorBaseChainConfigMap } from '../../../src/config/agent/validator.js';
import { Contexts } from '../../contexts.js';
import { getReorgPeriod } from '../../registry.js';
import { validatorBaseConfigsFn } from '../utils.js';
import { environment } from './chains.js';
@ -13,7 +12,7 @@ export const validatorChainConfig = (
return {
ancient8: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.ancient8),
reorgPeriod: getReorgPeriod('ancient8'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: ['0xbb5842ae0e05215b53df4787a29144efb7e67551'],
@ -27,7 +26,7 @@ export const validatorChainConfig = (
},
celo: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.celo),
reorgPeriod: getReorgPeriod('celo'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -47,7 +46,7 @@ export const validatorChainConfig = (
},
ethereum: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.ethereum),
reorgPeriod: getReorgPeriod('ethereum'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -67,7 +66,7 @@ export const validatorChainConfig = (
},
avalanche: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.avalanche),
reorgPeriod: getReorgPeriod('avalanche'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -87,7 +86,7 @@ export const validatorChainConfig = (
},
polygon: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.polygon),
reorgPeriod: getReorgPeriod('polygon'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -107,7 +106,7 @@ export const validatorChainConfig = (
},
bsc: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.bsc),
reorgPeriod: getReorgPeriod('bsc'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -127,7 +126,7 @@ export const validatorChainConfig = (
},
arbitrum: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.arbitrum),
reorgPeriod: getReorgPeriod('arbitrum'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -147,7 +146,7 @@ export const validatorChainConfig = (
},
optimism: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.optimism),
reorgPeriod: getReorgPeriod('optimism'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -167,7 +166,7 @@ export const validatorChainConfig = (
},
moonbeam: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.moonbeam),
reorgPeriod: getReorgPeriod('moonbeam'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -187,7 +186,7 @@ export const validatorChainConfig = (
},
gnosis: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.gnosis),
reorgPeriod: getReorgPeriod('gnosis'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -207,7 +206,7 @@ export const validatorChainConfig = (
},
base: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.base),
reorgPeriod: getReorgPeriod('base'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -227,7 +226,7 @@ export const validatorChainConfig = (
},
injective: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.injective),
reorgPeriod: getReorgPeriod('injective'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: ['0xbfb8911b72cfb138c7ce517c57d9c691535dc517'],
@ -239,7 +238,7 @@ export const validatorChainConfig = (
},
inevm: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.inevm),
reorgPeriod: getReorgPeriod('inevm'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -259,7 +258,7 @@ export const validatorChainConfig = (
},
scroll: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.scroll),
reorgPeriod: getReorgPeriod('scroll'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -279,7 +278,7 @@ export const validatorChainConfig = (
},
polygonzkevm: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.polygonzkevm),
reorgPeriod: getReorgPeriod('polygonzkevm'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -299,7 +298,7 @@ export const validatorChainConfig = (
},
neutron: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.neutron),
reorgPeriod: getReorgPeriod('neutron'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -319,7 +318,7 @@ export const validatorChainConfig = (
},
mantapacific: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.mantapacific),
reorgPeriod: getReorgPeriod('mantapacific'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -339,7 +338,7 @@ export const validatorChainConfig = (
},
viction: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.viction),
reorgPeriod: getReorgPeriod('viction'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: ['0x1f87c368f8e05a85ef9126d984a980a20930cb9c'],
@ -355,7 +354,7 @@ export const validatorChainConfig = (
},
blast: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.blast),
reorgPeriod: getReorgPeriod('blast'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: ['0xf20c0b09f597597c8d2430d3d72dfddaf09177d1'],
@ -369,7 +368,7 @@ export const validatorChainConfig = (
},
mode: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.mode),
reorgPeriod: getReorgPeriod('mode'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: ['0x7eb2e1920a4166c19d6884c1cec3d2cf356fc9b7'],

@ -7,7 +7,7 @@ import { RootAgentConfig } from '../../../src/config/agent/agent.js';
import { ALL_KEY_ROLES } from '../../../src/roles.js';
import { Contexts } from '../../contexts.js';
import { agentChainNames, chainNames } from './chains.js';
import { agentChainNames, testChainNames } from './chains.js';
import { validators } from './validators.js';
const roleBase = {
@ -24,7 +24,7 @@ const hyperlane: RootAgentConfig = {
context: Contexts.Hyperlane,
rolesWithKeys: ALL_KEY_ROLES,
contextChainNames: agentChainNames,
environmentChainNames: chainNames,
environmentChainNames: testChainNames,
relayer: {
...roleBase,
gasPaymentEnforcement: [

@ -1,18 +1,15 @@
import { ChainMap, ChainMetadata, chainMetadata } from '@hyperlane-xyz/sdk';
import {
testChainMetadata as defaultTestChainMetadata,
testChains as defaultTestChains,
} from '@hyperlane-xyz/sdk';
import { AgentChainNames, Role } from '../../../src/roles.js';
export const testConfigs: ChainMap<ChainMetadata> = {
test1: chainMetadata.test1,
test2: chainMetadata.test2,
test3: chainMetadata.test3,
};
export type TestChains = keyof typeof testConfigs;
export const chainNames = Object.keys(testConfigs) as TestChains[];
export const testChainNames = defaultTestChains;
export const testChainMetadata = { ...defaultTestChainMetadata };
export const agentChainNames: AgentChainNames = {
[Role.Validator]: chainNames,
[Role.Relayer]: chainNames,
[Role.Scraper]: chainNames,
[Role.Validator]: testChainNames,
[Role.Relayer]: testChainNames,
[Role.Scraper]: testChainNames,
};

@ -12,7 +12,7 @@ import {
getAllStorageGasOracleConfigs,
} from '../../../src/config/gas-oracle.js';
import { chainNames } from './chains.js';
import { testChainNames } from './chains.js';
const TEST_TOKEN_EXCHANGE_RATE = ethers.utils.parseUnits(
'1',
@ -37,4 +37,8 @@ function getTokenExchangeRate(
}
export const storageGasOracleConfig: AllStorageGasOracleConfigs =
getAllStorageGasOracleConfigs(chainNames, gasPrices, getTokenExchangeRate);
getAllStorageGasOracleConfigs(
testChainNames,
gasPrices,
getTokenExchangeRate,
);

@ -1,18 +1,19 @@
import {
ChainMap,
ChainName,
GasOracleContractType,
IgpConfig,
multisigIsmVerificationCost,
} from '@hyperlane-xyz/sdk';
import { Address, exclude, objMap } from '@hyperlane-xyz/utils';
import { TestChains, chainNames } from './chains.js';
import { testChainNames } from './chains.js';
import { multisigIsm } from './multisigIsm.js';
import { owners } from './owners.js';
function getGasOracles(local: TestChains) {
function getGasOracles(local: ChainName) {
return Object.fromEntries(
exclude(local, chainNames).map((name) => [
exclude(local, testChainNames).map((name) => [
name,
GasOracleContractType.StorageGasOracle,
]),
@ -21,7 +22,7 @@ function getGasOracles(local: TestChains) {
export const igp: ChainMap<IgpConfig> = objMap(owners, (chain, ownerConfig) => {
const overhead = Object.fromEntries(
exclude(chain, chainNames).map((remote) => [
exclude(chain, testChainNames).map((remote) => [
remote,
multisigIsmVerificationCost(
multisigIsm[remote].threshold,

@ -1,11 +1,10 @@
import { JsonRpcProvider } from '@ethersproject/providers';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { MultiProvider, testChainMetadata } from '@hyperlane-xyz/sdk';
import { EnvironmentConfig } from '../../../src/config/environment.js';
import { agents } from './agent.js';
import { testConfigs } from './chains.js';
import { core } from './core.js';
import { igp } from './igp.js';
import { infra } from './infra.js';
@ -13,7 +12,7 @@ import { owners } from './owners.js';
export const environment: EnvironmentConfig = {
environment: 'test',
chainMetadataConfigs: testConfigs,
chainMetadataConfigs: testChainMetadata,
agents,
core,
igp,

@ -1,9 +1,9 @@
import { ChainMap, OwnableConfig } from '@hyperlane-xyz/sdk';
import { chainNames } from './chains.js';
import { testChainNames } from './chains.js';
// Owner is hardhat account 0
const OWNER_ADDRESS = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
export const owners: ChainMap<OwnableConfig> = Object.fromEntries(
chainNames.map((chain) => [chain, { owner: OWNER_ADDRESS }]),
testChainNames.map((chain) => [chain, { owner: OWNER_ADDRESS }]),
);

@ -1,5 +1,4 @@
import {
Chains,
GasPaymentEnforcement,
GasPaymentEnforcementPolicyType,
RpcConsensusType,
@ -14,8 +13,9 @@ import { routerMatchingList } from '../../../src/config/agent/relayer.js';
import { ALL_KEY_ROLES, Role } from '../../../src/roles.js';
import { Contexts } from '../../contexts.js';
import { environment, supportedChainNames } from './chains.js';
import { environment } from './chains.js';
import { helloWorld } from './helloworld.js';
import { supportedChainNames } from './supportedChainNames.js';
import { validatorChainConfig } from './validators.js';
import plumetestnetSepoliaAddresses from './warp/plumetestnet-sepolia-addresses.json';
@ -32,36 +32,36 @@ const repo = 'gcr.io/abacus-labs-dev/hyperlane-agent';
// to allow for more fine-grained control over which chains are enabled for each agent role.
export const hyperlaneContextAgentChainConfig: AgentChainConfig = {
[Role.Validator]: {
[Chains.alfajores]: true,
[Chains.bsctestnet]: true,
[Chains.eclipsetestnet]: true,
[Chains.fuji]: true,
[Chains.plumetestnet]: true,
[Chains.scrollsepolia]: true,
[Chains.sepolia]: true,
[Chains.solanatestnet]: true,
alfajores: true,
bsctestnet: true,
eclipsetestnet: true,
fuji: true,
plumetestnet: true,
scrollsepolia: true,
sepolia: true,
solanatestnet: true,
},
[Role.Relayer]: {
[Chains.alfajores]: true,
[Chains.bsctestnet]: true,
[Chains.eclipsetestnet]: true,
[Chains.fuji]: true,
[Chains.plumetestnet]: true,
[Chains.scrollsepolia]: true,
[Chains.sepolia]: true,
[Chains.solanatestnet]: true,
alfajores: true,
bsctestnet: true,
eclipsetestnet: true,
fuji: true,
plumetestnet: true,
scrollsepolia: true,
sepolia: true,
solanatestnet: true,
},
[Role.Scraper]: {
[Chains.alfajores]: true,
[Chains.bsctestnet]: true,
alfajores: true,
bsctestnet: true,
// Cannot scrape non-EVM chains
[Chains.eclipsetestnet]: false,
[Chains.fuji]: true,
[Chains.plumetestnet]: true,
[Chains.scrollsepolia]: true,
[Chains.sepolia]: true,
eclipsetestnet: false,
fuji: true,
plumetestnet: true,
scrollsepolia: true,
sepolia: true,
// Cannot scrape non-EVM chains
[Chains.solanatestnet]: false,
solanatestnet: false,
},
};

@ -1,33 +1,24 @@
import {
ChainMap,
ChainMetadata,
Chains,
chainMetadata,
} from '@hyperlane-xyz/sdk';
import { ChainMap, ChainMetadata } from '@hyperlane-xyz/sdk';
import { objKeys } from '@hyperlane-xyz/utils';
// All supported chains for the testnet4 environment.
// These chains may be any protocol type.
export const supportedChainNames = [
Chains.alfajores,
Chains.bsctestnet,
Chains.eclipsetestnet,
Chains.fuji,
Chains.plumetestnet,
Chains.scrollsepolia,
Chains.sepolia,
Chains.solanatestnet,
];
import { getChainMetadatas } from '../../../src/config/chain.js';
import { getChain } from '../../registry.js';
import { supportedChainNames } from './supportedChainNames.js';
export const environment = 'testnet4';
const { ethereumMetadatas: defaultEthereumMainnetConfigs } =
getChainMetadatas(supportedChainNames);
export const testnetConfigs: ChainMap<ChainMetadata> = {
...Object.fromEntries(
supportedChainNames.map((chain) => [chain, chainMetadata[chain]]),
),
...defaultEthereumMainnetConfigs,
bsctestnet: {
...chainMetadata.bsctestnet,
...getChain('bsctestnet'),
transactionOverrides: {
gasPrice: 8 * 10 ** 9, // 8 gwei
},
},
};
export const ethereumChainNames = objKeys(defaultEthereumMainnetConfigs);

@ -18,17 +18,20 @@ import {
RoutingIsmConfig,
defaultMultisigConfigs,
} from '@hyperlane-xyz/sdk';
import { Address, objMap } from '@hyperlane-xyz/utils';
import { Address, ProtocolType, objMap } from '@hyperlane-xyz/utils';
import { getChain } from '../../registry.js';
import { supportedChainNames } from './chains.js';
import { igp } from './igp.js';
import { owners } from './owners.js';
import { supportedChainNames } from './supportedChainNames.js';
export const core: ChainMap<CoreConfig> = objMap(
owners,
(local, ownerConfig) => {
const originMultisigs: ChainMap<MultisigConfig> = Object.fromEntries(
supportedChainNames
.filter((chain) => getChain(chain).protocol === ProtocolType.Ethereum)
.filter((chain) => chain !== local)
.map((origin) => [origin, defaultMultisigConfigs[origin]]),
);

@ -13,7 +13,7 @@ import {
getTokenExchangeRateFromValues,
} from '../../../src/config/gas-oracle.js';
import { supportedChainNames } from './chains.js';
import { ethereumChainNames } from './chains.js';
// Taken by looking at each testnet and overestimating gas prices
const gasPrices: ChainMap<BigNumber> = {
@ -72,7 +72,7 @@ function getTokenExchangeRate(local: ChainName, remote: ChainName): BigNumber {
export const storageGasOracleConfig: AllStorageGasOracleConfigs =
getAllStorageGasOracleConfigs(
supportedChainNames,
ethereumChainNames,
objMap(gasPrices, (_, gasPrice) => ({
amount: gasPrice.toString(),
decimals: 1,

@ -6,9 +6,9 @@ import {
} from '@hyperlane-xyz/sdk';
import { Address, exclude, objMap } from '@hyperlane-xyz/utils';
import { supportedChainNames } from './chains.js';
import { storageGasOracleConfig } from './gas-oracle.js';
import { owners } from './owners.js';
import { supportedChainNames } from './supportedChainNames.js';
export const igp: ChainMap<IgpConfig> = objMap(owners, (chain, ownerConfig) => {
return {

@ -2,32 +2,31 @@ import {
BridgeAdapterConfig,
BridgeAdapterType,
ChainMap,
Chains,
chainMetadata,
getDomainId,
} from '@hyperlane-xyz/sdk';
import { getDomainId } from '../../registry.js';
const circleDomainMapping = [
{ hyperlaneDomain: getDomainId(chainMetadata[Chains.fuji]), circleDomain: 1 },
{ hyperlaneDomain: getDomainId('fuji'), circleDomain: 1 },
];
const wormholeDomainMapping = [
{
hyperlaneDomain: getDomainId(chainMetadata[Chains.fuji]),
hyperlaneDomain: getDomainId('fuji'),
wormholeDomain: 6,
},
{
hyperlaneDomain: getDomainId(chainMetadata[Chains.bsctestnet]),
hyperlaneDomain: getDomainId('bsctestnet'),
wormholeDomain: 4,
},
{
hyperlaneDomain: getDomainId(chainMetadata[Chains.alfajores]),
hyperlaneDomain: getDomainId('alfajores'),
wormholeDomain: 14,
},
];
export const bridgeAdapterConfigs: ChainMap<BridgeAdapterConfig> = {
[Chains.fuji]: {
fuji: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x61E44E506Ca5659E6c0bba9b678586fA2d729756',
@ -41,14 +40,14 @@ export const bridgeAdapterConfigs: ChainMap<BridgeAdapterConfig> = {
circleDomainMapping,
},
},
[Chains.bsctestnet]: {
bsctestnet: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x9dcF9D205C9De35334D646BeE44b2D2859712A09',
wormholeDomainMapping,
},
},
[Chains.alfajores]: {
alfajores: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x05ca6037eC51F8b712eD2E6Fa72219FEaE74E153',

@ -1,13 +1,13 @@
import { ChainMap, OwnableConfig } from '@hyperlane-xyz/sdk';
import { supportedChainNames } from '../testnet4/chains.js';
import { ethereumChainNames } from './chains.js';
const ETHEREUM_DEPLOYER_ADDRESS = '0xfaD1C94469700833717Fa8a3017278BC1cA8031C';
// const SEALEVEL_DEPLOYER_ADDRESS = '6DjHX6Ezjpq3zZMZ8KsqyoFYo1zPSDoiZmLLkxD4xKXS';
export const owners: ChainMap<OwnableConfig> = {
...Object.fromEntries(
supportedChainNames.map((chain) => [
ethereumChainNames.map((chain) => [
chain,
{ owner: ETHEREUM_DEPLOYER_ADDRESS },
]),

@ -0,0 +1,12 @@
// These chains may be any protocol type.
// Placing them here instead of adjacent chains file to avoid circular dep
export const supportedChainNames = [
'alfajores',
'bsctestnet',
'eclipsetestnet',
'fuji',
'plumetestnet',
'scrollsepolia',
'sepolia',
'solanatestnet',
];

@ -2,32 +2,31 @@ import {
BridgeAdapterConfig,
BridgeAdapterType,
ChainMap,
Chains,
chainMetadata,
getDomainId,
} from '@hyperlane-xyz/sdk';
import { getDomainId } from '../../registry.js';
const circleDomainMapping = [
{ hyperlaneDomain: getDomainId(chainMetadata[Chains.fuji]), circleDomain: 1 },
{ hyperlaneDomain: getDomainId('fuji'), circleDomain: 1 },
];
const wormholeDomainMapping = [
{
hyperlaneDomain: getDomainId(chainMetadata[Chains.fuji]),
hyperlaneDomain: getDomainId('fuji'),
wormholeDomain: 6,
},
{
hyperlaneDomain: getDomainId(chainMetadata[Chains.bsctestnet]),
hyperlaneDomain: getDomainId('bsctestnet'),
wormholeDomain: 4,
},
{
hyperlaneDomain: getDomainId(chainMetadata[Chains.alfajores]),
hyperlaneDomain: getDomainId('alfajores'),
wormholeDomain: 14,
},
];
export const bridgeAdapterConfigs: ChainMap<BridgeAdapterConfig> = {
[Chains.fuji]: {
fuji: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x61E44E506Ca5659E6c0bba9b678586fA2d729756',
@ -41,14 +40,14 @@ export const bridgeAdapterConfigs: ChainMap<BridgeAdapterConfig> = {
circleDomainMapping,
},
},
[Chains.bsctestnet]: {
bsctestnet: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x9dcF9D205C9De35334D646BeE44b2D2859712A09',
wormholeDomainMapping,
},
},
[Chains.alfajores]: {
alfajores: {
portal: {
type: BridgeAdapterType.Portal,
portalBridgeAddress: '0x05ca6037eC51F8b712eD2E6Fa72219FEaE74E153',

@ -1,7 +1,6 @@
import { chainMetadata, getReorgPeriod } from '@hyperlane-xyz/sdk';
import { ValidatorBaseChainConfigMap } from '../../../src/config/agent/validator.js';
import { Contexts } from '../../contexts.js';
import { getReorgPeriod } from '../../registry.js';
import { validatorBaseConfigsFn } from '../utils.js';
import { environment } from './chains.js';
@ -13,7 +12,7 @@ export const validatorChainConfig = (
return {
alfajores: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.alfajores),
reorgPeriod: getReorgPeriod('alfajores'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -33,7 +32,7 @@ export const validatorChainConfig = (
},
fuji: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.alfajores),
reorgPeriod: getReorgPeriod('alfajores'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -53,7 +52,7 @@ export const validatorChainConfig = (
},
bsctestnet: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.bsctestnet),
reorgPeriod: getReorgPeriod('bsctestnet'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -73,7 +72,7 @@ export const validatorChainConfig = (
},
scrollsepolia: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.scrollsepolia),
reorgPeriod: getReorgPeriod('scrollsepolia'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -93,7 +92,7 @@ export const validatorChainConfig = (
},
sepolia: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.sepolia),
reorgPeriod: getReorgPeriod('sepolia'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -113,7 +112,7 @@ export const validatorChainConfig = (
},
plumetestnet: {
interval: 5,
reorgPeriod: getReorgPeriod(chainMetadata.plumetestnet),
reorgPeriod: getReorgPeriod('plumetestnet'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: [
@ -133,7 +132,7 @@ export const validatorChainConfig = (
},
solanatestnet: {
interval: 1,
reorgPeriod: getReorgPeriod(chainMetadata.solanatestnet),
reorgPeriod: getReorgPeriod('solanatestnet'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: ['0xd4ce8fa138d4e083fc0e480cca0dbfa4f5f30bd5'],
@ -145,7 +144,7 @@ export const validatorChainConfig = (
},
eclipsetestnet: {
interval: 1,
reorgPeriod: getReorgPeriod(chainMetadata.eclipsetestnet),
reorgPeriod: getReorgPeriod('eclipsetestnet'),
validators: validatorsConfig(
{
[Contexts.Hyperlane]: ['0xf344f34abca9a444545b5295066348a0ae22dda3'],

@ -1,4 +1,4 @@
import { CoreChainName } from '@hyperlane-xyz/sdk';
import { ChainName } from '@hyperlane-xyz/sdk';
import {
CheckpointSyncerType,
@ -16,7 +16,7 @@ export const s3BucketRegion = 'us-east-1';
export const s3BucketName = (
context: Contexts,
environment: string,
chainName: CoreChainName,
chainName: ChainName,
index: number,
) => `${context}-${environment}-${chainName}-validator-${index}`;
@ -35,7 +35,7 @@ export const validatorBaseConfigsFn = (
context: Contexts,
): ((
addresses: Record<Contexts, string[]>,
chain: CoreChainName,
chain: ChainName,
) => ValidatorBaseConfig[]) => {
return (addresses, chain) => {
return addresses[context].map((address, index) => {

@ -9,16 +9,8 @@ import {
import { DeployEnvironment } from '../src/config/environment.js';
import { Contexts } from './contexts.js';
import { supportedChainNames as mainnet3Chains } from './environments/mainnet3/chains.js';
import { chainNames as testChains } from './environments/test/chains.js';
import { supportedChainNames as testnet4Chains } from './environments/testnet4/chains.js';
import { rcMultisigIsmConfigs } from './rcMultisigIsmConfigs.js';
const chains = {
mainnet3: mainnet3Chains,
testnet4: testnet4Chains,
test: testChains,
};
import { getEnvChains } from './registry.js';
export const multisigIsms = (
env: DeployEnvironment,
@ -30,7 +22,12 @@ export const multisigIsms = (
context === Contexts.ReleaseCandidate
? rcMultisigIsmConfigs
: defaultMultisigConfigs;
return buildMultisigIsmConfigs(type, local, chains[env], multisigConfigs);
return buildMultisigIsmConfigs(
type,
local,
getEnvChains(env),
multisigConfigs,
);
};
export const multisigIsm = (

@ -0,0 +1,109 @@
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { ChainAddresses } from '@hyperlane-xyz/registry';
import { LocalRegistry } from '@hyperlane-xyz/registry/local';
import {
ChainMap,
ChainMetadata,
ChainName,
getDomainId as resolveDomainId,
getReorgPeriod as resolveReorgPeriod,
} from '@hyperlane-xyz/sdk';
import { objFilter, rootLogger } from '@hyperlane-xyz/utils';
import type { DeployEnvironment } from '../src/config/environment.js';
import { supportedChainNames as mainnet3Chains } from './environments/mainnet3/supportedChainNames.js';
import {
testChainMetadata,
testChainNames as testChains,
} from './environments/test/chains.js';
import { supportedChainNames as testnet4Chains } from './environments/testnet4/supportedChainNames.js';
const DEFAULT_REGISTRY_URI = join(
dirname(fileURLToPath(import.meta.url)),
'../../../../',
'hyperlane-registry',
);
// A global Registry singleton
// All uses of chain metadata or chain address artifacts should go through this registry.
let registry: LocalRegistry;
export function setRegistry(reg: LocalRegistry) {
registry = reg;
}
export function getRegistry(): LocalRegistry {
if (!registry) {
const registryUri = process.env.REGISTRY_URI || DEFAULT_REGISTRY_URI;
rootLogger.info('Using registry URI:', registryUri);
registry = new LocalRegistry({
uri: registryUri,
logger: rootLogger.child({ module: 'infra-registry' }),
});
}
return registry;
}
export function getChains(): ChainName[] {
return getRegistry().getChains();
}
export function getChain(chainName: ChainName): ChainMetadata {
if (testChains.includes(chainName)) {
return testChainMetadata[chainName];
}
return getRegistry().getChainMetadata(chainName);
}
export function getDomainId(chainName: ChainName): number {
return resolveDomainId(getChain(chainName));
}
export function getReorgPeriod(chainName: ChainName): number {
return resolveReorgPeriod(getChain(chainName));
}
export function getChainMetadata(): ChainMap<ChainMetadata> {
return getRegistry().getMetadata();
}
export function getChainAddresses(): ChainMap<ChainAddresses> {
return getRegistry().getAddresses();
}
export function getEnvChains(env: DeployEnvironment): ChainName[] {
if (env === 'mainnet3') return mainnet3Chains;
if (env === 'testnet4') return testnet4Chains;
if (env === 'test') return testChains;
throw Error(`Unsupported deploy environment: ${env}`);
}
export function getMainnets(): ChainName[] {
return getEnvChains('mainnet3');
}
export function getTestnets(): ChainName[] {
return getEnvChains('testnet4');
}
export function getEnvAddresses(
env: DeployEnvironment,
): ChainMap<ChainAddresses> {
const envChains = getEnvChains(env);
return objFilter(
getChainAddresses(),
(chain, addresses): addresses is ChainAddresses =>
getEnvChains(env).includes(chain),
);
}
export function getMainnetAddresses(): ChainMap<ChainAddresses> {
return getEnvAddresses('mainnet3');
}
export function getTestnetAddresses(): ChainMap<ChainAddresses> {
return getEnvAddresses('testnet4');
}

@ -6,22 +6,14 @@ import {
IsmType,
ModuleType,
RoutingIsmConfig,
TestChains,
} from '@hyperlane-xyz/sdk';
import { DeployEnvironment } from '../src/config/environment.js';
import { Contexts } from './contexts.js';
import { environments } from './environments/index.js';
import { ethereumChainNames as mainnet3Chains } from './environments/mainnet3/chains.js';
import { supportedChainNames as testnet4Chains } from './environments/testnet4/chains.js';
import { multisigIsm } from './multisigIsm.js';
const chains = {
test: TestChains,
testnet4: testnet4Chains,
mainnet3: mainnet3Chains,
};
import { getEnvChains } from './registry.js';
// Intended to be the "entrypoint" ISM.
// Routing ISM => Aggregation (1/2)
@ -34,7 +26,9 @@ export const routingIsm = (
local: ChainName,
context: Contexts,
): RoutingIsmConfig | string => {
const aggregationIsms: ChainMap<AggregationIsmConfig> = chains[environment]
const aggregationIsms: ChainMap<AggregationIsmConfig> = getEnvChains(
environment,
)
.filter((chain) => chain !== local)
.reduce(
(acc, chain) => ({

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save