diff --git a/.changeset/bright-emus-double.md b/.changeset/bright-emus-double.md
new file mode 100644
index 0000000000..d3c72ef212
--- /dev/null
+++ b/.changeset/bright-emus-double.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/cli': patch
+---
+
+Removes default pattern for chain name when creating a new chain.
diff --git a/.changeset/calm-eels-care.md b/.changeset/calm-eels-care.md
new file mode 100644
index 0000000000..a7911cd16e
--- /dev/null
+++ b/.changeset/calm-eels-care.md
@@ -0,0 +1,6 @@
+---
+'@hyperlane-xyz/cli': minor
+'@hyperlane-xyz/sdk': minor
+---
+
+Gracefully handle RPC failures during warp send & fix deriving hook error that prevents warp and core test messages on the cli.
diff --git a/.changeset/clean-bees-repeat.md b/.changeset/clean-bees-repeat.md
new file mode 100644
index 0000000000..0371000fab
--- /dev/null
+++ b/.changeset/clean-bees-repeat.md
@@ -0,0 +1,6 @@
+---
+'@hyperlane-xyz/cli': minor
+'@hyperlane-xyz/sdk': minor
+---
+
+Use metadata builders in message relaying
diff --git a/.changeset/five-baboons-smoke.md b/.changeset/five-baboons-smoke.md
new file mode 100644
index 0000000000..3cebc87ac8
--- /dev/null
+++ b/.changeset/five-baboons-smoke.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/sdk': minor
+---
+
+Add EvmWarpModule with create()
diff --git a/.changeset/five-owls-learn.md b/.changeset/five-owls-learn.md
new file mode 100644
index 0000000000..222e2e65f5
--- /dev/null
+++ b/.changeset/five-owls-learn.md
@@ -0,0 +1,5 @@
+---
+"@hyperlane-xyz/cli": patch
+---
+
+Add xerc20 limit lookups to warp read
diff --git a/.changeset/giant-lies-whisper.md b/.changeset/giant-lies-whisper.md
new file mode 100644
index 0000000000..374acdf494
--- /dev/null
+++ b/.changeset/giant-lies-whisper.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/cli': minor
+---
+
+Rename hyperlane config create chain -> hyperlane registry init. Rename all `configure` to `init`
diff --git a/.changeset/hip-melons-build.md b/.changeset/hip-melons-build.md
new file mode 100644
index 0000000000..c775deeb3c
--- /dev/null
+++ b/.changeset/hip-melons-build.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/cli': minor
+---
+
+Add a validator preFlightCheck command verifying that the validator has been announced for a given chain
diff --git a/.changeset/late-rings-attack.md b/.changeset/late-rings-attack.md
new file mode 100644
index 0000000000..613f32aa3f
--- /dev/null
+++ b/.changeset/late-rings-attack.md
@@ -0,0 +1,6 @@
+---
+'@hyperlane-xyz/cli': patch
+'@hyperlane-xyz/sdk': patch
+---
+
+Adds deployment support for IsmConfig within a WarpRouteConfig
diff --git a/.changeset/lovely-boxes-bow.md b/.changeset/lovely-boxes-bow.md
new file mode 100644
index 0000000000..38ee46ae59
--- /dev/null
+++ b/.changeset/lovely-boxes-bow.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/cli': minor
+---
+
+Add command to support creating agent configs
diff --git a/.changeset/mean-books-clean.md b/.changeset/mean-books-clean.md
new file mode 100644
index 0000000000..e41ee504d4
--- /dev/null
+++ b/.changeset/mean-books-clean.md
@@ -0,0 +1,7 @@
+---
+"@hyperlane-xyz/cli": minor
+"@hyperlane-xyz/helloworld": minor
+"@hyperlane-xyz/infra": minor
+---
+
+feat(cli): add `warp --symbol` flag
diff --git a/.changeset/new-taxis-fry.md b/.changeset/new-taxis-fry.md
new file mode 100644
index 0000000000..88882518fc
--- /dev/null
+++ b/.changeset/new-taxis-fry.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/cli': minor
+---
+
+Reintroduce `ism read` and `hook read` commands
diff --git a/.changeset/new-timers-applaud.md b/.changeset/new-timers-applaud.md
new file mode 100644
index 0000000000..af8820eac1
--- /dev/null
+++ b/.changeset/new-timers-applaud.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/cli': minor
+---
+
+Add chain displayName prompt with default
diff --git a/.changeset/sharp-geckos-wash.md b/.changeset/sharp-geckos-wash.md
new file mode 100644
index 0000000000..88ee03c287
--- /dev/null
+++ b/.changeset/sharp-geckos-wash.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/cli': minor
+---
+
+Fix createDefaultWarpIsmConfig to default to trusted relayer and fallback routing without prompts
diff --git a/.changeset/shiny-cups-help.md b/.changeset/shiny-cups-help.md
new file mode 100644
index 0000000000..dad4ee692c
--- /dev/null
+++ b/.changeset/shiny-cups-help.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/sdk': minor
+---
+
+Add logic to set smart provider log level to disable provider logs during Warp TokenType derive
diff --git a/.changeset/shy-countries-heal.md b/.changeset/shy-countries-heal.md
new file mode 100644
index 0000000000..295eba47fe
--- /dev/null
+++ b/.changeset/shy-countries-heal.md
@@ -0,0 +1,6 @@
+---
+'@hyperlane-xyz/sdk': minor
+---
+
+- Enables creation of new Hooks through the `EvmHookModule`.
+- Introduces an `EvmModuleDeployer` to perform the barebones tasks of deploying contracts/proxies.
diff --git a/.changeset/slimy-toys-argue.md b/.changeset/slimy-toys-argue.md
new file mode 100644
index 0000000000..7b9f760bbc
--- /dev/null
+++ b/.changeset/slimy-toys-argue.md
@@ -0,0 +1,6 @@
+---
+'@hyperlane-xyz/cli': minor
+'@hyperlane-xyz/sdk': minor
+---
+
+Implement hyperlane warp deploy
diff --git a/.changeset/slimy-ways-hide.md b/.changeset/slimy-ways-hide.md
new file mode 100644
index 0000000000..94082b9e9a
--- /dev/null
+++ b/.changeset/slimy-ways-hide.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/cli': major
+---
+
+Release CLI v4.0.0.
diff --git a/.changeset/sour-squids-buy.md b/.changeset/sour-squids-buy.md
new file mode 100644
index 0000000000..6bd0ae43c5
--- /dev/null
+++ b/.changeset/sour-squids-buy.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/cli': minor
+---
+
+Updates ci-test.sh to ci-advanced-test.sh.
diff --git a/.changeset/wise-cobras-juggle.md b/.changeset/wise-cobras-juggle.md
new file mode 100644
index 0000000000..4c7bb1a4a7
--- /dev/null
+++ b/.changeset/wise-cobras-juggle.md
@@ -0,0 +1,5 @@
+---
+'@hyperlane-xyz/cli': minor
+---
+
+Add stdout.rows to pagesize calculation with DEFAULT_PAGE_SIZE
diff --git a/.eslintrc b/.eslintrc
index 0fffcede50..23ad387d6b 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -23,6 +23,7 @@
"no-extra-boolean-cast": ["error"],
"no-ex-assign": ["error"],
"no-constant-condition": ["off"],
+ "no-return-await": ["error"],
"guard-for-in": ["error"],
"@typescript-eslint/ban-ts-comment": ["off"],
"@typescript-eslint/explicit-module-boundary-types": ["off"],
diff --git a/.github/workflows/test-skipped.yml b/.github/workflows/test-skipped.yml
index 7b4091321f..6b3f3b081a 100644
--- a/.github/workflows/test-skipped.yml
+++ b/.github/workflows/test-skipped.yml
@@ -70,7 +70,7 @@ jobs:
- name: Instant pass
run: echo "e2e job passed"
- cli-e2e:
+ cli-advanced-e2e:
runs-on: ubuntu-latest
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') || github.event_name == 'merge_group'
strategy:
@@ -81,7 +81,7 @@ jobs:
- test-type: pi_with_core_chain
steps:
- name: Instant pass
- run: echo "cli-e2e job passed"
+ run: echo "cli-advanced-e2e job passed"
env-test:
runs-on: ubuntu-latest
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8584a60f2a..ebd05df12b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,11 +1,12 @@
name: test
on:
- # Triggers the workflow on push or pull request against main
+ # Triggers the workflow on pushes to main branch
push:
branches: [main]
paths-ignore:
- '*.md'
+ # Triggers on pull requests ignoring md files
pull_request:
branches:
- '*' # run against all branches
@@ -187,7 +188,7 @@ jobs:
e2e-matrix:
runs-on: larger-runner
- if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') || github.event_name == 'merge_group'
+ if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main' || github.base_ref == 'cli-2.0') || github.event_name == 'merge_group'
needs: [yarn-build]
strategy:
matrix:
@@ -281,7 +282,7 @@ jobs:
prebuild-cli-e2e:
runs-on: larger-runner
- if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') || github.event_name == 'merge_group'
+ if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main' || github.base_ref == 'cli-2.0') || github.event_name == 'merge_group'
steps:
- uses: actions/checkout@v4
with:
@@ -328,9 +329,9 @@ jobs:
env:
RUST_BACKTRACE: 'full'
- cli-e2e:
+ cli-advanced-e2e:
runs-on: larger-runner
- if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') || github.event_name == 'merge_group'
+ if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main' || github.base_ref == 'cli-2.0') || github.event_name == 'merge_group'
needs: [yarn-build, prebuild-cli-e2e]
strategy:
matrix:
@@ -399,7 +400,7 @@ jobs:
uses: ./.github/actions/checkout-registry
- name: cli e2e tests
- run: ./typescript/cli/ci-test.sh ${{ matrix.test-type }}
+ run: ./typescript/cli/ci-advanced-test.sh ${{ matrix.test-type }}
env-test:
runs-on: ubuntu-latest
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index b7e03e6758..2466daf872 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -11,7 +11,7 @@ This CoC applies to all members of the Hyperlane Network's community including,
1. Never harass or bully anyone. Not verbally, not physically, not sexually. Harassment will not be tolerated.
2. Never discriminate on the basis of personal characteristics or group membership.
3. Treat your fellow contributors with respect, fairness, and professionalism, especially in situations of high pressure.
-4. Seek, offer, and accept objective criticism of yours and others work, strive to acknowledge the contributions of others.
+4. Seek, offer, and accept objective criticism of yours and others work, strive to acknowledge the contributions of others.
5. Be transparent and honest about your qualifications and any potential conflicts of interest. Transparency is a key tenet of the Hyperlane project and we expect it from all contributors.
6. Bring an open and curious mind, the Hyperlane project is designed to enable developers to express their curiosity, experiment, and build things we couldn't have imagined ourselves.
7. Stay on track - Do your best to avoid off-topic discussion and make sure you are posting to the correct channel and repositories. Distractions are costly and it is far too easy for work to go off track.
diff --git a/typescript/cli/ci-test.sh b/typescript/cli/ci-advanced-test.sh
similarity index 91%
rename from typescript/cli/ci-test.sh
rename to typescript/cli/ci-advanced-test.sh
index 7fbeea04b7..c794632cbb 100755
--- a/typescript/cli/ci-test.sh
+++ b/typescript/cli/ci-advanced-test.sh
@@ -15,7 +15,7 @@ _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_PRESET_HOOK | $TEST_TYPE_CONFIGURED_HOOK | $TEST_TYPE_PI_CORE>"
+ echo "Usage: ci-advanced-test.sh <$TEST_TYPE_PRESET_HOOK | $TEST_TYPE_CONFIGURED_HOOK | $TEST_TYPE_PI_CORE>"
exit 1
fi
@@ -34,11 +34,11 @@ _main() {
run_hyperlane_deploy_warp;
run_hyperlane_send_message;
- cd ./rust;
+ # cd ./rust;
- run_validator;
- run_relayer;
- run_hyperlane_status;
+ # run_validator;
+ # run_relayer;
+ # run_hyperlane_status;
kill_anvil;
@@ -75,6 +75,7 @@ prepare_environment_vars() {
}
prepare_anvil() {
+
CHAIN1_PORT=8545
CHAIN2_PORT=8555
@@ -142,12 +143,11 @@ run_hyperlane_deploy_core_dry_run() {
update_deployer_balance;
echo -e "\nDry-running contract deployments to Alfajores"
- yarn workspace @hyperlane-xyz/cli run hyperlane deploy core \
+ yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \
--dry-run alfajores \
--registry ${TEST_CONFIGS_PATH}/dry-run \
--overrides " " \
- $(if [ "$HOOK_FLAG" == "true" ]; then echo "--hook ${EXAMPLES_PATH}/hooks.yaml"; fi) \
- --ism ${TEST_CONFIGS_PATH}/dry-run/ism.yaml \
+ --config ${EXAMPLES_PATH}/core-config.yaml \
--from-address 0xfaD1C94469700833717Fa8a3017278BC1cA8031C \
--yes
@@ -162,7 +162,7 @@ run_hyperlane_deploy_warp_dry_run() {
update_deployer_balance;
echo -e "\nDry-running warp route deployments to Alfajores"
- yarn workspace @hyperlane-xyz/cli run hyperlane deploy warp \
+ yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \
--dry-run alfajores \
--overrides ${TEST_CONFIGS_PATH}/dry-run \
--config ${TEST_CONFIGS_PATH}/dry-run/warp-route-deployment.yaml \
@@ -175,14 +175,21 @@ run_hyperlane_deploy_warp_dry_run() {
run_hyperlane_deploy_core() {
update_deployer_balance;
- echo -e "\nDeploying contracts to ${CHAIN1} and ${CHAIN2}"
- yarn workspace @hyperlane-xyz/cli run hyperlane deploy core \
+ echo -e "\nDeploying contracts to ${CHAIN1}"
+ yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \
+ --registry $REGISTRY_PATH \
+ --overrides " " \
+ --config ${EXAMPLES_PATH}/core-config.yaml \
+ --chain $CHAIN1 \
+ --key $ANVIL_KEY \
+ --yes
+
+ echo -e "\nDeploying contracts to ${CHAIN2}"
+ yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \
--registry $REGISTRY_PATH \
--overrides " " \
- --targets ${CHAIN1},${CHAIN2} \
- $(if [ "$HOOK_FLAG" == "true" ]; then echo "--hook ${EXAMPLES_PATH}/hooks.yaml"; fi) \
- --ism $CORE_ISM_PATH \
- --agent /tmp/agent-config.json \
+ --config ${EXAMPLES_PATH}/core-config.yaml \
+ --chain $CHAIN2 \
--key $ANVIL_KEY \
--yes
@@ -193,7 +200,7 @@ run_hyperlane_deploy_warp() {
update_deployer_balance;
echo -e "\nDeploying hypNative warp route"
- yarn workspace @hyperlane-xyz/cli run hyperlane deploy warp \
+ yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \
--registry $REGISTRY_PATH \
--overrides " " \
--config $WARP_DEPLOY_CONFIG_PATH \
@@ -206,7 +213,7 @@ run_hyperlane_deploy_warp() {
/tmp/warp-collateral-deployment.json \
echo "Deploying hypCollateral warp route"
- yarn workspace @hyperlane-xyz/cli run hyperlane deploy warp \
+ yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \
--registry $REGISTRY_PATH \
--overrides " " \
--config /tmp/warp-collateral-deployment.json \
@@ -238,7 +245,7 @@ run_hyperlane_send_message() {
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 \
+ yarn workspace @hyperlane-xyz/cli run hyperlane warp send \
--registry $REGISTRY_PATH \
--overrides " " \
--origin ${CHAIN1} \
diff --git a/typescript/cli/cli.ts b/typescript/cli/cli.ts
index a8b9127f3d..2328c25311 100644
--- a/typescript/cli/cli.ts
+++ b/typescript/cli/cli.ts
@@ -6,8 +6,8 @@ import type { LogFormat, LogLevel } from '@hyperlane-xyz/utils';
import './env.js';
import { avsCommand } from './src/commands/avs.js';
-import { chainsCommand } from './src/commands/chains.js';
import { configCommand } from './src/commands/config.js';
+import { coreCommand } from './src/commands/core.js';
import { deployCommand } from './src/commands/deploy.js';
import { hookCommand } from './src/commands/hook.js';
import { ismCommand } from './src/commands/ism.js';
@@ -19,9 +19,11 @@ import {
registryUriCommandOption,
skipConfirmationOption,
} from './src/commands/options.js';
+import { registryCommand } from './src/commands/registry.js';
import { sendCommand } from './src/commands/send.js';
import { statusCommand } from './src/commands/status.js';
import { validatorCommand } from './src/commands/validator.js';
+import { warpCommand } from './src/commands/warp.js';
import { contextMiddleware } from './src/context/context.js';
import { configureLogger, errorRed } from './src/logger.js';
import { checkVersion } from './src/utils/version-check.js';
@@ -51,14 +53,16 @@ try {
contextMiddleware,
])
.command(avsCommand)
- .command(chainsCommand)
.command(configCommand)
+ .command(coreCommand)
.command(deployCommand)
.command(hookCommand)
.command(ismCommand)
+ .command(registryCommand)
.command(sendCommand)
.command(statusCommand)
.command(validatorCommand)
+ .command(warpCommand)
.version(VERSION)
.demandCommand()
.strict()
diff --git a/typescript/cli/examples/core-config.yaml b/typescript/cli/examples/core-config.yaml
new file mode 100644
index 0000000000..cfc548543b
--- /dev/null
+++ b/typescript/cli/examples/core-config.yaml
@@ -0,0 +1,19 @@
+# A config to define the core contract deployments
+owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
+defaultIsm:
+ type: 'testIsm'
+ threshold: 1 # Number: Signatures required to approve a message
+ validators: # Array: List of validator addresses
+ - '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
+defaultHook:
+ type: protocolFee
+ maxProtocolFee: '1000000000000000000' # in wei (string)
+ protocolFee: '200000000000000' # in wei (string)
+ beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
+ owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
+requiredHook:
+ type: protocolFee
+ maxProtocolFee: '1000000000000000000' # in wei (string)
+ protocolFee: '200000000000000' # in wei (string)
+ beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
+ owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
diff --git a/typescript/cli/examples/hooks.yaml b/typescript/cli/examples/hooks.yaml
index 9fc19433ab..d33bf09346 100644
--- a/typescript/cli/examples/hooks.yaml
+++ b/typescript/cli/examples/hooks.yaml
@@ -39,8 +39,10 @@ anvil1:
oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
overhead:
anvil2: 50000 # gas amount (number)
- gasOracleType:
- anvil2: StorageGasOracle
+ oracleConfig:
+ anvil2:
+ gasPrice: '100'
+ tokenExchangeRate: '100'
anvil2:
required:
type: protocolFee
@@ -62,5 +64,7 @@ anvil2:
oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
overhead:
anvil1: 50000
- gasOracleType:
- anvil1: StorageGasOracle
+ oracleConfig:
+ anvil1:
+ gasPrice: '100'
+ tokenExchangeRate: '100'
diff --git a/typescript/cli/package.json b/typescript/cli/package.json
index 1cf7e607cb..f8c07586ae 100644
--- a/typescript/cli/package.json
+++ b/typescript/cli/package.json
@@ -18,7 +18,8 @@
"tsx": "^4.7.1",
"yaml": "^2.4.1",
"yargs": "^17.7.2",
- "zod": "^3.21.2"
+ "zod": "^3.21.2",
+ "zod-validation-error": "^3.3.0"
},
"devDependencies": {
"@ethersproject/abi": "*",
diff --git a/typescript/cli/src/avs/stakeRegistry.ts b/typescript/cli/src/avs/stakeRegistry.ts
index 705d52da11..a9c50918da 100644
--- a/typescript/cli/src/avs/stakeRegistry.ts
+++ b/typescript/cli/src/avs/stakeRegistry.ts
@@ -119,7 +119,7 @@ export async function readOperatorFromEncryptedJson(
message: 'Enter the password for the operator key file: ',
});
- return await Wallet.fromEncryptedJson(encryptedJson, keyFilePassword);
+ return Wallet.fromEncryptedJson(encryptedJson, keyFilePassword);
}
async function getOperatorSignature(
diff --git a/typescript/cli/src/commands/config.ts b/typescript/cli/src/commands/config.ts
index bf30e78ee8..7b145ca44d 100644
--- a/typescript/cli/src/commands/config.ts
+++ b/typescript/cli/src/commands/config.ts
@@ -1,20 +1,13 @@
import { CommandModule } from 'yargs';
-import { createChainConfig, readChainConfigs } from '../config/chain.js';
-import { createHooksConfigMap } from '../config/hooks.js';
-import { createIsmConfigMap, readIsmConfig } from '../config/ism.js';
-import {
- createMultisigConfig,
- readMultisigConfig,
-} from '../config/multisig.js';
-import {
- createWarpRouteDeployConfig,
- readWarpRouteDeployConfig,
-} from '../config/warp.js';
+import { readChainConfigs } from '../config/chain.js';
+import { readIsmConfig } from '../config/ism.js';
+import { readMultisigConfig } from '../config/multisig.js';
+import { readWarpRouteDeployConfig } from '../config/warp.js';
import { CommandModuleWithContext } from '../context/types.js';
import { log, logGreen } from '../logger.js';
-import { inputFileCommandOption, outputFileCommandOption } from './options.js';
+import { inputFileCommandOption } from './options.js';
/**
* Parent command
@@ -23,90 +16,10 @@ export const configCommand: CommandModule = {
command: 'config',
describe: 'Create or validate Hyperlane configs',
builder: (yargs) =>
- yargs
- .command(createCommand)
- .command(validateCommand)
- .version(false)
- .demandCommand(),
+ yargs.command(validateCommand).version(false).demandCommand(),
handler: () => log('Command required'),
};
-/**
- * Create commands
- */
-const createCommand: CommandModule = {
- command: 'create',
- describe: 'Create a new Hyperlane config',
- builder: (yargs) =>
- yargs
- .command(createChainConfigCommand)
- .command(createIsmConfigCommand)
- .command(createHookConfigCommand)
- .command(createWarpRouteDeployConfigCommand)
- .version(false)
- .demandCommand(),
- handler: () => log('Command required'),
-};
-
-const createChainConfigCommand: CommandModuleWithContext<{}> = {
- command: 'chain',
- describe: 'Create a new, minimal Hyperlane chain config (aka chain metadata)',
- handler: async ({ context }) => {
- await createChainConfig({ context });
- process.exit(0);
- },
-};
-
-const createIsmConfigCommand: CommandModuleWithContext<{
- out: string;
- advanced: boolean;
-}> = {
- command: 'ism',
- describe: 'Create a basic or advanced ISM config for a validator set',
- builder: {
- out: outputFileCommandOption('./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({ context, outPath: out });
- }
- process.exit(0);
- },
-};
-
-const createHookConfigCommand: CommandModuleWithContext<{ out: string }> = {
- command: 'hooks',
- describe: 'Create a new hooks config (required & default)',
- builder: {
- out: outputFileCommandOption('./configs/hooks.yaml'),
- },
- handler: async ({ context, out }) => {
- await createHooksConfigMap({ context, outPath: out });
- process.exit(0);
- },
-};
-
-const createWarpRouteDeployConfigCommand: CommandModuleWithContext<{
- out: string;
-}> = {
- command: 'warp',
- describe: 'Create a new Warp Route deployment config',
- builder: {
- out: outputFileCommandOption('./configs/warp-route-deployment.yaml'),
- },
- handler: async ({ context, out }) => {
- await createWarpRouteDeployConfig({ context, outPath: out });
- process.exit(0);
- },
-};
-
/**
* Validate commands
*/
diff --git a/typescript/cli/src/commands/core.ts b/typescript/cli/src/commands/core.ts
new file mode 100644
index 0000000000..54a5786f60
--- /dev/null
+++ b/typescript/cli/src/commands/core.ts
@@ -0,0 +1,155 @@
+import { stringify as yamlStringify } from 'yaml';
+import { CommandModule } from 'yargs';
+
+import { EvmCoreReader } from '@hyperlane-xyz/sdk';
+
+import { createCoreDeployConfig } from '../config/core.js';
+import {
+ CommandModuleWithContext,
+ CommandModuleWithWriteContext,
+} from '../context/types.js';
+import { runCoreDeploy } from '../deploy/core.js';
+import { evaluateIfDryRunFailure } from '../deploy/dry-run.js';
+import { log, logGray, logGreen } from '../logger.js';
+import {
+ indentYamlOrJson,
+ readYamlOrJson,
+ writeYamlOrJson,
+} from '../utils/files.js';
+
+import {
+ chainCommandOption,
+ dryRunCommandOption,
+ fromAddressCommandOption,
+ outputFileCommandOption,
+} from './options.js';
+
+/**
+ * Parent command
+ */
+export const coreCommand: CommandModule = {
+ command: 'core',
+ describe: 'Manage core Hyperlane contracts & configs',
+ builder: (yargs) =>
+ yargs
+ .command(deploy)
+ .command(init)
+ .command(read)
+ .version(false)
+ .demandCommand(),
+ handler: () => log('Command required'),
+};
+
+/**
+ * Generates a command module for deploying Hyperlane contracts, given a command
+ *
+ * @param commandName - the deploy command key used to look up the deployFunction
+ * @returns A command module used to deploy Hyperlane contracts.
+ */
+export const deploy: CommandModuleWithWriteContext<{
+ chain: string;
+ config: string;
+ dryRun: string;
+ fromAddress: string;
+}> = {
+ command: 'deploy',
+ describe: 'Deploy Hyperlane contracts',
+ builder: {
+ chain: chainCommandOption,
+ config: outputFileCommandOption(
+ './configs/core-config.yaml',
+ false,
+ 'The path to a JSON or YAML file with a core deployment config.',
+ ),
+ 'dry-run': dryRunCommandOption,
+ 'from-address': fromAddressCommandOption,
+ },
+ handler: async ({ context, chain, config: configFilePath, dryRun }) => {
+ logGray(`Hyperlane permissionless deployment${dryRun ? ' dry-run' : ''}`);
+ logGray(`------------------------------------------------`);
+
+ try {
+ await runCoreDeploy({
+ context,
+ chain,
+ config: readYamlOrJson(configFilePath),
+ });
+ } catch (error: any) {
+ evaluateIfDryRunFailure(error, dryRun);
+ throw error;
+ }
+ process.exit(0);
+ },
+};
+
+export const init: CommandModuleWithContext<{
+ advanced: boolean;
+ config: string;
+}> = {
+ command: 'init',
+ describe: 'Create a core configuration, including ISMs and hooks.',
+ builder: {
+ advanced: {
+ type: 'boolean',
+ describe: 'Create an advanced ISM & hook configuration',
+ default: false,
+ },
+ config: outputFileCommandOption(
+ './configs/core-config.yaml',
+ false,
+ 'The path to output a Core Config JSON or YAML file.',
+ ),
+ },
+ handler: async ({ context, advanced, config: configFilePath }) => {
+ logGray('Hyperlane Core Configure');
+ logGray('------------------------');
+
+ await createCoreDeployConfig({
+ context,
+ configFilePath,
+ advanced,
+ });
+
+ process.exit(0);
+ },
+};
+
+export const read: CommandModuleWithContext<{
+ chain: string;
+ mailbox: string;
+ config: string;
+}> = {
+ command: 'read',
+ describe: 'Reads onchain Core configuration for a given mailbox address',
+ builder: {
+ chain: {
+ ...chainCommandOption,
+ demandOption: true,
+ },
+ mailbox: {
+ type: 'string',
+ description: 'Mailbox address used to derive the core config',
+ demandOption: true,
+ },
+ config: outputFileCommandOption(
+ './configs/core-config.yaml',
+ false,
+ 'The path to output a Core Config JSON or YAML file.',
+ ),
+ },
+ handler: async ({ context, chain, mailbox, config: configFilePath }) => {
+ logGray('Hyperlane Core Read');
+ logGray('-------------------');
+
+ const evmCoreReader = new EvmCoreReader(context.multiProvider, chain);
+ const coreConfig = await evmCoreReader.deriveCoreConfig(mailbox);
+
+ writeYamlOrJson(configFilePath, coreConfig, 'yaml');
+ logGreen(
+ `✅ Warp route config written successfully to ${configFilePath}:\n`,
+ );
+ log(indentYamlOrJson(yamlStringify(coreConfig, null, 2), 4));
+
+ process.exit(0);
+ },
+};
diff --git a/typescript/cli/src/commands/deploy.ts b/typescript/cli/src/commands/deploy.ts
index 7de7fb182a..aa73f18992 100644
--- a/typescript/cli/src/commands/deploy.ts
+++ b/typescript/cli/src/commands/deploy.ts
@@ -1,25 +1,13 @@
import { CommandModule } from 'yargs';
-import {
- CommandModuleWithContext,
- CommandModuleWithWriteContext,
-} from '../context/types.js';
+import { CommandModuleWithContext } from '../context/types.js';
import { runKurtosisAgentDeploy } from '../deploy/agent.js';
-import { runCoreDeploy } from '../deploy/core.js';
-import { evaluateIfDryRunFailure } from '../deploy/dry-run.js';
-import { runWarpRouteDeploy } from '../deploy/warp.js';
import { log, logGray } from '../logger.js';
import {
agentConfigCommandOption,
agentTargetsCommandOption,
- coreTargetsCommandOption,
- dryRunCommandOption,
- fromAddressCommandOption,
- hookCommandOption,
- ismCommandOption,
originCommandOption,
- warpDeploymentConfigCommandOption,
} from './options.js';
/**
@@ -29,12 +17,7 @@ export const deployCommand: CommandModule = {
command: 'deploy',
describe: 'Permissionlessly deploy a Hyperlane contracts or extensions',
builder: (yargs) =>
- yargs
- .command(coreCommand)
- .command(warpCommand)
- .command(agentCommand)
- .version(false)
- .demandCommand(),
+ yargs.command(agentCommand).version(false).demandCommand(),
handler: () => log('Command required'),
};
@@ -65,79 +48,3 @@ const agentCommand: CommandModuleWithContext<{
process.exit(0);
},
};
-
-/**
- * Core command
- */
-const coreCommand: CommandModuleWithWriteContext<{
- targets: string;
- ism?: string;
- hook?: string;
- 'dry-run': string;
- 'from-address': string;
- agent: string;
-}> = {
- command: 'core',
- describe: 'Deploy core Hyperlane contracts',
- builder: {
- targets: coreTargetsCommandOption,
- ism: ismCommandOption,
- hook: hookCommandOption,
- agent: agentConfigCommandOption(false, './configs/agent.json'),
- 'dry-run': dryRunCommandOption,
- 'from-address': fromAddressCommandOption,
- },
- handler: async ({ context, targets, ism, hook, agent, dryRun }) => {
- logGray(
- `Hyperlane permissionless core deployment${dryRun ? ' dry-run' : ''}`,
- );
- logGray(`------------------------------------------------`);
-
- try {
- const chains = targets?.split(',').map((r: string) => r.trim());
- await runCoreDeploy({
- context,
- chains,
- ismConfigPath: ism,
- hookConfigPath: hook,
- agentOutPath: agent,
- });
- } catch (error: any) {
- evaluateIfDryRunFailure(error, dryRun);
- throw error;
- }
- process.exit(0);
- },
-};
-
-/**
- * Warp command
- */
-const warpCommand: CommandModuleWithWriteContext<{
- config: string;
- 'dry-run': string;
- 'from-address': string;
-}> = {
- command: 'warp',
- describe: 'Deploy Warp Route contracts',
- builder: {
- config: warpDeploymentConfigCommandOption,
- 'dry-run': dryRunCommandOption,
- 'from-address': fromAddressCommandOption,
- },
- handler: async ({ context, config, dryRun }) => {
- logGray(`Hyperlane warp route deployment${dryRun ? ' dry-run' : ''}`);
- logGray('------------------------------------------------');
-
- try {
- await runWarpRouteDeploy({
- context,
- warpRouteDeploymentConfigPath: config,
- });
- } catch (error: any) {
- evaluateIfDryRunFailure(error, dryRun);
- throw error;
- }
- process.exit(0);
- },
-};
diff --git a/typescript/cli/src/commands/hook.ts b/typescript/cli/src/commands/hook.ts
index 34410dcbd9..986b0d2be4 100644
--- a/typescript/cli/src/commands/hook.ts
+++ b/typescript/cli/src/commands/hook.ts
@@ -2,7 +2,7 @@ import { CommandModule } from 'yargs';
import { CommandModuleWithContext } from '../context/types.js';
import { readHookConfig } from '../hook/read.js';
-import { log } from '../logger.js';
+import { log, logGray } from '../logger.js';
import {
addressCommandOption,
@@ -41,6 +41,8 @@ export const read: CommandModuleWithContext<{
out: outputFileCommandOption(),
},
handler: async (args) => {
+ logGray('Hyperlane Hook Read');
+ logGray('------------------');
await readHookConfig(args);
process.exit(0);
},
diff --git a/typescript/cli/src/commands/ism.ts b/typescript/cli/src/commands/ism.ts
index 831ea7207c..c4363ec826 100644
--- a/typescript/cli/src/commands/ism.ts
+++ b/typescript/cli/src/commands/ism.ts
@@ -2,7 +2,7 @@ import { CommandModule } from 'yargs';
import { CommandModuleWithContext } from '../context/types.js';
import { readIsmConfig } from '../ism/read.js';
-import { log } from '../logger.js';
+import { log, logGray } from '../logger.js';
import {
addressCommandOption,
@@ -46,6 +46,8 @@ export const read: CommandModuleWithContext<{
out: outputFileCommandOption(),
},
handler: async (argv) => {
+ logGray('Hyperlane ISM Read');
+ logGray('------------------');
await readIsmConfig(argv);
process.exit(0);
},
diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts
index 033fb7ba82..f91df414c0 100644
--- a/typescript/cli/src/commands/options.ts
+++ b/typescript/cli/src/commands/options.ts
@@ -96,8 +96,6 @@ export const warpCoreConfigCommandOption: Options = {
type: 'string',
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 = (
@@ -111,11 +109,23 @@ export const agentConfigCommandOption = (
default: defaultPath,
});
-export const outputFileCommandOption = (defaultPath?: string): Options => ({
+export const chainTargetsCommandOption: Options = {
type: 'string',
- description: 'Output file path',
+ description: 'Comma-separated list of chain names',
+ alias: 'c',
+ demandOption: true,
+};
+
+export const outputFileCommandOption = (
+ defaultPath?: string,
+ demandOption = false,
+ description = 'Output file path',
+): Options => ({
+ type: 'string',
+ description,
default: defaultPath,
alias: 'o',
+ demandOption,
});
export const inputFileCommandOption: Options = {
@@ -143,6 +153,17 @@ export const chainCommandOption: Options = {
description: 'The specific chain to perform operations with.',
};
+export const symbolCommandOption: Options = {
+ type: 'string',
+ description: 'Token symbol (e.g. ETH, USDC)',
+};
+
+export const validatorCommandOption: Options = {
+ type: 'string',
+ description: 'Comma separated list of validator addresses',
+ demandOption: true,
+};
+
export const addressCommandOption = (
description: string,
demandOption = false,
diff --git a/typescript/cli/src/commands/chains.ts b/typescript/cli/src/commands/registry.ts
similarity index 53%
rename from typescript/cli/src/commands/chains.ts
rename to typescript/cli/src/commands/registry.ts
index 9d0e970fa2..9334064d55 100644
--- a/typescript/cli/src/commands/chains.ts
+++ b/typescript/cli/src/commands/registry.ts
@@ -1,21 +1,28 @@
import { CommandModule } from 'yargs';
-import { CommandModuleWithContext } from '../context/types.js';
-import { log, logBlue, logGray, logTable } from '../logger.js';
+import { createAgentConfig } from '../config/agent.js';
+import { createChainConfig } from '../config/chain.js';
+import { CommandContext, CommandModuleWithContext } from '../context/types.js';
+import { log, logBlue, logGray, logRed, logTable } from '../logger.js';
-const ChainTypes = ['mainnet', 'testnet'];
-type ChainType = (typeof ChainTypes)[number];
+import {
+ chainTargetsCommandOption,
+ outputFileCommandOption,
+} from './options.js';
+import { ChainType, ChainTypes } from './types.js';
/**
* Parent command
*/
-export const chainsCommand: CommandModule = {
- command: 'chains',
- describe: 'View information about Hyperlane chains in a registry',
+export const registryCommand: CommandModule = {
+ command: 'registry',
+ describe: 'Manage Hyperlane chains in a registry',
builder: (yargs) =>
yargs
- .command(listCommand)
.command(addressesCommand)
+ .command(createAgentConfigCommand)
+ .command(initCommand)
+ .command(listCommand)
.version(false)
.demandCommand(),
handler: () => log('Command required'),
@@ -88,3 +95,59 @@ const addressesCommand: CommandModuleWithContext<{ name: string }> = {
}
},
};
+
+/**
+ * agent-config command
+ */
+const createAgentConfigCommand: CommandModuleWithContext<{
+ chains: string;
+ out: string;
+}> = {
+ command: 'agent-config',
+ describe: 'Create a new agent config',
+
+ builder: {
+ chains: chainTargetsCommandOption,
+ out: outputFileCommandOption(
+ './configs/agent-config.json',
+ false,
+ 'The path to output an agent config JSON file.',
+ ),
+ },
+ handler: async ({
+ context,
+ chains,
+ out,
+ }: {
+ context: CommandContext;
+ chains: string;
+ out: string;
+ }) => {
+ const { multiProvider } = context;
+
+ const chainNames = chains.split(',');
+ const invalidChainNames = chainNames.filter(
+ (chainName) => !multiProvider.hasChain(chainName),
+ );
+ if (invalidChainNames.length > 0) {
+ logRed(
+ `Invalid chain names: ${invalidChainNames
+ .join(', ')
+ .replace(/, $/, '')}`,
+ );
+ process.exit(1);
+ }
+
+ await createAgentConfig({ context, chains: chainNames, out });
+ process.exit(0);
+ },
+};
+
+const initCommand: CommandModuleWithContext<{}> = {
+ command: 'init',
+ describe: 'Create a new, minimal Hyperlane chain config (aka chain metadata)',
+ handler: async ({ context }) => {
+ await createChainConfig({ context });
+ process.exit(0);
+ },
+};
diff --git a/typescript/cli/src/commands/send.ts b/typescript/cli/src/commands/send.ts
index e77a0998c9..1167b3b559 100644
--- a/typescript/cli/src/commands/send.ts
+++ b/typescript/cli/src/commands/send.ts
@@ -4,22 +4,15 @@ 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 { warpCoreConfigCommandOption } from './options.js';
/**
* Parent command
*/
export const sendCommand: CommandModule = {
command: 'send',
- describe: 'Send a test message or transfer',
+ describe: 'Send a test message',
builder: (yargs) =>
- yargs
- .command(messageCommand)
- .command(transferCommand)
- .version(false)
- .demandCommand(),
+ yargs.command(messageCommand).version(false).demandCommand(),
handler: () => log('Command required'),
};
@@ -94,55 +87,3 @@ const messageCommand: CommandModuleWithWriteContext<
process.exit(0);
},
};
-
-/**
- * Transfer command
- */
-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: {
- ...messageOptions,
- warp: warpCoreConfigCommandOption,
- 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({
- context,
- warpConfigPath: warp,
- origin,
- destination,
- wei,
- recipient,
- timeoutSec: timeout,
- skipWaitForDelivery: quick,
- selfRelay: relay,
- });
- process.exit(0);
- },
-};
diff --git a/typescript/cli/src/commands/types.ts b/typescript/cli/src/commands/types.ts
new file mode 100644
index 0000000000..bc017069aa
--- /dev/null
+++ b/typescript/cli/src/commands/types.ts
@@ -0,0 +1,2 @@
+export const ChainTypes = ['mainnet', 'testnet'];
+export type ChainType = (typeof ChainTypes)[number];
diff --git a/typescript/cli/src/commands/validator.ts b/typescript/cli/src/commands/validator.ts
index 973c0cd25e..a065e7acbf 100644
--- a/typescript/cli/src/commands/validator.ts
+++ b/typescript/cli/src/commands/validator.ts
@@ -1,8 +1,16 @@
import { CommandModule } from 'yargs';
+import {
+ Address,
+ ProtocolType,
+ isValidAddressEvm,
+ normalizeAddressEvm,
+} from '@hyperlane-xyz/utils';
+
import { CommandModuleWithContext } from '../context/types.js';
-import { log } from '../logger.js';
+import { errorRed, log } from '../logger.js';
import { getValidatorAddress } from '../validator/address.js';
+import { checkValidatorSetup } from '../validator/preFlightCheck.js';
import {
awsAccessKeyCommandOption,
@@ -10,13 +18,20 @@ import {
awsKeyIdCommandOption,
awsRegionCommandOption,
awsSecretKeyCommandOption,
+ chainCommandOption,
+ demandOption,
+ validatorCommandOption,
} from './options.js';
// Parent command to help configure and set up Hyperlane validators
export const validatorCommand: CommandModule = {
command: 'validator',
describe: 'Configure and manage Hyperlane validators',
- builder: (yargs) => yargs.command(addressCommand).demandCommand(),
+ builder: (yargs) =>
+ yargs
+ .command(addressCommand)
+ .command(preFlightCheckCommand)
+ .demandCommand(),
handler: () => log('Command required'),
};
@@ -49,3 +64,58 @@ const addressCommand: CommandModuleWithContext<{
process.exit(0);
},
};
+
+const preFlightCheckCommand: CommandModuleWithContext<{
+ chain: string;
+ validators: string;
+}> = {
+ command: 'check',
+ describe: 'Check the validator has announced correctly for a given chain',
+ builder: {
+ chain: demandOption(chainCommandOption),
+ validators: validatorCommandOption,
+ },
+ handler: async ({ context, chain, validators }) => {
+ const { multiProvider } = context;
+
+ // validate chain
+ if (!multiProvider.hasChain(chain)) {
+ errorRed(
+ `❌ No metadata found for ${chain}. Ensure it is included in your configured registry.`,
+ );
+ process.exit(1);
+ }
+
+ const chainMetadata = multiProvider.getChainMetadata(chain);
+
+ if (chainMetadata.protocol !== ProtocolType.Ethereum) {
+ errorRed(
+ `\n❌ Validator pre flight check only supports EVM chains. Exiting.`,
+ );
+ process.exit(1);
+ }
+
+ // validate validators addresses
+ const validatorList = validators.split(',');
+ const invalidAddresses: Set
= new Set();
+ const validAddresses: Set = new Set();
+
+ for (const address of validatorList) {
+ if (isValidAddressEvm(address)) {
+ validAddresses.add(normalizeAddressEvm(address));
+ } else {
+ invalidAddresses.add(address);
+ }
+ }
+
+ if (invalidAddresses.size > 0) {
+ errorRed(
+ `❌ Invalid addresses: ${Array.from(invalidAddresses).join(', ')}`,
+ );
+ process.exit(1);
+ }
+
+ await checkValidatorSetup(context, chain, validAddresses);
+ process.exit(0);
+ },
+};
diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts
new file mode 100644
index 0000000000..7a1367779e
--- /dev/null
+++ b/typescript/cli/src/commands/warp.ts
@@ -0,0 +1,290 @@
+import { ethers } from 'ethers';
+import { stringify as yamlStringify } from 'yaml';
+import { CommandModule } from 'yargs';
+
+import {
+ HypXERC20Lockbox__factory,
+ HypXERC20__factory,
+ IXERC20__factory,
+} from '@hyperlane-xyz/core';
+import {
+ ChainMap,
+ EvmERC20WarpRouteReader,
+ TokenStandard,
+ WarpCoreConfig,
+} from '@hyperlane-xyz/sdk';
+import { objMap, promiseObjAll } from '@hyperlane-xyz/utils';
+
+import {
+ createWarpRouteDeployConfig,
+ readWarpCoreConfig,
+} from '../config/warp.js';
+import {
+ CommandModuleWithContext,
+ CommandModuleWithWriteContext,
+} from '../context/types.js';
+import { evaluateIfDryRunFailure } from '../deploy/dry-run.js';
+import { runWarpRouteDeploy } from '../deploy/warp.js';
+import { log, logGray, logGreen, logRed, logTable } from '../logger.js';
+import { sendTestTransfer } from '../send/transfer.js';
+import { indentYamlOrJson, writeYamlOrJson } from '../utils/files.js';
+import { selectRegistryWarpRoute } from '../utils/tokens.js';
+
+import {
+ addressCommandOption,
+ chainCommandOption,
+ dryRunCommandOption,
+ fromAddressCommandOption,
+ outputFileCommandOption,
+ symbolCommandOption,
+ warpCoreConfigCommandOption,
+ warpDeploymentConfigCommandOption,
+} from './options.js';
+import { MessageOptionsArgTypes, messageOptions } from './send.js';
+
+/**
+ * Parent command
+ */
+export const warpCommand: CommandModule = {
+ command: 'warp',
+ describe: 'Manage Hyperlane warp routes',
+ builder: (yargs) =>
+ yargs
+ .command(deploy)
+ .command(init)
+ .command(read)
+ .command(send)
+ .version(false)
+ .demandCommand(),
+
+ handler: () => log('Command required'),
+};
+
+export const deploy: CommandModuleWithWriteContext<{
+ config: string;
+ 'dry-run': string;
+ 'from-address': string;
+}> = {
+ command: 'deploy',
+ describe: 'Deploy Warp Route contracts',
+ builder: {
+ config: warpDeploymentConfigCommandOption,
+ 'dry-run': dryRunCommandOption,
+ 'from-address': fromAddressCommandOption,
+ },
+ handler: async ({ context, config, dryRun }) => {
+ logGray(`Hyperlane warp route deployment${dryRun ? ' dry-run' : ''}`);
+ logGray('------------------------------------------------');
+
+ try {
+ await runWarpRouteDeploy({
+ context,
+ warpRouteDeploymentConfigPath: config,
+ });
+ } catch (error: any) {
+ evaluateIfDryRunFailure(error, dryRun);
+ throw error;
+ }
+ process.exit(0);
+ },
+};
+
+export const init: CommandModuleWithContext<{
+ advanced: boolean;
+ out: string;
+}> = {
+ command: 'init',
+ describe: 'Create a warp route configuration.',
+ builder: {
+ advanced: {
+ type: 'boolean',
+ describe: 'Create an advanced ISM',
+ default: false,
+ },
+ out: outputFileCommandOption('./configs/warp-route-deployment.yaml'),
+ },
+ handler: async ({ context, advanced, out }) => {
+ logGray('Hyperlane Warp Configure');
+ logGray('------------------------');
+
+ await createWarpRouteDeployConfig({
+ context,
+ outPath: out,
+ advanced,
+ });
+ process.exit(0);
+ },
+};
+
+export const read: CommandModuleWithContext<{
+ chain?: string;
+ address?: string;
+ out?: string;
+ symbol?: string;
+}> = {
+ command: 'read',
+ describe: 'Derive the warp route config from onchain artifacts',
+ builder: {
+ symbol: {
+ ...symbolCommandOption,
+ demandOption: false,
+ },
+ chain: {
+ ...chainCommandOption,
+ demandOption: false,
+ },
+ address: addressCommandOption(
+ 'Address of the router contract to read.',
+ false,
+ ),
+ out: outputFileCommandOption(),
+ },
+ handler: async ({ context, chain, address, out, symbol }) => {
+ logGray('Hyperlane Warp Reader');
+ logGray('---------------------');
+
+ const { multiProvider } = context;
+
+ let addresses: ChainMap;
+ if (symbol) {
+ const warpCoreConfig = await selectRegistryWarpRoute(
+ context.registry,
+ symbol,
+ );
+
+ // TODO: merge with XERC20TokenAdapter and WarpRouteReader
+ const xerc20Limits = await Promise.all(
+ warpCoreConfig.tokens
+ .filter(
+ (t) =>
+ t.standard === TokenStandard.EvmHypXERC20 ||
+ t.standard === TokenStandard.EvmHypXERC20Lockbox,
+ )
+ .map(async (t) => {
+ const provider = multiProvider.getProvider(t.chainName);
+ const router = t.addressOrDenom!;
+ const xerc20Address =
+ t.standard === TokenStandard.EvmHypXERC20Lockbox
+ ? await HypXERC20Lockbox__factory.connect(
+ router,
+ provider,
+ ).xERC20()
+ : await HypXERC20__factory.connect(
+ router,
+ provider,
+ ).wrappedToken();
+
+ const xerc20 = IXERC20__factory.connect(xerc20Address, provider);
+ const mint = await xerc20.mintingCurrentLimitOf(router);
+ const burn = await xerc20.burningCurrentLimitOf(router);
+
+ const formattedLimits = objMap({ mint, burn }, (_, v) =>
+ ethers.utils.formatUnits(v, t.decimals),
+ );
+
+ return [t.chainName, formattedLimits];
+ }),
+ );
+ if (xerc20Limits.length > 0) {
+ logGray('xERC20 Limits:');
+ logTable(Object.fromEntries(xerc20Limits));
+ }
+
+ addresses = Object.fromEntries(
+ warpCoreConfig.tokens.map((t) => [t.chainName, t.addressOrDenom!]),
+ );
+ } else if (chain && address) {
+ addresses = {
+ [chain]: address,
+ };
+ } else {
+ logGreen(`Please specify either a symbol or chain and address`);
+ process.exit(0);
+ }
+
+ const config = await promiseObjAll(
+ objMap(addresses, async (chain, address) =>
+ new EvmERC20WarpRouteReader(multiProvider, chain).deriveWarpRouteConfig(
+ address,
+ ),
+ ),
+ );
+
+ if (out) {
+ writeYamlOrJson(out, config, 'yaml');
+ logGreen(`✅ Warp route config written successfully to ${out}:\n`);
+ } else {
+ logGreen(`✅ Warp route config read successfully:\n`);
+ }
+ log(indentYamlOrJson(yamlStringify(config, null, 2), 4));
+ process.exit(0);
+ },
+};
+
+const send: CommandModuleWithWriteContext<
+ MessageOptionsArgTypes & {
+ warp?: string;
+ symbol?: string;
+ router?: string;
+ amount: string;
+ recipient?: string;
+ }
+> = {
+ command: 'send',
+ describe: 'Send a test token transfer on a warp route',
+ builder: {
+ ...messageOptions,
+ symbol: {
+ ...symbolCommandOption,
+ demandOption: false,
+ },
+ warp: {
+ ...warpCoreConfigCommandOption,
+ demandOption: false,
+ },
+ amount: {
+ type: 'string',
+ description: 'Amount to send (in smallest unit)',
+ default: 1,
+ },
+ recipient: {
+ type: 'string',
+ description: 'Token recipient address (defaults to sender)',
+ },
+ },
+ handler: async ({
+ context,
+ origin,
+ destination,
+ timeout,
+ quick,
+ relay,
+ symbol,
+ warp,
+ amount,
+ recipient,
+ }) => {
+ let warpCoreConfig: WarpCoreConfig;
+ if (symbol) {
+ warpCoreConfig = await selectRegistryWarpRoute(context.registry, symbol);
+ } else if (warp) {
+ warpCoreConfig = readWarpCoreConfig(warp);
+ } else {
+ logRed(`Please specify either a symbol or warp config`);
+ process.exit(0);
+ }
+
+ await sendTestTransfer({
+ context,
+ warpCoreConfig,
+ origin,
+ destination,
+ amount,
+ recipient,
+ timeoutSec: timeout,
+ skipWaitForDelivery: quick,
+ selfRelay: relay,
+ });
+ process.exit(0);
+ },
+};
diff --git a/typescript/cli/src/config/agent.ts b/typescript/cli/src/config/agent.ts
new file mode 100644
index 0000000000..d4b59d68fa
--- /dev/null
+++ b/typescript/cli/src/config/agent.ts
@@ -0,0 +1,75 @@
+import { fromError } from 'zod-validation-error';
+
+import {
+ AgentConfigSchema,
+ ChainMap,
+ HyperlaneCore,
+ HyperlaneDeploymentArtifacts,
+ buildAgentConfig,
+} from '@hyperlane-xyz/sdk';
+import { objMap, promiseObjAll } from '@hyperlane-xyz/utils';
+
+import { CommandContext } from '../context/types.js';
+import { errorRed, logBlue, logGreen, logRed } from '../logger.js';
+import { writeYamlOrJson } from '../utils/files.js';
+
+export async function createAgentConfig({
+ context,
+ chains,
+ out,
+}: {
+ context: CommandContext;
+ chains: string[];
+ out: string;
+}) {
+ logBlue('\nCreating agent config...');
+
+ const { registry, multiProvider, chainMetadata } = context;
+ const addresses = await registry.getAddresses();
+
+ const core = HyperlaneCore.fromAddressesMap(addresses, multiProvider);
+
+ const startBlocks = await promiseObjAll(
+ objMap(addresses, async (chain, _) => {
+ // If the index.from is specified in the chain metadata, use that.
+ const indexFrom = chainMetadata[chain].index?.from;
+ if (indexFrom !== undefined) {
+ return indexFrom;
+ }
+
+ const mailbox = core.getContracts(chain).mailbox;
+ try {
+ const deployedBlock = await mailbox.deployedBlock();
+ return deployedBlock.toNumber();
+ } catch (err) {
+ logRed(
+ `Failed to get deployed block to set an index for ${chain}, this is potentially an issue with rpc provider or a misconfiguration`,
+ );
+ process.exit(1);
+ }
+ }),
+ );
+
+ // @TODO: consider adding additional config used to pass in gas prices for Cosmos chains
+ const agentConfig = buildAgentConfig(
+ chains,
+ multiProvider,
+ addresses as ChainMap,
+ startBlocks,
+ );
+
+ try {
+ AgentConfigSchema.parse(agentConfig);
+ } catch (e) {
+ errorRed(
+ `Agent config is invalid, this is possibly due to required contracts not being deployed. See details below:\n${fromError(
+ e,
+ ).toString()}`,
+ );
+ process.exit(1);
+ }
+
+ logBlue(`Agent config is valid, writing to file ${out}`);
+ writeYamlOrJson(out, agentConfig, 'json');
+ logGreen(`✅ Agent config successfully written to ${out}`);
+}
diff --git a/typescript/cli/src/config/chain.ts b/typescript/cli/src/config/chain.ts
index c2655cab31..924bbd3259 100644
--- a/typescript/cli/src/config/chain.ts
+++ b/typescript/cli/src/config/chain.ts
@@ -1,13 +1,18 @@
import { confirm, input } from '@inquirer/prompts';
import { ethers } from 'ethers';
+import { stringify as yamlStringify } from 'yaml';
-import { ChainMetadata, ChainMetadataSchema } from '@hyperlane-xyz/sdk';
+import {
+ ChainMetadata,
+ ChainMetadataSchema,
+ ZChainName,
+} from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { CommandContext } from '../context/types.js';
import { errorRed, log, logBlue, logGreen } from '../logger.js';
-import { detectAndConfirmOrPrompt } from '../utils/chains.js';
-import { readYamlOrJson } from '../utils/files.js';
+import { indentYamlOrJson, readYamlOrJson } from '../utils/files.js';
+import { detectAndConfirmOrPrompt } from '../utils/input.js';
export function readChainConfigs(filePath: string) {
log(`Reading file configs in ${filePath}`);
@@ -48,19 +53,19 @@ export async function createChainConfig({
},
'Enter http or https',
'rpc url',
+ 'JSON RPC provider',
);
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
- const name = await detectAndConfirmOrPrompt(
- async () => {
- const clientName = await provider.send('web3_clientVersion', []);
- const port = rpcUrl.split(':').slice(-1);
- const client = clientName.split('/')[0];
- return `${client}${port}`;
- },
- 'Enter (one word, lower case)',
- 'chain name',
- );
+ const name = await input({
+ message: 'Enter chain name (one word, lower case)',
+ validate: (chainName) => ZChainName.safeParse(chainName).success,
+ });
+
+ const displayName = await input({
+ message: 'Enter chain display name',
+ default: name[0].toUpperCase() + name.slice(1),
+ });
const chainId = parseInt(
await detectAndConfirmOrPrompt(
@@ -70,79 +75,29 @@ export async function createChainConfig({
},
'Enter a (number)',
'chain id',
+ 'JSON RPC provider',
),
10,
);
const metadata: ChainMetadata = {
name,
+ displayName,
chainId,
domainId: chainId,
protocol: ProtocolType.Ethereum,
rpcUrls: [{ http: rpcUrl }],
};
- const wantAdvancedConfig = await confirm({
- default: false,
- message:
- 'Do you want to set block or gas properties for this chain config?',
- });
- if (wantAdvancedConfig) {
- const wantBlockConfig = await confirm({
- message: 'Do you want to add block config for this chain?',
- });
- if (wantBlockConfig) {
- const blockConfirmation = await input({
- message:
- 'Enter no. of blocks to wait before considering a transaction confirmed(0-500)',
- validate: (value) => parseInt(value) >= 0 && parseInt(value) <= 500,
- });
- const blockReorgPeriod = await input({
- message:
- 'Enter no. of blocks before a transaction has a near-zero chance of reverting(0-500)',
- validate: (value) => parseInt(value) >= 0 && parseInt(value) <= 500,
- });
- const blockTimeEstimate = await input({
- message: 'Enter the rough estimate of time per block in seconds(0-20)',
- validate: (value) => parseInt(value) >= 0 && parseInt(value) <= 20,
- });
- metadata.blocks = {
- confirmations: parseInt(blockConfirmation, 10),
- reorgPeriod: parseInt(blockReorgPeriod, 10),
- estimateBlockTime: parseInt(blockTimeEstimate, 10),
- };
- }
- const wantGasConfig = await confirm({
- message: 'Do you want to add gas config for this chain?',
- });
- if (wantGasConfig) {
- const isEIP1559 = await confirm({
- message: 'Is your chain an EIP1559 enabled?',
- });
- if (isEIP1559) {
- const maxFeePerGas = await input({
- message: 'Enter the max fee per gas in gwei',
- });
- const maxPriorityFeePerGas = await input({
- message: 'Enter the max priority fee per gas in gwei',
- });
- metadata.transactionOverrides = {
- maxFeePerGas: BigInt(maxFeePerGas) * BigInt(10 ** 9),
- maxPriorityFeePerGas: BigInt(maxPriorityFeePerGas) * BigInt(10 ** 9),
- };
- } else {
- const gasPrice = await input({
- message: 'Enter the gas price in gwei',
- });
- metadata.transactionOverrides = {
- gasPrice: BigInt(gasPrice) * BigInt(10 ** 9),
- };
- }
- }
- }
+ await addBlockOrGasConfig(metadata);
+
+ await addNativeTokenConfig(metadata);
+
const parseResult = ChainMetadataSchema.safeParse(metadata);
if (parseResult.success) {
- logGreen(`Chain config is valid, writing to registry`);
+ logGreen(`Chain config is valid, writing to registry:`);
+ const metadataYaml = yamlStringify(metadata, null, 2);
+ log(indentYamlOrJson(metadataYaml, 4));
await context.registry.updateChain({ chainName: metadata.name, metadata });
} else {
errorRed(
@@ -152,3 +107,97 @@ export async function createChainConfig({
throw new Error('Invalid chain config');
}
}
+
+async function addBlockOrGasConfig(metadata: ChainMetadata): Promise {
+ const wantBlockOrGasConfig = await confirm({
+ default: false,
+ message: 'Do you want to set block or gas properties for this chain config',
+ });
+ if (wantBlockOrGasConfig) {
+ await addBlockConfig(metadata);
+ await addGasConfig(metadata);
+ }
+}
+
+async function addBlockConfig(metadata: ChainMetadata): Promise {
+ const wantBlockConfig = await confirm({
+ message: 'Do you want to add block config for this chain',
+ });
+ if (wantBlockConfig) {
+ const blockConfirmation = await input({
+ message:
+ 'Enter no. of blocks to wait before considering a transaction confirmed (0-500):',
+ validate: (value) => parseInt(value) >= 0 && parseInt(value) <= 500,
+ });
+ const blockReorgPeriod = await input({
+ message:
+ 'Enter no. of blocks before a transaction has a near-zero chance of reverting (0-500):',
+ validate: (value) => parseInt(value) >= 0 && parseInt(value) <= 500,
+ });
+ const blockTimeEstimate = await input({
+ message: 'Enter the rough estimate of time per block in seconds (0-20):',
+ validate: (value) => parseInt(value) >= 0 && parseInt(value) <= 20,
+ });
+ metadata.blocks = {
+ confirmations: parseInt(blockConfirmation, 10),
+ reorgPeriod: parseInt(blockReorgPeriod, 10),
+ estimateBlockTime: parseInt(blockTimeEstimate, 10),
+ };
+ }
+}
+
+async function addGasConfig(metadata: ChainMetadata): Promise {
+ const wantGasConfig = await confirm({
+ message: 'Do you want to add gas config for this chain',
+ });
+ if (wantGasConfig) {
+ const isEIP1559 = await confirm({
+ message: 'Is your chain an EIP1559 enabled',
+ });
+ if (isEIP1559) {
+ const maxFeePerGas = await input({
+ message: 'Enter the max fee per gas (gwei):',
+ });
+ const maxPriorityFeePerGas = await input({
+ message: 'Enter the max priority fee per gas (gwei):',
+ });
+ metadata.transactionOverrides = {
+ maxFeePerGas: BigInt(maxFeePerGas) * BigInt(10 ** 9),
+ maxPriorityFeePerGas: BigInt(maxPriorityFeePerGas) * BigInt(10 ** 9),
+ };
+ } else {
+ const gasPrice = await input({
+ message: 'Enter the gas price (gwei):',
+ });
+ metadata.transactionOverrides = {
+ gasPrice: BigInt(gasPrice) * BigInt(10 ** 9),
+ };
+ }
+ }
+}
+
+async function addNativeTokenConfig(metadata: ChainMetadata): Promise {
+ const wantNativeConfig = await confirm({
+ default: false,
+ message:
+ 'Do you want to set native token properties for this chain config (defaults to ETH)',
+ });
+ let symbol, name, decimals;
+ if (wantNativeConfig) {
+ symbol = await input({
+ message: "Enter the native token's symbol:",
+ });
+ name = await input({
+ message: `Enter the native token's name:`,
+ });
+ decimals = await input({
+ message: "Enter the native token's decimals:",
+ });
+ }
+
+ metadata.nativeToken = {
+ symbol: symbol ?? 'ETH',
+ name: name ?? 'Ether',
+ decimals: decimals ? parseInt(decimals, 10) : 18,
+ };
+}
diff --git a/typescript/cli/src/config/core.ts b/typescript/cli/src/config/core.ts
new file mode 100644
index 0000000000..0588185f49
--- /dev/null
+++ b/typescript/cli/src/config/core.ts
@@ -0,0 +1,71 @@
+import { stringify as yamlStringify } from 'yaml';
+
+import { CoreConfigSchema, HookConfig, IsmConfig } from '@hyperlane-xyz/sdk';
+
+import { CommandContext } from '../context/types.js';
+import { errorRed, log, logBlue, logGreen } from '../logger.js';
+import { indentYamlOrJson, writeYamlOrJson } from '../utils/files.js';
+import { detectAndConfirmOrPrompt } from '../utils/input.js';
+
+import {
+ createHookConfig,
+ createMerkleTreeConfig,
+ createProtocolFeeConfig,
+} from './hooks.js';
+import { createAdvancedIsmConfig, createTrustedRelayerConfig } from './ism.js';
+
+export async function createCoreDeployConfig({
+ context,
+ configFilePath,
+ advanced = false,
+}: {
+ context: CommandContext;
+ configFilePath: string;
+ advanced: boolean;
+}) {
+ logBlue('Creating a new core deployment config...');
+
+ const owner = await detectAndConfirmOrPrompt(
+ async () => context.signer?.getAddress(),
+ 'Enter the desired',
+ 'owner address',
+ 'signer',
+ );
+
+ const defaultIsm: IsmConfig = advanced
+ ? await createAdvancedIsmConfig(context)
+ : await createTrustedRelayerConfig(context, advanced);
+
+ let defaultHook: HookConfig, requiredHook: HookConfig;
+ if (advanced) {
+ defaultHook = await createHookConfig({
+ context,
+ selectMessage: 'Select default hook type',
+ advanced,
+ });
+ requiredHook = await createHookConfig({
+ context,
+ selectMessage: 'Select required hook type',
+ advanced,
+ });
+ } else {
+ defaultHook = await createMerkleTreeConfig();
+ requiredHook = await createProtocolFeeConfig(context, advanced);
+ }
+
+ try {
+ const coreConfig = CoreConfigSchema.parse({
+ owner,
+ defaultIsm,
+ defaultHook,
+ requiredHook,
+ });
+ logBlue(`Core config is valid, writing to file ${configFilePath}:\n`);
+ log(indentYamlOrJson(yamlStringify(coreConfig, null, 2), 4));
+ writeYamlOrJson(configFilePath, coreConfig, 'yaml');
+ logGreen('✅ Successfully created new core deployment config.');
+ } catch (e) {
+ errorRed(`Core deployment config is invalid.`);
+ throw e;
+ }
+}
diff --git a/typescript/cli/src/config/hooks.ts b/typescript/cli/src/config/hooks.ts
index aeb449deca..18308b81a1 100644
--- a/typescript/cli/src/config/hooks.ts
+++ b/typescript/cli/src/config/hooks.ts
@@ -6,9 +6,9 @@ import { z } from 'zod';
import {
ChainMap,
ChainName,
- GasOracleContractType,
+ HookConfig,
+ HookConfigSchema,
HookType,
- HooksConfig,
} from '@hyperlane-xyz/sdk';
import {
Address,
@@ -18,65 +18,24 @@ import {
} from '@hyperlane-xyz/utils';
import { CommandContext } from '../context/types.js';
-import { errorRed, log, logBlue, logGreen, logRed } from '../logger.js';
+import { errorRed, logBlue, logGreen, logRed } from '../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
-import { mergeYamlOrJson, readYamlOrJson } from '../utils/files.js';
+import { readYamlOrJson } from '../utils/files.js';
+import { detectAndConfirmOrPrompt, inputWithInfo } from '../utils/input.js';
-const ProtocolFeeSchema = z.object({
- type: z.literal(HookType.PROTOCOL_FEE),
- owner: z.string(),
- beneficiary: z.string(),
- maxProtocolFee: z.string(),
- protocolFee: z.string(),
-});
-
-const MerkleTreeSchema = z.object({
- type: z.literal(HookType.MERKLE_TREE),
-});
-
-const IGPSchema = z.object({
- type: z.literal(HookType.INTERCHAIN_GAS_PAYMASTER),
- owner: z.string(),
- beneficiary: z.string(),
- overhead: z.record(z.number()),
- gasOracleType: z.record(z.literal(GasOracleContractType.StorageGasOracle)),
- oracleKey: z.string(),
-});
-
-const RoutingConfigSchema: z.ZodSchema = z.lazy(() =>
- z.object({
- type: z.literal(HookType.ROUTING),
- owner: z.string(),
- domains: z.record(HookConfigSchema),
- }),
-);
-
-const AggregationConfigSchema: z.ZodSchema = z.lazy(() =>
- z.object({
- type: z.literal(HookType.AGGREGATION),
- hooks: z.array(HookConfigSchema),
- }),
-);
-
-const HookConfigSchema = z.union([
- ProtocolFeeSchema,
- MerkleTreeSchema,
- IGPSchema,
- RoutingConfigSchema,
- AggregationConfigSchema,
-]);
-export type HookConfig = z.infer;
+import { callWithConfigCreationLogs } from './utils.js';
+// TODO: deprecate in favor of CoreConfigSchema
const HooksConfigSchema = z.object({
- required: HookConfigSchema,
default: HookConfigSchema,
+ required: HookConfigSchema,
});
+export type HooksConfig = z.infer;
const HooksConfigMapSchema = z.record(HooksConfigSchema);
export type HooksConfigMap = z.infer;
-export function isValidHookConfigMap(config: any) {
- return HooksConfigMapSchema.safeParse(config).success;
-}
+const MAX_PROTOCOL_FEE_DEFAULT: string = toWei('0.1');
+const PROTOCOL_FEE_DEFAULT: string = toWei('0');
export function presetHookConfigs(owner: Address): HooksConfig {
return {
@@ -99,14 +58,7 @@ export function readHooksConfigMap(filePath: string) {
logRed(`No hook config found at ${filePath}`);
return;
}
- const result = HooksConfigMapSchema.safeParse(config);
- if (!result.success) {
- const firstIssue = result.error.issues[0];
- throw new Error(
- `Invalid hook config: ${firstIssue.path} => ${firstIssue.message}`,
- );
- }
- const parsedConfig = result.data;
+ const parsedConfig = HooksConfigMapSchema.parse(config);
const hooks: ChainMap = objMap(
parsedConfig,
(_, config) => config as HooksConfig,
@@ -115,47 +67,24 @@ export function readHooksConfigMap(filePath: string) {
return hooks;
}
-export async function createHooksConfigMap({
+export async function createHookConfig({
context,
- outPath,
+ selectMessage = 'Select hook type',
+ advanced = false,
}: {
context: CommandContext;
- outPath: string;
-}) {
- logBlue('Creating a new hook config');
- const chains = await runMultiChainSelectionStep(context.chainMetadata);
-
- const result: HooksConfigMap = {};
- for (const chain of chains) {
- for (const hookRequirements of ['required', 'default']) {
- log(`Setting ${hookRequirements} hook for chain ${chain}`);
- const remotes = chains.filter((c) => c !== chain);
- result[chain] = {
- ...result[chain],
- [hookRequirements]: await createHookConfig(context, chain, remotes),
- };
- }
- if (isValidHookConfigMap(result)) {
- logGreen(`Hook config is valid, writing to file ${outPath}`);
- 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`,
- );
- throw new Error('Invalid hook config');
- }
- }
-}
-
-export async function createHookConfig(
- context: CommandContext,
- chain: ChainName,
- remotes: ChainName[],
-): Promise {
- let lastConfig: HookConfig;
+ selectMessage?: string;
+ advanced?: boolean;
+}): Promise {
const hookType = await select({
- message: 'Select hook type',
+ message: selectMessage,
choices: [
+ {
+ value: HookType.AGGREGATION,
+ name: HookType.AGGREGATION,
+ description:
+ 'Aggregate multiple hooks into a single hook (e.g. merkle tree + IGP) which will be called in sequence',
+ },
{
value: HookType.MERKLE_TREE,
name: HookType.MERKLE_TREE,
@@ -167,193 +96,190 @@ export async function createHookConfig(
name: HookType.PROTOCOL_FEE,
description: 'Charge fees for each message dispatch from this chain',
},
- {
- value: HookType.INTERCHAIN_GAS_PAYMASTER,
- name: HookType.INTERCHAIN_GAS_PAYMASTER,
- description:
- 'Allow for payments for expected gas to be paid by the relayer while delivering on remote chain',
- },
- {
- value: HookType.AGGREGATION,
- name: HookType.AGGREGATION,
- description:
- 'Aggregate multiple hooks into a single hook (e.g. merkle tree + IGP) which will be called in sequence',
- },
- {
- value: HookType.ROUTING,
- name: HookType.ROUTING,
- description:
- 'Each destination domain can have its own hook configured via DomainRoutingHook',
- },
],
pageSize: 10,
});
- if (hookType === HookType.MERKLE_TREE) {
- lastConfig = { type: HookType.MERKLE_TREE };
- } else if (hookType === HookType.PROTOCOL_FEE) {
- lastConfig = await createProtocolFeeConfig(context, chain);
- } else if (hookType === HookType.INTERCHAIN_GAS_PAYMASTER) {
- lastConfig = await createIGPConfig(remotes);
- } else if (hookType === HookType.AGGREGATION) {
- lastConfig = await createAggregationConfig(context, chain, remotes);
- } else if (hookType === HookType.ROUTING) {
- lastConfig = await createRoutingConfig(context, chain, remotes);
- } else {
- throw new Error(`Invalid hook type: ${hookType}`);
+
+ switch (hookType) {
+ case HookType.AGGREGATION:
+ return createAggregationConfig(context, advanced);
+ case HookType.MERKLE_TREE:
+ return createMerkleTreeConfig();
+ case HookType.PROTOCOL_FEE:
+ return createProtocolFeeConfig(context, advanced);
+ default:
+ throw new Error(`Invalid hook type: ${hookType}`);
}
- return lastConfig;
}
-export async function createProtocolFeeConfig(
- context: CommandContext,
- chain: ChainName,
-): Promise {
- const owner = await input({
- message: 'Enter owner address',
- });
- const ownerAddress = normalizeAddressEvm(owner);
- let beneficiary;
- let sameAsOwner = false;
- sameAsOwner = await confirm({
- message: 'Use this same address for the beneficiary?',
- });
- if (sameAsOwner) {
- beneficiary = ownerAddress;
- } else {
- beneficiary = await input({
- message: 'Enter beneficiary address',
- });
- }
- const beneficiaryAddress = normalizeAddressEvm(beneficiary);
- // TODO: input in gwei, wei, etc
- const maxProtocolFee = toWei(
- await input({
- message: `Enter max protocol fee ${nativeTokenAndDecimals(
- context,
- chain,
- )} e.g. 1.0)`,
- }),
- );
- const protocolFee = toWei(
- await input({
- message: `Enter protocol fee in ${nativeTokenAndDecimals(
- context,
- chain,
- )} e.g. 0.01)`,
- }),
- );
- if (BigNumberJs(protocolFee).gt(maxProtocolFee)) {
- errorRed('Protocol fee cannot be greater than max protocol fee');
- throw new Error('Invalid protocol fee');
- }
+export const createMerkleTreeConfig = callWithConfigCreationLogs(
+ async (): Promise => {
+ return { type: HookType.MERKLE_TREE };
+ },
+ HookType.MERKLE_TREE,
+);
- return {
- type: HookType.PROTOCOL_FEE,
- maxProtocolFee: maxProtocolFee.toString(),
- protocolFee: protocolFee.toString(),
- beneficiary: beneficiaryAddress,
- owner: ownerAddress,
- };
-}
+export const createProtocolFeeConfig = callWithConfigCreationLogs(
+ async (
+ context: CommandContext,
+ advanced: boolean = false,
+ ): Promise => {
+ const unnormalizedOwner =
+ !advanced && context.signer
+ ? await context.signer.getAddress()
+ : await detectAndConfirmOrPrompt(
+ async () => context.signer?.getAddress(),
+ 'For protocol fee hook, enter',
+ 'owner address',
+ 'signer',
+ );
+ const owner = normalizeAddressEvm(unnormalizedOwner);
+ let beneficiary = owner;
-export async function createIGPConfig(
- remotes: ChainName[],
-): Promise {
- const owner = await input({
- message: 'Enter owner address',
- });
- const ownerAddress = normalizeAddressEvm(owner);
- let beneficiary, oracleKey;
- let sameAsOwner = false;
- sameAsOwner = await confirm({
- message: 'Use this same address for the beneficiary and gasOracleKey?',
- });
- if (sameAsOwner) {
- beneficiary = ownerAddress;
- oracleKey = ownerAddress;
- } else {
- beneficiary = await input({
- message: 'Enter beneficiary address',
+ const isBeneficiarySameAsOwner = advanced
+ ? await confirm({
+ message: `Use this same address (${owner}) for the beneficiary?`,
+ })
+ : true;
+
+ if (!isBeneficiarySameAsOwner) {
+ const unnormalizedBeneficiary = await input({
+ message: 'Enter beneficiary address for protocol fee hook:',
+ });
+ beneficiary = normalizeAddressEvm(unnormalizedBeneficiary);
+ }
+ // TODO: input in gwei, wei, etc
+ const maxProtocolFee = advanced
+ ? toWei(
+ await inputWithInfo({
+ message: `Enter max protocol fee for protocol fee hook (wei):`,
+ info: `The max protocol fee (ProtocolFee.MAX_PROTOCOL_FEE) is the maximum value the protocol fee on the ProtocolFee hook contract can ever be set to.\nDefault is set to ${MAX_PROTOCOL_FEE_DEFAULT} wei; between 0.001 and 0.1 wei is recommended.`,
+ }),
+ )
+ : MAX_PROTOCOL_FEE_DEFAULT;
+ const protocolFee = advanced
+ ? toWei(
+ await inputWithInfo({
+ message: `Enter protocol fee for protocol fee hook (wei):`,
+ info: `The protocol fee is the fee collected by the beneficiary of the ProtocolFee hook for every transaction executed with this hook.\nDefault is set to 0 wei; must be less than max protocol fee of ${maxProtocolFee}.`,
+ }),
+ )
+ : PROTOCOL_FEE_DEFAULT;
+ if (BigNumberJs(protocolFee).gt(maxProtocolFee)) {
+ errorRed(
+ `Protocol fee (${protocolFee}) cannot be greater than max protocol fee (${maxProtocolFee}).`,
+ );
+ throw new Error(`Invalid protocol fee (${protocolFee}).`);
+ }
+ return {
+ type: HookType.PROTOCOL_FEE,
+ maxProtocolFee,
+ protocolFee,
+ beneficiary,
+ owner,
+ };
+ },
+ HookType.PROTOCOL_FEE,
+);
+
+// TODO: make this usable
+export const createIGPConfig = callWithConfigCreationLogs(
+ async (remotes: ChainName[]): Promise => {
+ const unnormalizedOwner = await input({
+ message: 'Enter owner address for IGP hook',
});
- oracleKey = await input({
- message: 'Enter gasOracleKey address',
+ const owner = normalizeAddressEvm(unnormalizedOwner);
+ let beneficiary = owner;
+ let oracleKey = owner;
+
+ const beneficiarySameAsOwner = await confirm({
+ message: 'Use this same address for the beneficiary and gasOracleKey?',
});
- }
- const beneficiaryAddress = normalizeAddressEvm(beneficiary);
- const oracleKeyAddress = normalizeAddressEvm(oracleKey);
- const overheads: ChainMap = {};
- for (const chain of remotes) {
- const overhead = parseInt(
+
+ if (!beneficiarySameAsOwner) {
+ const unnormalizedBeneficiary = await input({
+ message: 'Enter beneficiary address for IGP hook',
+ });
+ beneficiary = normalizeAddressEvm(unnormalizedBeneficiary);
+ const unnormalizedOracleKey = await input({
+ message: 'Enter gasOracleKey address for IGP hook',
+ });
+ oracleKey = normalizeAddressEvm(unnormalizedOracleKey);
+ }
+ const overheads: ChainMap = {};
+ for (const chain of remotes) {
+ const overhead = parseInt(
+ await input({
+ message: `Enter overhead for ${chain} (eg 75000) for IGP hook`,
+ }),
+ );
+ overheads[chain] = overhead;
+ }
+ return {
+ type: HookType.INTERCHAIN_GAS_PAYMASTER,
+ beneficiary,
+ owner,
+ oracleKey,
+ overhead: overheads,
+ oracleConfig: {},
+ };
+ },
+ HookType.INTERCHAIN_GAS_PAYMASTER,
+);
+
+export const createAggregationConfig = callWithConfigCreationLogs(
+ async (
+ context: CommandContext,
+ advanced: boolean = false,
+ ): Promise => {
+ const hooksNum = parseInt(
await input({
- message: `Enter overhead for ${chain} (eg 75000)`,
+ message: 'Enter the number of hooks to aggregate (number)',
}),
+ 10,
);
- overheads[chain] = overhead;
- }
- return {
- type: HookType.INTERCHAIN_GAS_PAYMASTER,
- beneficiary: beneficiaryAddress,
- owner: ownerAddress,
- oracleKey: oracleKeyAddress,
- overhead: overheads,
- gasOracleType: objMap(
- overheads,
- () => GasOracleContractType.StorageGasOracle,
- ),
- };
-}
-
-export async function createAggregationConfig(
- context: CommandContext,
- chain: ChainName,
- remotes: ChainName[],
-): Promise {
- const hooksNum = parseInt(
- await input({
- message: 'Enter the number of hooks to aggregate (number)',
- }),
- 10,
- );
- const hooks: Array = [];
- for (let i = 0; i < hooksNum; i++) {
- logBlue(`Creating hook ${i + 1} of ${hooksNum} ...`);
- hooks.push(await createHookConfig(context, chain, remotes));
- }
- return {
- type: HookType.AGGREGATION,
- hooks,
- };
-}
-
-export async function createRoutingConfig(
- context: CommandContext,
- origin: ChainName,
- remotes: ChainName[],
-): Promise {
- const owner = await input({
- message: 'Enter owner address',
- });
- const ownerAddress = owner;
+ const hooks: Array = [];
+ for (let i = 0; i < hooksNum; i++) {
+ logBlue(`Creating hook ${i + 1} of ${hooksNum} ...`);
+ hooks.push(
+ await createHookConfig({
+ context,
+ advanced,
+ }),
+ );
+ }
+ return {
+ type: HookType.AGGREGATION,
+ hooks,
+ };
+ },
+ HookType.AGGREGATION,
+);
- const domainsMap: ChainMap = {};
- for (const chain of remotes) {
- await confirm({
- message: `You are about to configure hook for remote chain ${chain}. Continue?`,
+export const createRoutingConfig = callWithConfigCreationLogs(
+ async (
+ context: CommandContext,
+ advanced: boolean = false,
+ ): Promise => {
+ const owner = await input({
+ message: 'Enter owner address for routing ISM',
});
- const config = await createHookConfig(context, origin, remotes);
- domainsMap[chain] = config;
- }
- return {
- type: HookType.ROUTING,
- owner: ownerAddress,
- domains: domainsMap,
- };
-}
+ const ownerAddress = owner;
+ const chains = await runMultiChainSelectionStep(context.chainMetadata);
-function nativeTokenAndDecimals(context: CommandContext, chain: ChainName) {
- return `10^${
- context.chainMetadata[chain].nativeToken?.decimals ?? '18'
- } which you cannot exceed (in ${
- context.chainMetadata[chain].nativeToken?.symbol ?? 'eth'
- }`;
-}
+ const domainsMap: ChainMap = {};
+ for (const chain of chains) {
+ await confirm({
+ message: `You are about to configure hook for remote chain ${chain}. Continue?`,
+ });
+ const config = await createHookConfig({ context, advanced });
+ domainsMap[chain] = config;
+ }
+ return {
+ type: HookType.ROUTING,
+ owner: ownerAddress,
+ domains: domainsMap,
+ };
+ },
+ HookType.ROUTING,
+);
diff --git a/typescript/cli/src/config/ism.ts b/typescript/cli/src/config/ism.ts
index 66075c4938..eb976f2dd4 100644
--- a/typescript/cli/src/config/ism.ts
+++ b/typescript/cli/src/config/ism.ts
@@ -1,7 +1,15 @@
-import { confirm, input, select } from '@inquirer/prompts';
+import { input, select } from '@inquirer/prompts';
import { z } from 'zod';
-import { ChainMap, ChainName, IsmType, ZHash } from '@hyperlane-xyz/sdk';
+import {
+ AggregationIsmConfig,
+ ChainMap,
+ IsmConfig,
+ IsmConfigSchema,
+ IsmType,
+ MultisigIsmConfig,
+ TrustedRelayerIsmConfig,
+} from '@hyperlane-xyz/sdk';
import { CommandContext } from '../context/types.js';
import {
@@ -9,70 +17,22 @@ import {
log,
logBlue,
logBoldUnderlinedRed,
- logGreen,
logRed,
} from '../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
-import { mergeYamlOrJson, readYamlOrJson } from '../utils/files.js';
-
-const MultisigIsmConfigSchema = z.object({
- type: z.union([
- z.literal(IsmType.MERKLE_ROOT_MULTISIG),
- z.literal(IsmType.MESSAGE_ID_MULTISIG),
- ]),
- threshold: z.number(),
- validators: z.array(ZHash),
-});
-
-const RoutingIsmConfigSchema: z.ZodSchema = z.lazy(() =>
- z.object({
- type: z.union([
- z.literal(IsmType.ROUTING),
- z.literal(IsmType.FALLBACK_ROUTING),
- ]),
- owner: ZHash,
- domains: z.record(IsmConfigSchema),
- }),
-);
+import { readYamlOrJson } from '../utils/files.js';
+import { detectAndConfirmOrPrompt } from '../utils/input.js';
-const AggregationIsmConfigSchema: z.ZodSchema = z
- .lazy(() =>
- z.object({
- type: z.literal(IsmType.AGGREGATION),
- modules: z.array(IsmConfigSchema),
- threshold: z.number(),
- }),
- )
- .refine(
- // check ig modules.length >= threshold
- (ismConfig) => {
- return ismConfig.modules.length >= ismConfig.threshold;
- },
- {
- message: 'Threshold cannot be greater than number of modules',
- },
- );
-
-const TestIsmConfigSchema = z.object({
- type: z.literal(IsmType.TEST_ISM),
-});
+import { callWithConfigCreationLogs } from './utils.js';
-const TrustedRelayerIsmConfigSchema = z.object({
- type: z.literal(IsmType.TRUSTED_RELAYER),
- relayer: ZHash,
-});
-
-const IsmConfigSchema = z.union([
- MultisigIsmConfigSchema,
- RoutingIsmConfigSchema,
- AggregationIsmConfigSchema,
- TestIsmConfigSchema,
- TrustedRelayerIsmConfigSchema,
-]);
const IsmConfigMapSchema = z.record(IsmConfigSchema).refine(
(ismConfigMap) => {
// check if any key in IsmConfigMap is found in its own RoutingIsmConfigSchema.domains
for (const [key, config] of Object.entries(ismConfigMap)) {
+ if (typeof config === 'string') {
+ continue;
+ }
+
if (config.type === IsmType.ROUTING) {
if (config.domains && key in config.domains) {
return false;
@@ -86,8 +46,6 @@ const IsmConfigMapSchema = z.record(IsmConfigSchema).refine(
'Cannot set RoutingIsm.domain to the same chain you are configuring',
},
);
-export type ZodIsmConfig = z.infer;
-export type ZodIsmConfigMap = z.infer;
export function parseIsmConfig(filePath: string) {
const config = readYamlOrJson(filePath);
@@ -107,196 +65,210 @@ export function readIsmConfig(filePath: string) {
return parsedConfig;
}
-export function isValildIsmConfig(config: any) {
- return IsmConfigMapSchema.safeParse(config).success;
-}
+const ISM_TYPE_DESCRIPTIONS: Record = {
+ [IsmType.AGGREGATION]:
+ 'You can aggregate multiple ISMs into one ISM via AggregationISM',
+ [IsmType.FALLBACK_ROUTING]:
+ "You can specify ISM type for specific chains you like and fallback to mailbox's default ISM for other chains via DefaultFallbackRoutingISM",
+ [IsmType.MERKLE_ROOT_MULTISIG]:
+ 'Validators need to sign the root of the merkle tree of all messages from origin chain',
+ [IsmType.MESSAGE_ID_MULTISIG]: 'Validators need to sign just this messageId',
+ [IsmType.ROUTING]:
+ 'Each origin chain can be verified by the specified ISM type via RoutingISM',
+ [IsmType.TEST_ISM]:
+ 'ISM where you can deliver messages without any validation (WARNING: only for testing, do not use in production)',
+ [IsmType.TRUSTED_RELAYER]: 'Deliver messages from an authorized address',
+};
-export async function createIsmConfigMap({
- context,
- outPath,
-}: {
- context: CommandContext;
- outPath: string;
-}) {
+export async function createAdvancedIsmConfig(
+ context: CommandContext,
+): Promise {
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 chains = await runMultiChainSelectionStep(
- context.chainMetadata,
- 'Select chains to configure ISM for',
- true,
- );
-
- const result: ZodIsmConfigMap = {};
- for (const chain of chains) {
- log(`Setting values for chain ${chain}`);
- result[chain] = await createIsmConfig(chain, chains);
- // TODO consider re-enabling. Disabling based on feedback from @nambrot for now.
- // repeat = await confirm({
- // message: 'Use this same config for remaining chains?',
- // });
- }
-
- if (isValildIsmConfig(result)) {
- logGreen(`ISM config is valid, writing to file ${outPath}`);
- 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`,
- );
- throw new Error('Invalid ISM config');
- }
-}
-
-export async function createIsmConfig(
- remote: ChainName,
- origins: ChainName[],
-): Promise {
- let lastConfig: ZodIsmConfig;
const moduleType = await select({
message: 'Select ISM type',
- choices: [
- {
- value: IsmType.MESSAGE_ID_MULTISIG,
- description: 'Validators need to sign just this messageId',
- },
- {
- value: IsmType.MERKLE_ROOT_MULTISIG,
- description:
- 'Validators need to sign the root of the merkle tree of all messages from origin chain',
- },
- {
- value: IsmType.ROUTING,
- description:
- 'Each origin chain can be verified by the specified ISM type via RoutingISM',
- },
- {
- value: IsmType.FALLBACK_ROUTING,
- description:
- "You can specify ISM type for specific chains you like and fallback to mailbox's default ISM for other chains via DefaultFallbackRoutingISM",
- },
- {
- value: IsmType.AGGREGATION,
- description:
- 'You can aggregate multiple ISMs into one ISM via AggregationISM',
- },
- {
- value: IsmType.TRUSTED_RELAYER,
- description: 'Deliver messages from an authorized address',
- },
- {
- value: IsmType.TEST_ISM,
- description:
- 'ISM where you can deliver messages without any validation (WARNING: only for testing, do not use in production)',
- },
- ],
+ choices: Object.entries(ISM_TYPE_DESCRIPTIONS).map(
+ ([value, description]) => ({
+ value,
+ description,
+ }),
+ ),
pageSize: 10,
});
- if (
- moduleType === IsmType.MESSAGE_ID_MULTISIG ||
- moduleType === IsmType.MERKLE_ROOT_MULTISIG
- ) {
- lastConfig = await createMultisigConfig(moduleType);
- } else if (
- moduleType === IsmType.ROUTING ||
- moduleType === IsmType.FALLBACK_ROUTING
- ) {
- lastConfig = await createRoutingConfig(moduleType, remote, origins);
- } else if (moduleType === IsmType.AGGREGATION) {
- lastConfig = await createAggregationConfig(remote, origins);
- } else if (moduleType === IsmType.TEST_ISM) {
- lastConfig = { type: IsmType.TEST_ISM };
- } else if (moduleType === IsmType.TRUSTED_RELAYER) {
- lastConfig = await createTrustedRelayerConfig();
- } else {
- throw new Error(`Invalid ISM type: ${moduleType}}`);
+
+ switch (moduleType) {
+ case IsmType.AGGREGATION:
+ return createAggregationConfig(context);
+ case IsmType.FALLBACK_ROUTING:
+ return createFallbackRoutingConfig(context);
+ case IsmType.MERKLE_ROOT_MULTISIG:
+ return createMerkleRootMultisigConfig(context);
+ case IsmType.MESSAGE_ID_MULTISIG:
+ return createMessageIdMultisigConfig(context);
+ case IsmType.ROUTING:
+ return createRoutingConfig(context);
+ case IsmType.TEST_ISM:
+ return { type: IsmType.TEST_ISM };
+ case IsmType.TRUSTED_RELAYER:
+ return createTrustedRelayerConfig(context, true);
+ default:
+ throw new Error(`Invalid ISM type: ${moduleType}.`);
}
- return lastConfig;
}
-export async function createMultisigConfig(
- type: IsmType.MERKLE_ROOT_MULTISIG | IsmType.MESSAGE_ID_MULTISIG,
-): Promise {
- const thresholdInput = await input({
- message: 'Enter threshold of validators (number)',
- });
- const threshold = parseInt(thresholdInput, 10);
+export const createMerkleRootMultisigConfig = callWithConfigCreationLogs(
+ async (): Promise => {
+ const validatorsInput = await input({
+ message:
+ 'Enter validator addresses (comma separated list) for merkle root multisig ISM:',
+ });
+ const validators = validatorsInput.split(',').map((v) => v.trim());
+ const thresholdInput = await input({
+ message:
+ 'Enter threshold of validators (number) for merkle root multisig ISM:',
+ });
+ const threshold = parseInt(thresholdInput, 10);
+ if (threshold > validators.length) {
+ errorRed(
+ `Merkle root multisig signer threshold (${threshold}) cannot be greater than total number of validators (${validators.length}).`,
+ );
+ throw new Error('Invalid protocol fee.');
+ }
+ return {
+ type: IsmType.MERKLE_ROOT_MULTISIG,
+ threshold,
+ validators,
+ };
+ },
+ IsmType.MERKLE_ROOT_MULTISIG,
+);
- const validatorsInput = await input({
- message: 'Enter validator addresses (comma separated list)',
- });
- const validators = validatorsInput.split(',').map((v) => v.trim());
- return {
- type,
- threshold,
- validators,
- };
-}
+export const createMessageIdMultisigConfig = callWithConfigCreationLogs(
+ async (): Promise => {
+ const thresholdInput = await input({
+ message:
+ 'Enter threshold of validators (number) for message ID multisig ISM',
+ });
+ const threshold = parseInt(thresholdInput, 10);
-async function createTrustedRelayerConfig(): Promise {
- const relayer = await input({
- message: 'Enter relayer address',
- });
- return {
- type: IsmType.TRUSTED_RELAYER,
- relayer,
- };
-}
+ const validatorsInput = await input({
+ message:
+ 'Enter validator addresses (comma separated list) for message ID multisig ISM',
+ });
+ const validators = validatorsInput.split(',').map((v) => v.trim());
+ return {
+ type: IsmType.MESSAGE_ID_MULTISIG,
+ threshold,
+ validators,
+ };
+ },
+ IsmType.MESSAGE_ID_MULTISIG,
+);
-export async function createAggregationConfig(
- remote: ChainName,
- chains: ChainName[],
-): Promise {
- const isms = parseInt(
- await input({
- message: 'Enter the number of ISMs to aggregate (number)',
- }),
- 10,
- );
+export const createTrustedRelayerConfig = callWithConfigCreationLogs(
+ async (
+ context: CommandContext,
+ advanced: boolean = false,
+ ): Promise => {
+ const relayer =
+ !advanced && context.signer
+ ? await context.signer.getAddress()
+ : await detectAndConfirmOrPrompt(
+ async () => context.signer?.getAddress(),
+ 'For trusted relayer ISM, enter',
+ 'relayer address',
+ 'signer',
+ );
+ return {
+ type: IsmType.TRUSTED_RELAYER,
+ relayer,
+ };
+ },
+ IsmType.TRUSTED_RELAYER,
+);
- const threshold = parseInt(
- await input({
- message: 'Enter the threshold of ISMs to for verification (number)',
- }),
- 10,
- );
+export const createAggregationConfig = callWithConfigCreationLogs(
+ async (context: CommandContext): Promise => {
+ const isms = parseInt(
+ await input({
+ message: 'Enter the number of ISMs to aggregate (number)',
+ }),
+ 10,
+ );
- const modules: Array = [];
- for (let i = 0; i < isms; i++) {
- modules.push(await createIsmConfig(remote, chains));
- }
- return {
- type: IsmType.AGGREGATION,
- modules,
- threshold,
- };
-}
+ const threshold = parseInt(
+ await input({
+ message: 'Enter the threshold of ISMs for verification (number)',
+ }),
+ 10,
+ );
-export async function createRoutingConfig(
- type: IsmType.ROUTING | IsmType.FALLBACK_ROUTING,
- remote: ChainName,
- chains: ChainName[],
-): Promise {
- const owner = await input({
- message: 'Enter owner address',
- });
- const ownerAddress = owner;
- const origins = chains.filter((chain) => chain !== remote);
+ const modules: Array = [];
+ for (let i = 0; i < isms; i++) {
+ modules.push(await createAdvancedIsmConfig(context));
+ }
+ return {
+ type: IsmType.AGGREGATION,
+ modules,
+ threshold,
+ };
+ },
+ IsmType.AGGREGATION,
+);
- const domainsMap: ChainMap = {};
- for (const chain of origins) {
- await confirm({
- message: `You are about to configure ISM from source chain ${chain}. Continue?`,
+export const createRoutingConfig = callWithConfigCreationLogs(
+ async (context: CommandContext): Promise => {
+ const owner = await input({
+ message: 'Enter owner address for routing ISM',
});
- const config = await createIsmConfig(chain, chains);
- domainsMap[chain] = config;
- }
- return {
- type,
- owner: ownerAddress,
- domains: domainsMap,
- };
-}
+ const ownerAddress = owner;
+ const requireMultiple = true;
+ const chains = await runMultiChainSelectionStep(
+ context.chainMetadata,
+ 'Select chains to configure routing ISM for',
+ requireMultiple,
+ );
+
+ const domainsMap: ChainMap = {};
+ for (const chain of chains) {
+ log(`You are about to configure routing ISM from source chain ${chain}.`);
+ const config = await createAdvancedIsmConfig(context);
+ domainsMap[chain] = config;
+ }
+ return {
+ type: IsmType.ROUTING,
+ owner: ownerAddress,
+ domains: domainsMap,
+ };
+ },
+ IsmType.ROUTING,
+);
+
+export const createFallbackRoutingConfig = callWithConfigCreationLogs(
+ async (context: CommandContext): Promise => {
+ const chains = await runMultiChainSelectionStep(
+ context.chainMetadata,
+ 'Select chains to configure fallback routing ISM for',
+ true,
+ );
+
+ const domainsMap: ChainMap = {};
+ for (const chain of chains) {
+ log(
+ `You are about to configure fallback routing ISM from source chain ${chain}.`,
+ );
+ const config = await createAdvancedIsmConfig(context);
+ domainsMap[chain] = config;
+ }
+ return {
+ type: IsmType.FALLBACK_ROUTING,
+ owner: '',
+ domains: domainsMap,
+ };
+ },
+ IsmType.FALLBACK_ROUTING,
+);
diff --git a/typescript/cli/src/config/utils.ts b/typescript/cli/src/config/utils.ts
new file mode 100644
index 0000000000..abd1c6f82b
--- /dev/null
+++ b/typescript/cli/src/config/utils.ts
@@ -0,0 +1,18 @@
+import { HookConfig, HookType, IsmConfig, IsmType } from '@hyperlane-xyz/sdk';
+
+import { logGray } from '../logger.js';
+
+export function callWithConfigCreationLogs(
+ fn: (...args: any[]) => Promise,
+ type: IsmType | HookType,
+) {
+ return async (...args: any[]): Promise => {
+ logGray(`Creating ${type}...`);
+ try {
+ const result = await fn(...args);
+ return result;
+ } finally {
+ logGray(`Created ${type}!`);
+ }
+ };
+}
diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts
index d9ca9b4e5d..7bd71e7473 100644
--- a/typescript/cli/src/config/warp.ts
+++ b/typescript/cli/src/config/warp.ts
@@ -1,7 +1,10 @@
import { input, select } from '@inquirer/prompts';
+import { stringify as yamlStringify } from 'yaml';
import {
ChainMap,
+ IsmConfig,
+ IsmType,
MailboxClientConfig,
TokenType,
WarpCoreConfig,
@@ -9,15 +12,19 @@ import {
WarpRouteDeployConfig,
WarpRouteDeployConfigSchema,
} from '@hyperlane-xyz/sdk';
-import { assert, objMap, promiseObjAll } from '@hyperlane-xyz/utils';
+import { Address, assert, objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import { CommandContext } from '../context/types.js';
-import { errorRed, logBlue, logGreen } from '../logger.js';
+import { errorRed, log, logBlue, logGreen } from '../logger.js';
+import { runMultiChainSelectionStep } from '../utils/chains.js';
import {
- detectAndConfirmOrPrompt,
- runMultiChainSelectionStep,
-} from '../utils/chains.js';
-import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js';
+ indentYamlOrJson,
+ readYamlOrJson,
+ writeYamlOrJson,
+} from '../utils/files.js';
+import { detectAndConfirmOrPrompt } from '../utils/input.js';
+
+import { createAdvancedIsmConfig } from './ism.js';
const TYPE_DESCRIPTIONS: Record = {
[TokenType.synthetic]: 'A new ERC20 with remote transfer functionality',
@@ -94,16 +101,19 @@ export function isValidWarpRouteDeployConfig(config: any) {
export async function createWarpRouteDeployConfig({
context,
outPath,
+ advanced = false,
}: {
context: CommandContext;
outPath: string;
+ advanced: boolean;
}) {
- logBlue('Creating a new warp route deployment config');
+ logBlue('Creating a new warp route deployment config...');
const owner = await detectAndConfirmOrPrompt(
async () => context.signer?.getAddress(),
'Enter the desired',
'owner address',
+ 'signer',
);
const warpChains = await runMultiChainSelectionStep(
@@ -130,8 +140,13 @@ export async function createWarpRouteDeployConfig({
},
`For ${chain}, enter the`,
'mailbox address',
+ 'hyperlane-registry',
);
+ const interchainSecurityModule = advanced
+ ? await createAdvancedIsmConfig(context)
+ : createDefaultWarpIsmConfig(owner);
+
switch (type) {
case TokenType.collateral:
case TokenType.XERC20:
@@ -145,23 +160,32 @@ export async function createWarpRouteDeployConfig({
type,
owner,
isNft,
+ interchainSecurityModule,
token: await input({
message: `Enter the existing token address on chain ${chain}`,
}),
};
break;
default:
- result[chain] = { mailbox, type, owner, isNft };
+ result[chain] = {
+ mailbox,
+ type,
+ owner,
+ isNft,
+ interchainSecurityModule,
+ };
}
}
try {
- const parsed = WarpRouteDeployConfigSchema.parse(result);
- logGreen(`Warp Route config is valid, writing to file ${outPath}`);
- writeYamlOrJson(outPath, parsed);
+ const warpRouteDeployConfig = WarpRouteDeployConfigSchema.parse(result);
+ logBlue(`Warp Route config is valid, writing to file ${outPath}:\n`);
+ log(indentYamlOrJson(yamlStringify(warpRouteDeployConfig, null, 2), 4));
+ writeYamlOrJson(outPath, warpRouteDeployConfig, 'yaml');
+ logGreen('✅ Successfully created new warp route deployment config.');
} catch (e) {
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`,
+ `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.`,
);
throw e;
}
@@ -169,8 +193,34 @@ export async function createWarpRouteDeployConfig({
// 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 {
+export function readWarpCoreConfig(filePath: string): WarpCoreConfig {
const config = readYamlOrJson(filePath);
if (!config) throw new Error(`No warp route config found at ${filePath}`);
return WarpCoreConfigSchema.parse(config);
}
+
+/**
+ * Creates a default configuration for an ISM with a TRUSTED_RELAYER and FALLBACK_ROUTING.
+ *
+ * Properties relayer and owner are both set as input owner.
+ *
+ * @param owner - The address of the owner of the ISM.
+ * @returns The default Aggregation ISM configuration.
+ */
+function createDefaultWarpIsmConfig(owner: Address): IsmConfig {
+ return {
+ type: IsmType.AGGREGATION,
+ modules: [
+ {
+ type: IsmType.TRUSTED_RELAYER,
+ relayer: owner,
+ },
+ {
+ type: IsmType.FALLBACK_ROUTING,
+ domains: {},
+ owner,
+ },
+ ],
+ threshold: 1,
+ };
+}
diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts
index fba159a696..bc3258c3fe 100644
--- a/typescript/cli/src/context/context.ts
+++ b/typescript/cli/src/context/context.ts
@@ -56,7 +56,7 @@ export async function getContext({
const registry = getRegistry(registryUri, registryOverrideUri);
let signer: ethers.Wallet | undefined = undefined;
- if (requiresKey) {
+ if (key || requiresKey) {
({ key, signer } = await getSigner({ key, skipConfirmation }));
}
const multiProvider = await getMultiProvider(registry, signer);
@@ -99,9 +99,8 @@ export async function getDryRunContext(
logBlue(`Dry-running against chain: ${chain}`);
await verifyAnvil();
- const multiProvider = await getMultiProvider(registry);
- await forkNetworkToMultiProvider(multiProvider, chain);
-
+ let multiProvider = await getMultiProvider(registry);
+ multiProvider = await forkNetworkToMultiProvider(multiProvider, chain);
const { impersonatedKey, impersonatedSigner } = await getImpersonatedSigner({
fromAddress,
key,
diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts
index 8f8bf20439..7e585d4b97 100644
--- a/typescript/cli/src/deploy/core.ts
+++ b/typescript/cli/src/deploy/core.ts
@@ -1,461 +1,90 @@
-import { confirm } from '@inquirer/prompts';
-import { ethers } from 'ethers';
+import { stringify as yamlStringify } from 'yaml';
-import { ChainAddresses, IRegistry } from '@hyperlane-xyz/registry';
-import {
- ChainMap,
- ChainName,
- CoreConfig,
- GasOracleContractType,
- HooksConfig,
- HyperlaneAddressesMap,
- HyperlaneContractsMap,
- HyperlaneCore,
- HyperlaneCoreDeployer,
- HyperlaneIsmFactory,
- HyperlaneProxyFactoryDeployer,
- IgpConfig,
- IsmConfig,
- IsmType,
- MultisigConfig,
- RoutingIsmConfig,
- buildAgentConfig,
- buildAggregationIsmConfigs,
- defaultMultisigConfigs,
- multisigIsmVerificationCost,
- serializeContractsMap,
-} from '@hyperlane-xyz/sdk';
-import { Address, objFilter, objMap, objMerge } from '@hyperlane-xyz/utils';
+import { ChainName, CoreConfig, EvmCoreModule } from '@hyperlane-xyz/sdk';
-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 { WriteCommandContext } from '../context/types.js';
-import {
- log,
- logBlue,
- logBoldUnderlinedRed,
- logGray,
- logGreen,
- logRed,
-} from '../logger.js';
-import { runMultiChainSelectionStep } from '../utils/chains.js';
-import { runFileSelectionStep, writeJson } from '../utils/files.js';
+import { log, logBlue, logGreen } from '../logger.js';
+import { runSingleChainSelectionStep } from '../utils/chains.js';
+import { indentYamlOrJson } from '../utils/files.js';
import {
completeDeploy,
- isISMConfig,
- isZODISMConfig,
prepareDeploy,
+ runDeployPlanStep,
runPreflightChecksForChains,
} from './utils.js';
-const CONTRACT_CACHE_EXCLUSIONS = ['interchainGasPaymaster'];
-
+interface DeployParams {
+ context: WriteCommandContext;
+ chain: ChainName;
+ config: CoreConfig;
+}
/**
* Executes the core deploy command.
*/
export async function runCoreDeploy({
context,
- chains,
- ismConfigPath,
- hookConfigPath,
- agentOutPath,
+ chain,
+ config,
}: {
context: WriteCommandContext;
- chains?: ChainName[];
- ismConfigPath?: string;
- hookConfigPath?: string;
- agentOutPath: string;
+ chain: ChainName;
+ config: CoreConfig;
}) {
- 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(
+ const {
+ signer,
+ isDryRun,
+ chainMetadata,
+ dryRunChain,
+ registry,
+ skipConfirmation,
+ } = context;
+
+ // Select a dry-run chain if it's not supplied
+ if (dryRunChain) {
+ chain = dryRunChain;
+ } else if (!chain) {
+ if (skipConfirmation) throw new Error('No chain provided');
+ chain = await runSingleChainSelectionStep(
chainMetadata,
- 'Select chains to connect:',
- true,
+ 'Select chain to connect:',
);
}
-
- const result = await runIsmStep(chains, skipConfirmation, ismConfigPath);
- // we can either specify the full ISM config or just the multisig config
- const isIsmConfig = isISMConfig(result);
- const ismConfigs = isIsmConfig ? (result as ChainMap) : undefined;
- const multisigConfigs = isIsmConfig
- ? defaultMultisigConfigs
- : (result as ChainMap);
- const hooksConfig = await runHookStep(chains, hookConfigPath);
-
const deploymentParams: DeployParams = {
context,
- chains,
- ismConfigs,
- multisigConfigs,
- hooksConfig,
- agentOutPath,
+ chain,
+ config,
};
await runDeployPlanStep(deploymentParams);
await runPreflightChecksForChains({
...deploymentParams,
+ chains: [chain],
minGas: MINIMUM_CORE_DEPLOY_GAS,
});
const userAddress = await signer.getAddress();
- const initialBalances = await prepareDeploy(context, userAddress, chains);
-
- await executeDeploy(deploymentParams);
-
- await completeDeploy(context, 'core', initialBalances, userAddress, chains);
-}
-
-async function runIsmStep(
- selectedChains: ChainName[],
- skipConfirmation: boolean,
- ismConfigPath?: string,
-) {
- if (!ismConfigPath) {
- logBlue(
- '\n',
- 'Hyperlane instances requires an Interchain Security Module (ISM).',
- );
- logGray(
- 'Example config: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/cli/typescript/cli/examples/ism.yaml',
- );
- if (skipConfirmation) throw new Error('ISM config required');
- ismConfigPath = await runFileSelectionStep(
- './configs',
- 'ISM config',
- 'ism',
- );
- }
-
- const isAdvancedIsm = isZODISMConfig(ismConfigPath);
- // separate flow for 'ism' and 'ism-advanced' options
- if (isAdvancedIsm) {
- logBoldUnderlinedRed(
- 'WARNING: YOU ARE DEPLOYING WITH AN ADVANCED ISM CONFIG',
- );
- 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 ismConfig = readIsmConfig(ismConfigPath);
- const requiredIsms = objFilter(
- ismConfig,
- (chain, config): config is IsmConfig => selectedChains.includes(chain),
- );
- // selected chains - (user configs + default configs) = missing config
- const missingConfigs = selectedChains.filter(
- (c) => !Object.keys(ismConfig).includes(c),
- );
- if (missingConfigs.length > 0) {
- throw new Error(
- `Missing advanced ISM config for one or more chains: ${missingConfigs.join(
- ', ',
- )}`,
- );
- }
-
- log(`Found configs for chains: ${selectedChains.join(', ')}`);
- return requiredIsms as ChainMap;
- } else {
- const multisigConfigs = {
- ...defaultMultisigConfigs,
- ...readMultisigConfig(ismConfigPath),
- } as ChainMap;
- const requiredMultisigs = objFilter(
- multisigConfigs,
- (chain, config): config is MultisigConfig =>
- selectedChains.includes(chain),
- );
- // selected chains - (user configs + default configs) = missing config
- const missingConfigs = selectedChains.filter(
- (c) => !Object.keys(requiredMultisigs).includes(c),
- );
- if (missingConfigs.length > 0) {
- throw new Error(
- `Missing ISM config for one or more chains: ${missingConfigs.join(
- ', ',
- )}`,
- );
- }
-
- log(`Found configs for chains: ${selectedChains.join(', ')}`);
- return requiredMultisigs as ChainMap;
- }
-}
-
-async function runHookStep(
- _selectedChains: ChainName[],
- hookConfigPath?: string,
-) {
- if (!hookConfigPath) return {};
- return readHooksConfigMap(hookConfigPath);
-}
-
-interface DeployParams {
- context: WriteCommandContext;
- chains: ChainName[];
- ismConfigs?: ChainMap;
- multisigConfigs?: ChainMap;
- hooksConfig?: ChainMap;
- agentOutPath: string;
-}
-
-async function runDeployPlanStep({ context, chains }: DeployParams) {
- const { signer, skipConfirmation } = context;
- const address = await signer.getAddress();
-
- logBlue('\nDeployment plan');
- logGray('===============');
- 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 your provided registries will be skipped`,
- );
+ const initialBalances = await prepareDeploy(context, userAddress, [chain]);
- if (skipConfirmation) return;
- const isConfirmed = await confirm({
- message: 'Is this deployment plan correct?',
- });
- if (!isConfirmed) throw new Error('Deployment cancelled');
-}
-
-async function executeDeploy({
- context,
- chains,
- ismConfigs = {},
- multisigConfigs = {},
- hooksConfig = {},
- agentOutPath,
-}: DeployParams) {
logBlue('All systems ready, captain! Beginning deployment...');
- const { signer, multiProvider, registry } = context;
-
- let chainAddresses = await registry.getAddresses();
- chainAddresses = filterAddressesToCache(chainAddresses);
-
- const owner = await signer.getAddress();
- let artifacts: HyperlaneAddressesMap = {};
-
- // 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(chainAddresses);
-
- const ismFactoryConfig = chains.reduce((chainMap, curr) => {
- chainMap[curr] = {};
- return chainMap;
- }, {} as ChainMap<{}>);
- const ismFactoryContracts = await ismFactoryDeployer.deploy(ismFactoryConfig);
-
- artifacts = await updateChainAddresses(
- registry,
- ismFactoryContracts,
- artifacts,
- context.isDryRun,
- );
-
- 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(
- chainAddresses,
- multiProvider,
- );
- // 3. Construct ISM configs for all deployable chains
- const defaultIsms: ChainMap = {};
- for (const ismOrigin of chains) {
- defaultIsms[ismOrigin] =
- ismConfigs[ismOrigin] ??
- buildIsmConfig(owner, ismOrigin, chains, multisigConfigs);
- }
-
- // 4. Deploy core contracts to chains
- logBlue(`Deploying core contracts to ${chains.join(', ')}`);
- const coreDeployer = new HyperlaneCoreDeployer(multiProvider, ismFactory);
- coreDeployer.cacheAddressesMap(chainAddresses as any);
- const coreConfigs = buildCoreConfigMap(
- owner,
- chains,
- defaultIsms,
- hooksConfig,
- );
- const coreContracts = await coreDeployer.deploy(coreConfigs);
-
- // 4.5 recover the toplevel ISM address
- const isms: HyperlaneAddressesMap = {};
- for (const chain of chains) {
- isms[chain] = {
- interchainSecurityModule:
- coreDeployer.cachedAddresses[chain].interchainSecurityModule,
- };
- }
- artifacts = objMerge(artifacts, isms);
- artifacts = await updateChainAddresses(
- registry,
- coreContracts,
- artifacts,
- context.isDryRun,
- );
- logGreen('✅ Core contracts deployed');
- log(JSON.stringify(artifacts, null, 2));
-
- await writeAgentConfig(context, artifacts, chains, agentOutPath);
-
- logBlue('Deployment is complete!');
-}
-
-function filterAddressesToCache(addressesMap: ChainMap) {
- // 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(
- owner: Address,
- local: ChainName,
- chains: ChainName[],
- multisigIsmConfigs: ChainMap,
-): RoutingIsmConfig {
- const aggregationIsmConfigs = buildAggregationIsmConfigs(
- local,
- chains,
- multisigIsmConfigs,
- );
- return {
- owner,
- type: IsmType.ROUTING,
- domains: aggregationIsmConfigs,
- };
-}
-
-function buildCoreConfigMap(
- owner: Address,
- chains: ChainName[],
- defaultIsms: ChainMap,
- hooksConfig: ChainMap,
-): ChainMap {
- return chains.reduce>((config, chain) => {
- const hooks = hooksConfig[chain] ?? presetHookConfigs(owner);
- config[chain] = {
- owner,
- defaultIsm: defaultIsms[chain],
- defaultHook: hooks.default,
- requiredHook: hooks.required,
- };
- return config;
- }, {});
-}
-
-export function buildIgpConfigMap(
- owner: Address,
- chains: ChainName[],
- multisigConfigs: ChainMap,
-): ChainMap {
- const configMap: ChainMap = {};
- for (const chain of chains) {
- const overhead: ChainMap = {};
- const gasOracleType: ChainMap = {};
- for (const remote of chains) {
- if (chain === remote) continue;
- // TODO: accurate estimate of gas from ChainMap
- const threshold = multisigConfigs[remote]
- ? multisigConfigs[remote].threshold
- : 2;
- const validatorsLength = multisigConfigs[remote]
- ? multisigConfigs[remote].validators.length
- : 3;
- overhead[remote] = multisigIsmVerificationCost(
- threshold,
- validatorsLength,
- );
- gasOracleType[remote] = GasOracleContractType.StorageGasOracle;
- }
- configMap[chain] = {
- owner,
- beneficiary: owner,
- gasOracleType,
- overhead,
- oracleKey: owner,
- };
- }
- return configMap;
-}
-
-async function updateChainAddresses(
- registry: IRegistry,
- newContracts: HyperlaneContractsMap,
- otherAddresses: HyperlaneAddressesMap,
- isDryRun?: boolean,
-) {
- 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 evmCoreModule = await EvmCoreModule.create({
+ chain,
+ config,
+ multiProvider: context.multiProvider,
});
- const mergedAddresses = objMerge(otherAddresses, newAddresses);
- if (isDryRun) return mergedAddresses;
+ await completeDeploy(context, 'core', initialBalances, userAddress, [chain]);
+ const deployedAddresses = evmCoreModule.serialize();
- for (const chainName of Object.keys(newContracts)) {
+ if (!isDryRun) {
await registry.updateChain({
- chainName,
- addresses: mergedAddresses[chainName],
+ chainName: chain,
+ addresses: deployedAddresses,
});
}
- return mergedAddresses;
-}
-async function writeAgentConfig(
- context: WriteCommandContext,
- artifacts: HyperlaneAddressesMap,
- chains: ChainName[],
- outPath: string,
-) {
- if (context.isDryRun) return;
- log('Writing agent configs');
- const { multiProvider, registry } = context;
- const startBlocks: ChainMap = {};
- const core = HyperlaneCore.fromAddressesMap(artifacts, multiProvider);
-
- for (const chain of chains) {
- const mailbox = core.getContracts(chain).mailbox;
- startBlocks[chain] = (await mailbox.deployedBlock()).toNumber();
- }
-
- const chainAddresses = await registry.getAddresses();
- for (const chain of chains) {
- if (!chainAddresses[chain].interchainGasPaymaster) {
- chainAddresses[chain].interchainGasPaymaster =
- ethers.constants.AddressZero;
- }
- }
- const agentConfig = buildAgentConfig(
- chains, // Use only the chains that were deployed to
- multiProvider,
- chainAddresses as any,
- startBlocks,
- );
- writeJson(outPath, agentConfig);
- logGreen('Agent configs written');
+ logGreen('✅ Core contract deployments complete:\n');
+ log(indentYamlOrJson(yamlStringify(deployedAddresses, null, 2), 4));
}
diff --git a/typescript/cli/src/deploy/dry-run.ts b/typescript/cli/src/deploy/dry-run.ts
index 11cbe379bf..f821dc179f 100644
--- a/typescript/cli/src/deploy/dry-run.ts
+++ b/typescript/cli/src/deploy/dry-run.ts
@@ -21,10 +21,11 @@ export async function forkNetworkToMultiProvider(
chain: string,
) {
multiProvider = multiProvider.extendChainMetadata({
- [chain]: { blocks: { confirmations: 0 } },
+ [chain]: { blocks: { confirmations: 1 } },
});
await setFork(multiProvider, chain);
+ return multiProvider;
}
/**
diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts
index f1082b3013..2d92db996f 100644
--- a/typescript/cli/src/deploy/utils.ts
+++ b/typescript/cli/src/deploy/utils.ts
@@ -1,7 +1,9 @@
+import { confirm } from '@inquirer/prompts';
import { BigNumber, ethers } from 'ethers';
import {
ChainMap,
+ ChainMetadata,
ChainName,
IsmConfig,
MultisigConfig,
@@ -11,7 +13,14 @@ import { Address, ProtocolType } from '@hyperlane-xyz/utils';
import { parseIsmConfig } from '../config/ism.js';
import { WriteCommandContext } from '../context/types.js';
-import { log, logGreen, logPink } from '../logger.js';
+import {
+ log,
+ logBlue,
+ logGray,
+ logGreen,
+ logPink,
+ logTable,
+} from '../logger.js';
import { gasBalancesAreSufficient } from '../utils/balances.js';
import { ENV } from '../utils/env.js';
import { assertSigner } from '../utils/keys.js';
@@ -55,6 +64,35 @@ export async function runPreflightChecksForChains({
if (sufficient) logGreen('✅ Balances are sufficient');
}
+export async function runDeployPlanStep({
+ context,
+ chain,
+}: {
+ context: WriteCommandContext;
+ chain: ChainName;
+}) {
+ const { signer, chainMetadata: chainMetadataMap, skipConfirmation } = context;
+ const address = await signer.getAddress();
+
+ logBlue('\nDeployment plan');
+ logGray('===============');
+ log(`Transaction signer and owner of new contracts: ${address}`);
+ log(`Deploying core contracts to network: ${chain}`);
+ const transformedChainMetadata = transformChainMetadataForDisplay(
+ chainMetadataMap[chain],
+ );
+ logTable(transformedChainMetadata);
+ log(
+ `Note: There are several contracts required for each chain, but contracts in your provided registries will be skipped.`,
+ );
+
+ if (skipConfirmation) return;
+ const isConfirmed = await confirm({
+ message: 'Is this deployment plan correct?',
+ });
+ if (!isConfirmed) throw new Error('Deployment cancelled');
+}
+
// from parsed types
export function isISMConfig(
config: ChainMap | ChainMap,
@@ -106,7 +144,7 @@ export async function completeDeploy(
`\t- Gas required for ${command} ${
isDryRun ? 'dry-run' : 'deploy'
} on ${chain}: ${ethers.utils.formatEther(balanceDelta)} ${
- multiProvider.getChainMetadata(chain).nativeToken?.symbol
+ multiProvider.getChainMetadata(chain).nativeToken?.symbol ?? 'ETH'
}`,
);
}
@@ -117,3 +155,17 @@ export async function completeDeploy(
export function toUpperCamelCase(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
+
+function transformChainMetadataForDisplay(chainMetadata: ChainMetadata) {
+ return {
+ Name: chainMetadata.name,
+ 'Display Name': chainMetadata.displayName,
+ 'Chain ID': chainMetadata.chainId,
+ 'Domain ID': chainMetadata.domainId,
+ Protocol: chainMetadata.protocol,
+ 'JSON RPC URL': chainMetadata.rpcUrls[0].http,
+ 'Native Token: Symbol': chainMetadata.nativeToken?.symbol,
+ 'Native Token: Name': chainMetadata.nativeToken?.name,
+ 'Native Token: Decimals': chainMetadata.nativeToken?.decimals,
+ };
+}
diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts
index dfcd7a2c22..58758eac87 100644
--- a/typescript/cli/src/deploy/warp.ts
+++ b/typescript/cli/src/deploy/warp.ts
@@ -1,13 +1,15 @@
import { confirm } from '@inquirer/prompts';
+import { stringify as yamlStringify } from 'yaml';
+import { IRegistry } from '@hyperlane-xyz/registry';
import {
- HypXERC20Lockbox__factory,
- HypXERC20__factory,
-} from '@hyperlane-xyz/core';
-import {
+ EvmIsmModule,
HypERC20Deployer,
HypERC721Deployer,
+ HyperlaneAddresses,
HyperlaneContractsMap,
+ HyperlaneProxyFactoryDeployer,
+ MultiProvider,
TOKEN_TYPE_TO_STANDARD,
TokenFactories,
TokenType,
@@ -15,14 +17,19 @@ import {
WarpRouteDeployConfig,
getTokenConnectionId,
isTokenMetadata,
+ serializeContracts,
} from '@hyperlane-xyz/sdk';
-import { ProtocolType } from '@hyperlane-xyz/utils';
+import { ProtocolType, objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import { readWarpRouteDeployConfig } from '../config/warp.js';
import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js';
import { WriteCommandContext } from '../context/types.js';
import { log, logBlue, logGray, logGreen, logTable } from '../logger.js';
-import { isFile, runFileSelectionStep } from '../utils/files.js';
+import {
+ indentYamlOrJson,
+ isFile,
+ runFileSelectionStep,
+} from '../utils/files.js';
import {
completeDeploy,
@@ -123,17 +130,124 @@ async function executeDeploy(params: DeployParams) {
? { [dryRunChain]: configMap[dryRunChain] }
: configMap;
- const deployedContracts = await deployer.deploy(config);
+ const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
- logGreen('✅ Warp contract deployments complete');
+ // For each chain in WarpRouteConfig, deploy each Ism Factory, if it's not in the registry
+ // Then return a modified config with the ism address as a string
+ const modifiedConfig = await deployAndResolveWarpIsm(
+ config,
+ multiProvider,
+ registry,
+ ismFactoryDeployer,
+ );
+
+ const deployedContracts = await deployer.deploy(modifiedConfig);
const warpCoreConfig = await getWarpCoreConfig(params, deployedContracts);
+ logGreen('✅ Warp contract deployments complete');
+
if (!isDryRun) {
- log('Writing deployment artifacts');
+ log('Writing deployment artifacts...');
await registry.addWarpRoute(warpCoreConfig);
}
- log(JSON.stringify(warpCoreConfig, null, 2));
- logBlue('Deployment is complete!');
+ log(indentYamlOrJson(yamlStringify(warpCoreConfig, null, 2), 4));
+}
+
+async function deployAndResolveWarpIsm(
+ warpConfig: WarpRouteDeployConfig,
+ multiProvider: MultiProvider,
+ registry: IRegistry,
+ ismFactoryDeployer: HyperlaneProxyFactoryDeployer,
+): Promise {
+ return promiseObjAll(
+ objMap(warpConfig, async (chain, config) => {
+ // Skip deployment if Ism is empty, or a string
+ if (
+ !config.interchainSecurityModule ||
+ typeof config.interchainSecurityModule === 'string'
+ ) {
+ logGray(
+ `Config Ism is ${
+ !config.interchainSecurityModule
+ ? 'empty'
+ : config.interchainSecurityModule
+ }, skipping deployment`,
+ );
+ return config;
+ }
+
+ logBlue('Loading Registry factory addresses');
+ let chainAddresses = await registry.getChainAddresses(chain); // Can includes other addresses
+
+ if (!chainAddresses) {
+ logGray('Registry factory addresses not found, deploying');
+ chainAddresses = serializeContracts(
+ await ismFactoryDeployer.deployContracts(chain),
+ ) as Record;
+ }
+
+ logGray(
+ `Creating ${config.interchainSecurityModule.type} Ism for ${config.type} token on ${chain} chain`,
+ );
+
+ const deployedIsm = await createWarpIsm(
+ chain,
+ warpConfig,
+ multiProvider,
+ {
+ domainRoutingIsmFactory: chainAddresses.domainRoutingIsmFactory,
+ staticAggregationHookFactory:
+ chainAddresses.staticAggregationHookFactory,
+ staticAggregationIsmFactory:
+ chainAddresses.staticAggregationIsmFactory,
+ staticMerkleRootMultisigIsmFactory:
+ chainAddresses.staticMerkleRootMultisigIsmFactory,
+ staticMessageIdMultisigIsmFactory:
+ chainAddresses.staticMessageIdMultisigIsmFactory,
+ },
+ );
+
+ logGreen(
+ `Finished creating ${config.interchainSecurityModule.type} Ism for ${config.type} token on ${chain} chain`,
+ );
+ return { ...warpConfig[chain], interchainSecurityModule: deployedIsm };
+ }),
+ );
+}
+
+/**
+ * Deploys the Warp ISM for a given config
+ *
+ * @returns The deployed ism address
+ */
+async function createWarpIsm(
+ chain: string,
+ warpConfig: WarpRouteDeployConfig,
+ multiProvider: MultiProvider,
+ factoryAddresses: HyperlaneAddresses,
+): Promise {
+ const {
+ domainRoutingIsmFactory,
+ staticAggregationHookFactory,
+ staticAggregationIsmFactory,
+ staticMerkleRootMultisigIsmFactory,
+ staticMessageIdMultisigIsmFactory,
+ } = factoryAddresses;
+ const evmIsmModule = await EvmIsmModule.create({
+ chain,
+ multiProvider,
+ mailbox: warpConfig[chain].mailbox,
+ proxyFactoryFactories: {
+ domainRoutingIsmFactory,
+ staticAggregationHookFactory,
+ staticAggregationIsmFactory,
+ staticMerkleRootMultisigIsmFactory,
+ staticMessageIdMultisigIsmFactory,
+ },
+ config: warpConfig[chain].interchainSecurityModule!,
+ });
+ const { deployedIsm } = evmIsmModule.serialize();
+ return deployedIsm;
}
async function getWarpCoreConfig(
@@ -165,30 +279,8 @@ async function getWarpCoreConfig(
throw new Error('Missing decimals on token metadata');
}
- const collateralAddressOrDenom = await (async () => {
- if (config.type === TokenType.XERC20Lockbox) {
- const provider = context.multiProvider.tryGetProvider(chainName);
- if (!provider) {
- throw new Error(`Unable to pull provider for ${chainName}`);
- }
-
- const xERC20 = await HypXERC20Lockbox__factory.connect(
- config.token,
- provider,
- ).xERC20();
- const wrappedToken = await HypXERC20__factory.connect(
- xERC20,
- provider,
- ).wrappedToken();
- return wrappedToken;
- }
-
- if (config.type === TokenType.collateral) {
- return config.token;
- }
-
- return undefined;
- })();
+ const collateralAddressOrDenom =
+ config.type === TokenType.collateral ? config.token : undefined;
warpCoreConfig.tokens.push({
chainName,
standard: TOKEN_TYPE_TO_STANDARD[config.type],
diff --git a/typescript/cli/src/send/message.ts b/typescript/cli/src/send/message.ts
index 102592bbc8..ca303f6ebc 100644
--- a/typescript/cli/src/send/message.ts
+++ b/typescript/cli/src/send/message.ts
@@ -1,4 +1,4 @@
-import { ethers } from 'ethers';
+import { stringify as yamlStringify } from 'yaml';
import { ChainName, HyperlaneCore } from '@hyperlane-xyz/sdk';
import { addressToBytes32, timeout } from '@hyperlane-xyz/utils';
@@ -8,6 +8,7 @@ import { CommandContext, WriteCommandContext } from '../context/types.js';
import { runPreflightChecksForChains } from '../deploy/utils.js';
import { errorRed, log, logBlue, logGreen } from '../logger.js';
import { runSingleChainSelectionStep } from '../utils/chains.js';
+import { indentYamlOrJson } from '../utils/files.js';
export async function sendTestMessage({
context,
@@ -81,18 +82,12 @@ async function executeDelivery({
const { registry, multiProvider } = context;
const chainAddresses = await registry.getAddresses();
const core = HyperlaneCore.fromAddressesMap(chainAddresses, multiProvider);
- const mailbox = core.getContracts(origin).mailbox;
- let hook = chainAddresses[origin]?.customHook;
+ const hook = chainAddresses[origin]?.customHook;
if (hook) {
logBlue(`Using custom hook ${hook} for ${origin} -> ${destination}`);
- } else {
- hook = await mailbox.defaultHook();
- logBlue(`Using default hook ${hook} for ${origin} -> ${destination}`);
}
- const destinationDomain = multiProvider.getDomainId(destination);
- let txReceipt: ethers.ContractReceipt;
try {
const recipient = chainAddresses[destination].testRecipient;
if (!recipient) {
@@ -100,42 +95,32 @@ async function executeDelivery({
}
const formattedRecipient = addressToBytes32(recipient);
- log('Getting gas quote');
- const value = await mailbox[
- 'quoteDispatch(uint32,bytes32,bytes,bytes,address)'
- ](
- destinationDomain,
- formattedRecipient,
- messageBody,
- ethers.utils.hexlify([]),
- hook,
- );
- log(`Paying for gas with ${value} wei`);
-
log('Dispatching message');
- const messageTx = await mailbox[
- 'dispatch(uint32,bytes32,bytes,bytes,address)'
- ](
- destinationDomain,
+ const { dispatchTx, message } = await core.sendMessage(
+ origin,
+ destination,
formattedRecipient,
messageBody,
- ethers.utils.hexlify([]),
hook,
- {
- value,
- },
+ undefined,
);
- txReceipt = await multiProvider.handleTx(origin, messageTx);
- const message = core.getDispatchedMessages(txReceipt)[0];
logBlue(`Sent message from ${origin} to ${recipient} on ${destination}.`);
logBlue(`Message ID: ${message.id}`);
- log(`Message: ${JSON.stringify(message)}`);
+ log(`Message:\n${indentYamlOrJson(yamlStringify(message, null, 2), 4)}`);
if (selfRelay) {
log('Attempting self-relay of message');
- await core.relayMessage(message);
+ await core.relayMessage(message, dispatchTx);
logGreen('Message was self-relayed!');
- return;
+ } else {
+ if (skipWaitForDelivery) {
+ return;
+ }
+
+ log('Waiting for message delivery on destination chain...');
+ // Max wait 10 minutes
+ await core.waitForMessageProcessed(dispatchTx, 10000, 60);
+ logGreen('Message was delivered!');
}
} catch (e) {
errorRed(
@@ -143,11 +128,4 @@ async function executeDelivery({
);
throw e;
}
-
- if (skipWaitForDelivery) return;
-
- log('Waiting for message delivery on destination chain...');
- // Max wait 10 minutes
- await core.waitForMessageProcessed(txReceipt, 10000, 60);
- logGreen('Message was delivered!');
}
diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts
index 23cd5ba521..2df762b88d 100644
--- a/typescript/cli/src/send/transfer.ts
+++ b/typescript/cli/src/send/transfer.ts
@@ -10,7 +10,6 @@ import {
} from '@hyperlane-xyz/sdk';
import { timeout } from '@hyperlane-xyz/utils';
-import { readWarpRouteConfig } from '../config/warp.js';
import { MINIMUM_TEST_SEND_GAS } from '../consts.js';
import { WriteCommandContext } from '../context/types.js';
import { runPreflightChecksForChains } from '../deploy/utils.js';
@@ -20,20 +19,20 @@ import { runTokenSelectionStep } from '../utils/tokens.js';
export async function sendTestTransfer({
context,
- warpConfigPath,
+ warpCoreConfig,
origin,
destination,
- wei,
+ amount,
recipient,
timeoutSec,
skipWaitForDelivery,
selfRelay,
}: {
context: WriteCommandContext;
- warpConfigPath: string;
+ warpCoreConfig: WarpCoreConfig;
origin?: ChainName;
destination?: ChainName;
- wei: string;
+ amount: string;
recipient?: string;
timeoutSec: number;
skipWaitForDelivery: boolean;
@@ -41,8 +40,6 @@ export async function sendTestTransfer({
}) {
const { chainMetadata } = context;
- const warpCoreConfig = readWarpRouteConfig(warpConfigPath);
-
if (!origin) {
origin = await runSingleChainSelectionStep(
chainMetadata,
@@ -70,7 +67,7 @@ export async function sendTestTransfer({
origin,
destination,
warpCoreConfig,
- wei,
+ amount,
recipient,
skipWaitForDelivery,
selfRelay,
@@ -85,7 +82,7 @@ async function executeDelivery({
origin,
destination,
warpCoreConfig,
- wei,
+ amount,
recipient,
skipWaitForDelivery,
selfRelay,
@@ -94,7 +91,7 @@ async function executeDelivery({
origin: ChainName;
destination: ChainName;
warpCoreConfig: WarpCoreConfig;
- wei: string;
+ amount: string;
recipient?: string;
skipWaitForDelivery: boolean;
selfRelay?: boolean;
@@ -131,7 +128,7 @@ async function executeDelivery({
const senderAddress = await signer.getAddress();
const errors = await warpCore.validateTransfer({
- originTokenAmount: token.amount(wei),
+ originTokenAmount: token.amount(amount),
destination,
recipient: recipient ?? senderAddress,
sender: senderAddress,
@@ -142,7 +139,7 @@ async function executeDelivery({
}
const transferTxs = await warpCore.getTransferRemoteTxs({
- originTokenAmount: new TokenAmount(wei, token),
+ originTokenAmount: new TokenAmount(amount, token),
destination,
sender: senderAddress,
recipient: recipient ?? senderAddress,
@@ -164,7 +161,7 @@ async function executeDelivery({
logBlue(`Message ID: ${message.id}`);
if (selfRelay) {
- await core.relayMessage(message);
+ await core.relayMessage(message, transferTxReceipt);
logGreen('Message was self-relayed!');
return;
}
diff --git a/typescript/cli/src/status/message.ts b/typescript/cli/src/status/message.ts
index 77118b6de0..d7b36efd17 100644
--- a/typescript/cli/src/status/message.ts
+++ b/typescript/cli/src/status/message.ts
@@ -57,7 +57,7 @@ export async function checkMessageStatus({
const receipt = await core.getDispatchTx(origin, messageId);
const messages = core.getDispatchedMessages(receipt);
- await core.relayMessage(messages[0]);
+ await core.relayMessage(messages[0], receipt);
logGreen(`Message ${messageId} was self-relayed!`);
}
}
diff --git a/typescript/cli/src/submit/submit.ts b/typescript/cli/src/submit/submit.ts
index cc344c2f0b..fcde4afa4d 100644
--- a/typescript/cli/src/submit/submit.ts
+++ b/typescript/cli/src/submit/submit.ts
@@ -75,7 +75,7 @@ async function getTransformer(
transformerMetadata: TransformerMetadata,
): Promise> {
switch (transformerMetadata.type) {
- case TxTransformerType.ICA:
+ case TxTransformerType.INTERCHAIN_ACCOUNT:
return new EV5InterchainAccountTxTransformer(
multiProvider,
transformerMetadata.props,
diff --git a/typescript/cli/src/tests/hooks.test.ts b/typescript/cli/src/tests/hooks.test.ts
index 2848c0c061..3cb15d84d9 100644
--- a/typescript/cli/src/tests/hooks.test.ts
+++ b/typescript/cli/src/tests/hooks.test.ts
@@ -1,19 +1,14 @@
import { expect } from 'chai';
-import {
- ChainMap,
- GasOracleContractType,
- HookType,
- HooksConfig,
-} from '@hyperlane-xyz/sdk';
+import { HookType } from '@hyperlane-xyz/sdk';
-import { readHooksConfigMap } from '../config/hooks.js';
+import { HooksConfigMap, readHooksConfigMap } from '../config/hooks.js';
describe('readHooksConfigMap', () => {
it('parses and validates example correctly', () => {
const hooks = readHooksConfigMap('examples/hooks.yaml');
- const exampleHooksConfig: ChainMap = {
+ const exampleHooksConfig: HooksConfigMap = {
anvil1: {
required: {
type: HookType.PROTOCOL_FEE,
@@ -37,10 +32,13 @@ describe('readHooksConfigMap', () => {
beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
- gasOracleType: {
- anvil2: GasOracleContractType.StorageGasOracle,
- },
overhead: { anvil2: 50000 },
+ oracleConfig: {
+ anvil2: {
+ gasPrice: '100',
+ tokenExchangeRate: '100',
+ },
+ },
},
],
},
@@ -70,10 +68,13 @@ describe('readHooksConfigMap', () => {
beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
- gasOracleType: {
- anvil1: GasOracleContractType.StorageGasOracle,
- },
overhead: { anvil1: 50000 },
+ oracleConfig: {
+ anvil1: {
+ gasPrice: '100',
+ tokenExchangeRate: '100',
+ },
+ },
},
],
},
@@ -87,6 +88,6 @@ describe('readHooksConfigMap', () => {
it('parsing failure, missing internal key "overhead"', () => {
expect(() => {
readHooksConfigMap('src/tests/hooks/safe-parse-fail.yaml');
- }).to.throw('Invalid hook config: anvil2,default => Invalid input');
+ }).to.throw();
});
});
diff --git a/typescript/cli/src/tests/hooks/safe-parse-fail.yaml b/typescript/cli/src/tests/hooks/safe-parse-fail.yaml
index 4a2a5cedbf..7257817d47 100644
--- a/typescript/cli/src/tests/hooks/safe-parse-fail.yaml
+++ b/typescript/cli/src/tests/hooks/safe-parse-fail.yaml
@@ -19,8 +19,6 @@ anvil1:
oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
overhead:
anvil2: 50000
- gasOracleType:
- anvil2: StorageGasOracle
anvil2:
required:
type: protocolFee
@@ -40,5 +38,3 @@ anvil2:
beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
- gasOracleType:
- anvil1: StorageGasOracle
diff --git a/typescript/cli/src/tests/ism.test.ts b/typescript/cli/src/tests/ism.test.ts
index 4942963cf1..04d84caa92 100644
--- a/typescript/cli/src/tests/ism.test.ts
+++ b/typescript/cli/src/tests/ism.test.ts
@@ -80,6 +80,8 @@ describe('readIsmConfig', () => {
it('parsing failure, threshold > modules.length', () => {
expect(function () {
readIsmConfig('src/tests/ism/threshold-gt-modules-length-fail.yaml');
- }).to.throw('Threshold cannot be greater than number of modules');
+ }).to.throw(
+ 'Threshold must be less than or equal to the number of modules',
+ );
});
});
diff --git a/typescript/cli/src/utils/chains.ts b/typescript/cli/src/utils/chains.ts
index 3c0e7477d6..b22f7683a7 100644
--- a/typescript/cli/src/utils/chains.ts
+++ b/typescript/cli/src/utils/chains.ts
@@ -1,4 +1,4 @@
-import { Separator, checkbox, confirm, input } from '@inquirer/prompts';
+import { Separator, checkbox } from '@inquirer/prompts';
import select from '@inquirer/select';
import chalk from 'chalk';
@@ -6,6 +6,8 @@ import { ChainMap, ChainMetadata } from '@hyperlane-xyz/sdk';
import { log, logRed, logTip } from '../logger.js';
+import { calculatePageSize } from './cli-options.js';
+
// A special value marker to indicate user selected
// a new chain in the list
const NEW_CHAIN_MARKER = '__new__';
@@ -18,7 +20,7 @@ export async function runSingleChainSelectionStep(
const chain = (await select({
message,
choices,
- pageSize: 30,
+ pageSize: calculatePageSize(2),
})) as string;
handleNewChain([chain]);
return chain;
@@ -35,7 +37,7 @@ export async function runMultiChainSelectionStep(
const chains = (await checkbox({
message,
choices,
- pageSize: 30,
+ pageSize: calculatePageSize(2),
})) as string[];
handleNewChain(chains);
if (requireMultiple && chains?.length < 2) {
@@ -73,24 +75,3 @@ function handleNewChain(chainNames: string[]) {
process.exit(0);
}
}
-
-export async function detectAndConfirmOrPrompt(
- detect: () => Promise,
- prompt: string,
- label: string,
-): Promise {
- let detectedValue: string | undefined;
- try {
- detectedValue = await detect();
- if (detectedValue) {
- const confirmed = await confirm({
- message: `Detected ${label} as ${detectedValue}, is this correct?`,
- });
- if (confirmed) {
- return detectedValue;
- }
- }
- // eslint-disable-next-line no-empty
- } catch (e) {}
- return input({ message: `${prompt} ${label}`, default: detectedValue });
-}
diff --git a/typescript/cli/src/utils/cli-options.ts b/typescript/cli/src/utils/cli-options.ts
new file mode 100644
index 0000000000..415452e7eb
--- /dev/null
+++ b/typescript/cli/src/utils/cli-options.ts
@@ -0,0 +1,17 @@
+// Functions used to manipulate CLI specific options
+
+/**
+ * Calculates the page size for a CLI Terminal output, taking into account the number of lines to skip and a default page size.
+ *
+ * @param skipSize - The number of lines to skip, which can be used to skip previous prompts.
+ * @param defaultPageSize - The default page size to use if the terminal height is too small.
+ * @returns The calculated pageSize, which is the terminal height minus the skip size, or the default page size if the terminal height is too small.
+ */
+export function calculatePageSize(
+ skipSize: number = 0,
+ defaultPageSize: number = 15,
+) {
+ return process.stdout.rows > skipSize
+ ? process.stdout.rows - skipSize
+ : defaultPageSize;
+}
diff --git a/typescript/cli/src/utils/input.ts b/typescript/cli/src/utils/input.ts
new file mode 100644
index 0000000000..0f8c9ef66e
--- /dev/null
+++ b/typescript/cli/src/utils/input.ts
@@ -0,0 +1,55 @@
+import { confirm, input } from '@inquirer/prompts';
+
+import { logGray } from '../logger.js';
+
+import { indentYamlOrJson } from './files.js';
+
+export async function detectAndConfirmOrPrompt(
+ detect: () => Promise,
+ prompt: string,
+ label: string,
+ source?: string,
+): Promise {
+ let detectedValue: string | undefined;
+ try {
+ detectedValue = await detect();
+ if (detectedValue) {
+ const confirmed = await confirm({
+ message: `Detected ${label} as ${detectedValue}${
+ source ? ` from ${source}` : ''
+ }, is this correct?`,
+ });
+ if (confirmed) {
+ return detectedValue;
+ }
+ }
+ // eslint-disable-next-line no-empty
+ } catch (e) {}
+ return input({ message: `${prompt} ${label}:`, default: detectedValue });
+}
+
+const INFO_COMMAND: string = 'i';
+const DOCS_NOTICE: string =
+ 'For more information, please visit https://docs.hyperlane.xyz.';
+
+export async function inputWithInfo({
+ message,
+ info = 'No additional information available.',
+ defaultAnswer,
+}: {
+ message: string;
+ info?: string;
+ defaultAnswer?: string;
+}): Promise {
+ let answer: string = '';
+ do {
+ answer = await input({
+ message: message.concat(` [enter '${INFO_COMMAND}' for more info]`),
+ default: defaultAnswer,
+ });
+ answer = answer.trim().toLowerCase();
+ const indentedInfo = indentYamlOrJson(`${info}\n${DOCS_NOTICE}\n`, 4);
+ if (answer === INFO_COMMAND) logGray(indentedInfo);
+ } while (answer === INFO_COMMAND);
+ return answer;
+}
diff --git a/typescript/cli/src/utils/keys.ts b/typescript/cli/src/utils/keys.ts
index a7d2abb261..552b8d53d9 100644
--- a/typescript/cli/src/utils/keys.ts
+++ b/typescript/cli/src/utils/keys.ts
@@ -68,7 +68,7 @@ async function addressToImpersonatedSigner(
if (address.length != ETHEREUM_ADDRESS_LENGTH)
throw new Error('Invalid address length.');
else if (ethers.utils.isHexString(ensure0x(formattedKey)))
- return await impersonateAccount(address);
+ return impersonateAccount(address);
else throw new Error('Invalid address format');
}
@@ -93,7 +93,7 @@ async function retrieveKey(
): Promise {
if (skipConfirmation) throw new Error(`No private key provided`);
else
- return await input({
+ return input({
message: `Please enter private key or use the HYP_KEY environment variable.`,
});
}
diff --git a/typescript/cli/src/utils/tokens.ts b/typescript/cli/src/utils/tokens.ts
index ed876238cd..b31a2ddf37 100644
--- a/typescript/cli/src/utils/tokens.ts
+++ b/typescript/cli/src/utils/tokens.ts
@@ -1,6 +1,9 @@
import select from '@inquirer/select';
-import { Token } from '@hyperlane-xyz/sdk';
+import { IRegistry } from '@hyperlane-xyz/registry';
+import { Token, WarpCoreConfig } from '@hyperlane-xyz/sdk';
+
+import { logGreen, logRed } from '../logger.js';
export async function runTokenSelectionStep(
tokens: Token[],
@@ -17,3 +20,32 @@ export async function runTokenSelectionStep(
})) as string;
return routerAddress;
}
+
+export async function selectRegistryWarpRoute(
+ registry: IRegistry,
+ symbol: string,
+): Promise {
+ const matching = await registry.getWarpRoutes({
+ symbol,
+ });
+ const routes = Object.entries(matching);
+
+ let warpCoreConfig: WarpCoreConfig;
+ if (routes.length === 0) {
+ logRed(`No warp routes found for symbol ${symbol}`);
+ process.exit(0);
+ } else if (routes.length === 1) {
+ warpCoreConfig = routes[0][1];
+ } else {
+ logGreen(`Multiple warp routes found for symbol ${symbol}`);
+ const chosenRouteId = await select({
+ message: 'Select from matching warp routes',
+ choices: routes.map(([routeId, _]) => ({
+ value: routeId,
+ })),
+ });
+ warpCoreConfig = matching[chosenRouteId];
+ }
+
+ return warpCoreConfig;
+}
diff --git a/typescript/cli/src/validator/address.ts b/typescript/cli/src/validator/address.ts
index d816fcb1f5..cc13738db7 100644
--- a/typescript/cli/src/validator/address.ts
+++ b/typescript/cli/src/validator/address.ts
@@ -24,7 +24,7 @@ export async function getValidatorAddress({
region?: string;
bucket?: string;
keyId?: string;
-}) {
+}): Promise {
if (!bucket && !keyId) {
throw new Error('Must provide either an S3 bucket or a KMS Key ID.');
}
@@ -38,7 +38,7 @@ export async function getValidatorAddress({
assert(secretKey, 'No secret access key set.');
assert(region, 'No AWS region set.');
- let validatorAddress;
+ let validatorAddress: string;
if (bucket) {
validatorAddress = await getAddressFromBucket(
bucket,
@@ -68,7 +68,7 @@ async function getAddressFromBucket(
accessKeyId: string,
secretAccessKey: string,
region: string,
-) {
+): Promise {
const s3Client = new S3Client({
region: region,
credentials: {
@@ -101,7 +101,7 @@ async function getAddressFromKey(
accessKeyId: string,
secretAccessKey: string,
region: string,
-) {
+): Promise {
const client = new KMSClient({
region: region,
credentials: {
@@ -138,28 +138,28 @@ function getEthereumAddress(publicKey: Buffer): string {
return `0x${address.slice(-40)}`; // take last 20 bytes as ethereum address
}
-async function getAccessKeyId(skipConfirmation: boolean) {
+async function getAccessKeyId(skipConfirmation: boolean): Promise {
if (skipConfirmation) throw new Error('No AWS access key ID set.');
else
- return await input({
+ return input({
message:
'Please enter AWS access key ID or use the AWS_ACCESS_KEY_ID environment variable.',
});
}
-async function getSecretAccessKey(skipConfirmation: boolean) {
+async function getSecretAccessKey(skipConfirmation: boolean): Promise {
if (skipConfirmation) throw new Error('No AWS secret access key set.');
else
- return await input({
+ return input({
message:
'Please enter AWS secret access key or use the AWS_SECRET_ACCESS_KEY environment variable.',
});
}
-async function getRegion(skipConfirmation: boolean) {
+async function getRegion(skipConfirmation: boolean): Promise {
if (skipConfirmation) throw new Error('No AWS region set.');
else
- return await input({
+ return input({
message:
'Please enter AWS region or use the AWS_REGION environment variable.',
});
diff --git a/typescript/cli/src/validator/preFlightCheck.ts b/typescript/cli/src/validator/preFlightCheck.ts
new file mode 100644
index 0000000000..ca0c4d8504
--- /dev/null
+++ b/typescript/cli/src/validator/preFlightCheck.ts
@@ -0,0 +1,117 @@
+import { MerkleTreeHook__factory } from '@hyperlane-xyz/core';
+import { HyperlaneCore, S3Validator } from '@hyperlane-xyz/sdk';
+import { Address } from '@hyperlane-xyz/utils';
+
+import { CommandContext } from '../context/types.js';
+import { errorRed, logBlue, logGreen, warnYellow } from '../logger.js';
+
+export const checkValidatorSetup = async (
+ context: CommandContext,
+ chain: string,
+ validators: Set,
+) => {
+ const { multiProvider, registry } = context;
+
+ const addresses = await registry.getAddresses();
+
+ const core = HyperlaneCore.fromAddressesMap(addresses, multiProvider);
+
+ const validatorAnnounce = core.getContracts(chain).validatorAnnounce;
+ const merkleTreeHook = MerkleTreeHook__factory.connect(
+ addresses[chain].merkleTreeHook,
+ multiProvider.getProvider(chain),
+ );
+
+ let merkleTreeLatestCheckpointIndex: number | undefined;
+ try {
+ const [_, latestCheckpointIndex] = await merkleTreeHook.latestCheckpoint();
+
+ merkleTreeLatestCheckpointIndex = latestCheckpointIndex;
+ logBlue(
+ `\nLatest checkpoint index of incremental merkle tree: ${merkleTreeLatestCheckpointIndex}\n`,
+ );
+ } catch (err) {
+ warnYellow(
+ `❗️ Failed to fetch latest checkpoint index of merkleTreeHook on ${chain}: ${err} \n`,
+ );
+ }
+
+ const errorSet = new Set();
+
+ const validatorsArray = Array.from(validators);
+ let validatorStorageLocations: string[][] | undefined;
+
+ try {
+ validatorStorageLocations =
+ await validatorAnnounce.getAnnouncedStorageLocations(validatorsArray);
+ } catch (e) {
+ errorSet.add('Failed to read announced storage locations on chain.');
+ }
+
+ if (validatorStorageLocations) {
+ for (let i = 0; i < validatorsArray.length; i++) {
+ const validator = validatorsArray[i];
+ const storageLocations = validatorStorageLocations[i];
+
+ if (storageLocations.length === 0) {
+ errorRed(`❌ Validator ${validator} has not been announced\n`);
+ errorSet.add('Some validators have not been announced.');
+ continue;
+ }
+
+ const s3StorageLocation = storageLocations[0];
+
+ let s3Validator: S3Validator;
+ try {
+ s3Validator = await S3Validator.fromStorageLocation(s3StorageLocation);
+ } catch (e) {
+ errorRed(
+ `❌ Failed to fetch storage locations for validator ${validator}, this may be due to the storage location not being an S3 bucket\n\n`,
+ );
+ errorSet.add('Failed to fetch storage locations for some validators.');
+ continue;
+ }
+
+ const latestCheckpointIndex =
+ await s3Validator.getLatestCheckpointIndex();
+
+ logBlue(
+ `✅ Validator ${validator} announced\nstorage location: ${s3StorageLocation}\nlatest checkpoint index: ${latestCheckpointIndex}`,
+ );
+
+ // check is latestCheckpointIndex is within 1% of the merkleTreeLatestCheckpointIndex
+ if (merkleTreeLatestCheckpointIndex) {
+ const diff = Math.abs(
+ latestCheckpointIndex - merkleTreeLatestCheckpointIndex,
+ );
+ if (diff > merkleTreeLatestCheckpointIndex / 100) {
+ errorRed(
+ `❌ Validator is not signing the latest available checkpoint\n\n`,
+ );
+ errorSet.add(
+ `Some validators are not signing the latest available checkpoint`,
+ );
+ } else {
+ logBlue(
+ `✅ Validator is signing the latest available checkpoint\n\n`,
+ );
+ }
+ } else {
+ warnYellow(
+ `❗️ Cannot compare validator checkpoint signatures to latest checkpoint in the incremental merkletree, merkletree checkpoint could not be read\n`,
+ );
+ }
+ }
+ }
+
+ if (errorSet.size > 0) {
+ errorRed(
+ `\n❌ Validator pre flight check failed:\n${Array.from(errorSet).join(
+ '\n',
+ )}`,
+ );
+ process.exit(1);
+ } else {
+ logGreen(`\n✅ Validator pre flight check passed`);
+ }
+};
diff --git a/typescript/helloworld/src/deploy/deploy.ts b/typescript/helloworld/src/deploy/deploy.ts
index b7dccecdac..8fcbc61859 100644
--- a/typescript/helloworld/src/deploy/deploy.ts
+++ b/typescript/helloworld/src/deploy/deploy.ts
@@ -38,8 +38,9 @@ export class HelloWorldDeployer extends HyperlaneRouterDeployer<
async deployContracts(chain: ChainName, config: HelloWorldConfig) {
const router = await this.deployContract(chain, 'router', [
config.mailbox,
- config.hook ?? ethers.constants.AddressZero,
+ ethers.constants.AddressZero,
]);
+ await super.configureClient(chain, router, config);
return {
router,
};
diff --git a/typescript/infra/config/environments/mainnet3/core.ts b/typescript/infra/config/environments/mainnet3/core.ts
index 258b37c1c8..e2a2186620 100644
--- a/typescript/infra/config/environments/mainnet3/core.ts
+++ b/typescript/infra/config/environments/mainnet3/core.ts
@@ -7,7 +7,6 @@ import {
CoreConfig,
FallbackRoutingHookConfig,
HookType,
- IgpHookConfig,
IsmType,
MerkleTreeHookConfig,
MultisigConfig,
@@ -59,6 +58,7 @@ export const core: ChainMap = objMap(owners, (local, owner) => {
const pausableIsm: PausableIsmConfig = {
type: IsmType.PAUSABLE,
+ paused: false,
owner: DEPLOYER, // keep pausable hot
};
@@ -72,13 +72,11 @@ export const core: ChainMap = objMap(owners, (local, owner) => {
type: HookType.MERKLE_TREE,
};
- const igpHook: IgpHookConfig = {
- type: HookType.INTERCHAIN_GAS_PAYMASTER,
- ...igp[local],
- };
+ const igpHook = igp[local];
const pausableHook: PausableHookConfig = {
type: HookType.PAUSABLE,
+ paused: false,
owner: DEPLOYER, // keep pausable hot
};
const aggregationHooks = objMap(
diff --git a/typescript/infra/config/environments/mainnet3/igp.ts b/typescript/infra/config/environments/mainnet3/igp.ts
index cc7fa27ee5..89f083ee8b 100644
--- a/typescript/infra/config/environments/mainnet3/igp.ts
+++ b/typescript/infra/config/environments/mainnet3/igp.ts
@@ -3,6 +3,7 @@ import { BigNumber, ethers } from 'ethers';
import {
ChainMap,
ChainName,
+ HookType,
IgpConfig,
TOKEN_EXCHANGE_RATE_DECIMALS,
defaultMultisigConfigs,
@@ -57,20 +58,24 @@ const storageGasOracleConfig: AllStorageGasOracleConfigs =
(local) => remoteOverhead(local),
);
-export const igp: ChainMap = objMap(owners, (local, owner) => ({
- ...owner,
- ownerOverrides: {
- ...owner.ownerOverrides,
- interchainGasPaymaster: DEPLOYER,
- storageGasOracle: DEPLOYER,
- },
- oracleKey: DEPLOYER,
- beneficiary: DEPLOYER,
- overhead: Object.fromEntries(
- exclude(local, supportedChainNames).map((remote) => [
- remote,
- remoteOverhead(remote),
- ]),
- ),
- oracleConfig: storageGasOracleConfig[local],
-}));
+export const igp: ChainMap = objMap(
+ owners,
+ (local, owner): IgpConfig => ({
+ type: HookType.INTERCHAIN_GAS_PAYMASTER,
+ ...owner,
+ ownerOverrides: {
+ ...owner.ownerOverrides,
+ interchainGasPaymaster: DEPLOYER,
+ storageGasOracle: DEPLOYER,
+ },
+ oracleKey: DEPLOYER,
+ beneficiary: DEPLOYER,
+ overhead: Object.fromEntries(
+ exclude(local, supportedChainNames).map((remote) => [
+ remote,
+ remoteOverhead(remote),
+ ]),
+ ),
+ oracleConfig: storageGasOracleConfig[local],
+ }),
+);
diff --git a/typescript/infra/config/environments/test/core.ts b/typescript/infra/config/environments/test/core.ts
index 6063d71c06..4e5988b802 100644
--- a/typescript/infra/config/environments/test/core.ts
+++ b/typescript/infra/config/environments/test/core.ts
@@ -6,7 +6,6 @@ import {
CoreConfig,
FallbackRoutingHookConfig,
HookType,
- IgpHookConfig,
IsmType,
MerkleTreeHookConfig,
ProtocolFeeHookConfig,
@@ -34,10 +33,7 @@ export const core: ChainMap = objMap(owners, (local, owner) => {
type: HookType.MERKLE_TREE,
};
- const igpHook: IgpHookConfig = {
- type: HookType.INTERCHAIN_GAS_PAYMASTER,
- ...igp[local],
- };
+ const igpHook = igp[local];
const aggregationHook: AggregationHookConfig = {
type: HookType.AGGREGATION,
diff --git a/typescript/infra/config/environments/test/igp.ts b/typescript/infra/config/environments/test/igp.ts
index b47be48e66..e63e3de59b 100644
--- a/typescript/infra/config/environments/test/igp.ts
+++ b/typescript/infra/config/environments/test/igp.ts
@@ -1,7 +1,6 @@
import {
ChainMap,
- ChainName,
- GasOracleContractType,
+ HookType,
IgpConfig,
multisigIsmVerificationCost,
} from '@hyperlane-xyz/sdk';
@@ -11,30 +10,25 @@ import { testChainNames } from './chains.js';
import { multisigIsm } from './multisigIsm.js';
import { owners } from './owners.js';
-function getGasOracles(local: ChainName) {
- return Object.fromEntries(
- exclude(local, testChainNames).map((name) => [
- name,
- GasOracleContractType.StorageGasOracle,
- ]),
- );
-}
-
-export const igp: ChainMap = objMap(owners, (chain, ownerConfig) => {
- const overhead = Object.fromEntries(
- exclude(chain, testChainNames).map((remote) => [
- remote,
- multisigIsmVerificationCost(
- multisigIsm[remote].threshold,
- multisigIsm[remote].validators.length,
- ),
- ]),
- );
- return {
- oracleKey: ownerConfig.owner as Address, // owner can be AccountConfig
- beneficiary: ownerConfig.owner as Address, // same as above
- gasOracleType: getGasOracles(chain),
- overhead,
- ...ownerConfig,
- };
-});
+export const igp: ChainMap = objMap(
+ owners,
+ (chain, ownerConfig): IgpConfig => {
+ const overhead = Object.fromEntries(
+ exclude(chain, testChainNames).map((remote) => [
+ remote,
+ multisigIsmVerificationCost(
+ multisigIsm[remote].threshold,
+ multisigIsm[remote].validators.length,
+ ),
+ ]),
+ );
+ return {
+ type: HookType.INTERCHAIN_GAS_PAYMASTER,
+ oracleKey: ownerConfig.owner as Address, // owner can be AccountConfig
+ beneficiary: ownerConfig.owner as Address, // same as above
+ overhead,
+ oracleConfig: {},
+ ...ownerConfig,
+ };
+ },
+);
diff --git a/typescript/infra/config/environments/test/multisigIsm.ts b/typescript/infra/config/environments/test/multisigIsm.ts
index afa7916c95..733f5907dd 100644
--- a/typescript/infra/config/environments/test/multisigIsm.ts
+++ b/typescript/infra/config/environments/test/multisigIsm.ts
@@ -1,11 +1,18 @@
-import { ChainMap, IsmType, MultisigIsmConfig } from '@hyperlane-xyz/sdk';
+import {
+ ChainMap,
+ IsmType,
+ MultisigIsmConfig,
+ TestChainName,
+} from '@hyperlane-xyz/sdk';
+import { Address } from '@hyperlane-xyz/utils';
// the addresses here must line up with the e2e test's validator addresses
-// Validators are anvil accounts 4-6
-export const chainToValidator: Record = {
+// Validators are anvil accounts 4-7
+export const chainToValidator: Record = {
test1: '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65',
test2: '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc',
test3: '0x976EA74026E726554dB657fA54763abd0C3a0aa9',
+ test4: '0x14dC79964da2C08b23698B3D3cc7Ca32193d9955',
};
export const merkleRootMultisig = (validatorKey: string): MultisigIsmConfig => {
@@ -26,8 +33,9 @@ export const messageIdMultisig = (validatorKey: string): MultisigIsmConfig => {
// the addresses here must line up with the e2e test's validator addresses
export const multisigIsm: ChainMap = {
- // Validators are anvil accounts 4-6
+ // Validators are anvil accounts 4-7
test1: messageIdMultisig(chainToValidator['test1']),
test2: merkleRootMultisig(chainToValidator['test2']),
test3: messageIdMultisig(chainToValidator['test3']),
+ test4: messageIdMultisig(chainToValidator['test4']),
};
diff --git a/typescript/infra/config/environments/testnet4/core.ts b/typescript/infra/config/environments/testnet4/core.ts
index df52631f3e..121bcc90e4 100644
--- a/typescript/infra/config/environments/testnet4/core.ts
+++ b/typescript/infra/config/environments/testnet4/core.ts
@@ -7,7 +7,6 @@ import {
CoreConfig,
FallbackRoutingHookConfig,
HookType,
- IgpHookConfig,
IsmType,
MerkleTreeHookConfig,
MultisigConfig,
@@ -61,6 +60,7 @@ export const core: ChainMap = objMap(
const pausableIsm: PausableIsmConfig = {
type: IsmType.PAUSABLE,
+ paused: false,
...ownerConfig,
};
@@ -74,13 +74,11 @@ export const core: ChainMap = objMap(
type: HookType.MERKLE_TREE,
};
- const igpHook: IgpHookConfig = {
- type: HookType.INTERCHAIN_GAS_PAYMASTER,
- ...igp[local],
- };
+ const igpHook = igp[local];
const pausableHook: PausableHookConfig = {
type: HookType.PAUSABLE,
+ paused: false,
...ownerConfig,
};
diff --git a/typescript/infra/config/environments/testnet4/igp.ts b/typescript/infra/config/environments/testnet4/igp.ts
index f0736d46e1..4c388b4f0d 100644
--- a/typescript/infra/config/environments/testnet4/igp.ts
+++ b/typescript/infra/config/environments/testnet4/igp.ts
@@ -1,5 +1,6 @@
import {
ChainMap,
+ HookType,
IgpConfig,
defaultMultisigConfigs,
multisigIsmVerificationCost,
@@ -10,21 +11,25 @@ import { storageGasOracleConfig } from './gas-oracle.js';
import { owners } from './owners.js';
import { supportedChainNames } from './supportedChainNames.js';
-export const igp: ChainMap = objMap(owners, (chain, ownerConfig) => {
- return {
- ...ownerConfig,
- oracleKey: ownerConfig.owner as Address,
- beneficiary: ownerConfig.owner as Address,
- oracleConfig: storageGasOracleConfig[chain],
- overhead: Object.fromEntries(
- exclude(chain, supportedChainNames).map((remote) => [
- remote,
- multisigIsmVerificationCost(
- // TODO: parameterize this
- defaultMultisigConfigs[remote].threshold,
- defaultMultisigConfigs[remote].validators.length,
- ),
- ]),
- ),
- };
-});
+export const igp: ChainMap = objMap(
+ owners,
+ (chain, ownerConfig): IgpConfig => {
+ return {
+ type: HookType.INTERCHAIN_GAS_PAYMASTER,
+ ...ownerConfig,
+ oracleKey: ownerConfig.owner as Address,
+ beneficiary: ownerConfig.owner as Address,
+ oracleConfig: storageGasOracleConfig[chain],
+ overhead: Object.fromEntries(
+ exclude(chain, supportedChainNames).map((remote) => [
+ remote,
+ multisigIsmVerificationCost(
+ // TODO: parameterize this
+ defaultMultisigConfigs[remote].threshold,
+ defaultMultisigConfigs[remote].validators.length,
+ ),
+ ]),
+ ),
+ };
+ },
+);
diff --git a/typescript/infra/package.json b/typescript/infra/package.json
index 6b6188e63c..3cc965bca1 100644
--- a/typescript/infra/package.json
+++ b/typescript/infra/package.json
@@ -25,6 +25,7 @@
"json-stable-stringify": "^1.1.1",
"prom-client": "^14.0.1",
"prompts": "^2.4.2",
+ "yaml": "^2.4.5",
"yargs": "^17.7.2"
},
"devDependencies": {
diff --git a/typescript/infra/scripts/check-deploy.ts b/typescript/infra/scripts/check-deploy.ts
index bc14e0aeed..5a0d9c5b7f 100644
--- a/typescript/infra/scripts/check-deploy.ts
+++ b/typescript/infra/scripts/check-deploy.ts
@@ -10,7 +10,6 @@ import {
InterchainAccountChecker,
InterchainQuery,
InterchainQueryChecker,
- resolveOrDeployAccountOwner,
} from '@hyperlane-xyz/sdk';
import { Contexts } from '../config/contexts.js';
@@ -36,15 +35,15 @@ import { getHelloWorldApp } from './helloworld/utils.js';
function getArgs() {
return withChain(withModuleAndFork(withContext(getRootArgs())))
- .boolean('asdeployer')
- .default('asdeployer', false)
+ .boolean('asDeployer')
+ .default('asDeployer', false)
.boolean('govern')
.default('govern', false)
.alias('g', 'govern').argv;
}
async function check() {
- const { fork, govern, module, environment, context, chain, asdeployer } =
+ const { fork, govern, module, environment, context, chain, asDeployer } =
await getArgs();
const envConfig = getEnvironmentConfig(environment);
let multiProvider = await envConfig.getMultiProvider();
@@ -58,13 +57,7 @@ async function check() {
[fork]: { blocks: { confirmations: 0 } },
});
- const owner = asdeployer
- ? DEPLOYER
- : await resolveOrDeployAccountOwner(
- multiProvider,
- fork,
- envConfig.core[fork].owner,
- );
+ const owner = asDeployer ? DEPLOYER : envConfig.core[fork].owner;
const signer = await impersonateAccount(owner, 1e18);
multiProvider.setSigner(fork, signer);
diff --git a/typescript/infra/scripts/deploy.ts b/typescript/infra/scripts/deploy.ts
index 16a4c30176..e55c1b3e05 100644
--- a/typescript/infra/scripts/deploy.ts
+++ b/typescript/infra/scripts/deploy.ts
@@ -8,7 +8,6 @@ import {
ChainMap,
ContractVerifier,
ExplorerLicenseType,
- FallbackRoutingHookConfig,
HypERC20Deployer,
HyperlaneCoreDeployer,
HyperlaneDeployer,
@@ -26,7 +25,6 @@ import { objFilter, objMap } from '@hyperlane-xyz/utils';
import { Contexts } from '../config/contexts.js';
import { core as coreConfig } from '../config/environments/mainnet3/core.js';
-import { DEPLOYER } from '../config/environments/mainnet3/owners.js';
import { getEnvAddresses } from '../config/registry.js';
import { getWarpConfig } from '../config/warp.js';
import { deployWithArtifacts } from '../src/deployment/deploy.js';
@@ -200,11 +198,7 @@ async function main() {
);
// Config is intended to be changed for ad-hoc use cases:
config = {
- ethereum: {
- ...(coreConfig.ethereum.defaultHook as FallbackRoutingHookConfig)
- .domains.ancient8,
- owner: DEPLOYER,
- },
+ ethereum: coreConfig.ethereum.defaultHook,
};
} else {
console.log(`Skipping ${module}, deployer unimplemented`);
diff --git a/typescript/infra/scripts/generate-renzo-warp-route-config.ts b/typescript/infra/scripts/generate-renzo-warp-route-config.ts
new file mode 100644
index 0000000000..e41b7c32ee
--- /dev/null
+++ b/typescript/infra/scripts/generate-renzo-warp-route-config.ts
@@ -0,0 +1,150 @@
+import { writeFileSync } from 'fs';
+import { stringify as yamlStringify } from 'yaml';
+
+import { GithubRegistry } from '@hyperlane-xyz/registry';
+import {
+ IsmType,
+ TokenRouterConfig,
+ TokenType,
+ WarpRouteDeployConfig,
+ WarpRouteDeployConfigSchema,
+ buildAggregationIsmConfigs,
+} from '@hyperlane-xyz/sdk';
+
+const lockbox = '0xC8140dA31E6bCa19b287cC35531c2212763C2059';
+const xERC20 = '0x2416092f143378750bb29b79eD961ab195CcEea5';
+const lockboxChain = 'ethereum';
+
+const chainsToDeploy = [
+ 'arbitrum',
+ 'optimism',
+ 'base',
+ 'blast',
+ 'bsc',
+ 'mode',
+ 'linea',
+ 'ethereum',
+];
+
+const ezEthValidators = {
+ arbitrum: {
+ threshold: 1,
+ validators: [
+ '0xc27032c6bbd48c20005f552af3aaa0dbf14260f3', // Renzo
+ '0x9bCcFAd3BD12Ef0Ee8aE839dD9ED7835BcCaDc9D', // Everclear
+ ],
+ },
+ optimism: {
+ threshold: 1,
+ validators: [
+ '0xe2593D205F5E7F74A50fA900824501084E092eBd', // Renzo
+ '0x6f4cb8e96db5d44422a4495faa73fffb9d30e9e2', // Everclear
+ ],
+ },
+ base: {
+ threshold: 1,
+ validators: [
+ '0x25BA4eE5268CbfB8D69BAc531Aa10368778702BD', // Renzo
+ '0x9ec803b503e9c7d2611e231521ef3fde73f7a21c', // Everclear
+ ],
+ },
+ blast: {
+ threshold: 1,
+ validators: [
+ '0x54Bb0036F777202371429e062FE6AEE0d59442F9', // Renzo
+ '0x1652d8ba766821cf01aeea34306dfc1cab964a32', // Everclear
+ ],
+ },
+ bsc: {
+ threshold: 1,
+ validators: [
+ '0x3156Db97a3B3e2dcc3D69FdDfD3e12dc7c937b6D', // Renzo
+ '0x9a0326c43e4713ae2477f09e0f28ffedc24d8266', // Everclear
+ ],
+ },
+ mode: {
+ threshold: 1,
+ validators: [
+ '0x7e29608C6E5792bBf9128599ca309Be0728af7B4', // Renzo
+ '0x456fbbe05484fc9f2f38ea09648424f54d6872be', // Everclear
+ ],
+ },
+ linea: {
+ threshold: 1,
+ validators: [
+ '0xcb3e44EdD2229860bDBaA58Ba2c3817D111bEE9A', // Renzo
+ '0x06a5a2a429560034d38bf62ca6d470942535947e', // Everclear
+ ],
+ },
+ ethereum: {
+ threshold: 1,
+ validators: [
+ '0xc7f7b94a6BaF2FFFa54DfE1dDE6E5Fcbb749e04f', // Renzo
+ '0x1fd889337F60986aa57166bc5AC121eFD13e4fdd', // Everclear
+ ],
+ },
+};
+const zeroAddress = '0x0000000000000000000000000000000000000001';
+
+async function main() {
+ const registry = new GithubRegistry();
+
+ const tokenConfig: WarpRouteDeployConfig =
+ Object.fromEntries(
+ await Promise.all(
+ chainsToDeploy.map(
+ async (chain): Promise<[string, TokenRouterConfig]> => {
+ const ret: [string, TokenRouterConfig] = [
+ chain,
+ {
+ isNft: false,
+ type:
+ chain === lockboxChain
+ ? TokenType.XERC20Lockbox
+ : TokenType.XERC20,
+ token: chain === lockboxChain ? lockbox : xERC20,
+ owner: zeroAddress,
+ mailbox: (await registry.getChainAddresses(chain))!.mailbox,
+ interchainSecurityModule: {
+ type: IsmType.AGGREGATION,
+ threshold: 2,
+ modules: [
+ {
+ type: IsmType.ROUTING,
+ owner: zeroAddress,
+ domains: buildAggregationIsmConfigs(
+ chain,
+ chainsToDeploy,
+ ezEthValidators,
+ ),
+ },
+ {
+ type: IsmType.FALLBACK_ROUTING,
+ domains: {},
+ owner: zeroAddress,
+ },
+ ],
+ },
+ },
+ ];
+
+ return ret;
+ },
+ ),
+ ),
+ );
+
+ const parsed = WarpRouteDeployConfigSchema.safeParse(tokenConfig);
+
+ if (!parsed.success) {
+ console.dir(parsed.error.format(), { depth: null });
+ return;
+ }
+
+ writeFileSync(
+ 'renzo-warp-route-config.yaml',
+ yamlStringify(parsed.data, null, 2),
+ );
+}
+
+main().catch(console.error).then(console.log);
diff --git a/typescript/infra/scripts/helloworld/kathy.ts b/typescript/infra/scripts/helloworld/kathy.ts
index f3cbb1a495..cea226d4fd 100644
--- a/typescript/infra/scripts/helloworld/kathy.ts
+++ b/typescript/infra/scripts/helloworld/kathy.ts
@@ -12,7 +12,6 @@ import {
MultiProvider,
ProviderType,
TypedTransactionReceipt,
- resolveOrDeployAccountOwner,
} from '@hyperlane-xyz/sdk';
import {
Address,
@@ -234,12 +233,11 @@ async function main(): Promise {
}
chains.map(async (chain) => {
- const owner = await resolveOrDeployAccountOwner(
- multiProvider,
+ return updateWalletBalanceMetricFor(
+ app,
chain,
coreConfig.owners[chain].owner,
);
- return updateWalletBalanceMetricFor(app, chain, owner);
});
// Incremented each time an entire cycle has occurred
@@ -358,11 +356,7 @@ async function main(): Promise {
messagesSendCount.labels({ ...labels, status: 'failure' }).inc();
errorOccurred = true;
}
- const owner = await resolveOrDeployAccountOwner(
- multiProvider,
- origin,
- coreConfig.owners[origin].owner,
- );
+ const owner = coreConfig.owners[origin].owner;
updateWalletBalanceMetricFor(app, origin, owner).catch((e) => {
logger.warn('Failed to update wallet balance for chain', {
chain: origin,
diff --git a/typescript/infra/scripts/send-test-messages.ts b/typescript/infra/scripts/send-test-messages.ts
index f602f4b30e..d028263982 100644
--- a/typescript/infra/scripts/send-test-messages.ts
+++ b/typescript/infra/scripts/send-test-messages.ts
@@ -1,3 +1,4 @@
+import { Provider } from '@ethersproject/providers';
import { Wallet } from 'ethers';
import fs from 'fs';
import yargs from 'yargs';
@@ -100,15 +101,28 @@ async function main() {
const { timeout, defaultHook, requiredHook, mineforever } = args;
let messages = args.messages;
+ // Limit the test chains to a subset of the known chains
+ // E2E in Rust only knows about test1, test2 and test3
+ const kathyTestChains = [
+ TestChainName.test1,
+ TestChainName.test2,
+ TestChainName.test3,
+ ];
+
+ // Create a multi-provider with a signer
const signer = new Wallet(ANVIL_KEY);
const multiProvider = MultiProvider.createTestMultiProvider({ signer });
+
+ // Get the provider for the first chain
const provider = multiProvider.getProvider(TestChainName.test1);
+ // Create core from addresses
const addresses = JSON.parse(
fs.readFileSync('./config/environments/test/core/addresses.json', 'utf8'),
);
const core = HyperlaneCore.fromAddressesMap(addresses, multiProvider);
+ // helper function to get a random element from a list
const randomElement = (list: T[]) =>
list[Math.floor(Math.random() * list.length)];
@@ -121,9 +135,11 @@ async function main() {
const run_forever = messages === 0;
while (run_forever || messages-- > 0) {
// Round robin origin chain
- const local = core.chains()[messages % core.chains().length];
+ const local = kathyTestChains[messages % kathyTestChains.length];
// Random remote chain
- const remote: ChainName = randomElement(await core.remoteChains(local));
+ const remote: ChainName = randomElement(
+ kathyTestChains.filter((c) => c !== local),
+ );
const remoteId = multiProvider.getDomainId(remote);
const contracts = core.getContracts(local);
const mailbox = contracts.mailbox;
diff --git a/typescript/infra/src/govern/HyperlaneAppGovernor.ts b/typescript/infra/src/govern/HyperlaneAppGovernor.ts
index 7225bfb46c..5e901ee19f 100644
--- a/typescript/infra/src/govern/HyperlaneAppGovernor.ts
+++ b/typescript/infra/src/govern/HyperlaneAppGovernor.ts
@@ -200,12 +200,18 @@ export abstract class HyperlaneAppGovernor<
accountConfig.owner,
)} on ${origin}`,
);
- const callRemote = await this.interchainAccount.getCallRemote(
- origin,
- chain,
- [call],
- accountConfig,
- );
+ const callRemote = await this.interchainAccount.getCallRemote({
+ chain: origin,
+ destination: chain,
+ innerCalls: [
+ {
+ to: call.to,
+ data: call.data,
+ value: call.value?.toString() || '0',
+ },
+ ],
+ config: accountConfig,
+ });
if (!callRemote.to || !callRemote.data) {
return SubmissionType.MANUAL;
}
diff --git a/typescript/infra/test/govern.hardhat-test.ts b/typescript/infra/test/govern.hardhat-test.ts
index e5852d1f3b..876e8ddd00 100644
--- a/typescript/infra/test/govern.hardhat-test.ts
+++ b/typescript/infra/test/govern.hardhat-test.ts
@@ -26,7 +26,6 @@ import {
TestChainName,
TestCoreApp,
TestCoreDeployer,
- resolveOrDeployAccountOwner,
} from '@hyperlane-xyz/sdk';
import { Address, CallData, eqAddress } from '@hyperlane-xyz/utils';
@@ -134,11 +133,7 @@ describe('ICA governance', async () => {
localRouter: remote.address,
};
- accountOwner = await resolveOrDeployAccountOwner(
- multiProvider,
- remoteChain,
- accountConfig,
- );
+ accountOwner = await icaApp.deployAccount(remoteChain, accountConfig);
const recipientF = new TestRecipient__factory(signer);
recipient = await recipientF.deploy();
diff --git a/typescript/sdk/src/consts/testChains.ts b/typescript/sdk/src/consts/testChains.ts
index 2737967261..a5e8b91eec 100644
--- a/typescript/sdk/src/consts/testChains.ts
+++ b/typescript/sdk/src/consts/testChains.ts
@@ -10,6 +10,7 @@ export enum TestChainName {
test1 = 'test1',
test2 = 'test2',
test3 = 'test3',
+ test4 = 'test4',
}
export const testChains: Array = Object.values(TestChainName);
@@ -65,10 +66,19 @@ export const test3: ChainMetadata = {
name: 'test3',
};
+export const test4: ChainMetadata = {
+ ...test1,
+ chainId: 31337,
+ displayName: 'Test 4',
+ domainId: 31337,
+ name: 'test4',
+};
+
export const testChainMetadata: ChainMap = {
test1,
test2,
test3,
+ test4,
};
export const testCosmosChain: ChainMetadata = {
diff --git a/typescript/sdk/src/contracts/contracts.ts b/typescript/sdk/src/contracts/contracts.ts
index 557e68d17d..7a58b3bfdc 100644
--- a/typescript/sdk/src/contracts/contracts.ts
+++ b/typescript/sdk/src/contracts/contracts.ts
@@ -146,6 +146,15 @@ export function attachContractsMapAndGetForeignDeployments<
};
}
+export function attachAndConnectContracts(
+ addresses: HyperlaneAddresses,
+ factories: F,
+ connection: Connection,
+): HyperlaneContracts {
+ const contracts = attachContracts(addresses, factories);
+ return connectContracts(contracts, connection);
+}
+
export function connectContracts(
contracts: HyperlaneContracts,
connection: Connection,
diff --git a/typescript/sdk/src/core/AbstractHyperlaneModule.ts b/typescript/sdk/src/core/AbstractHyperlaneModule.ts
index a5159ac851..b4a42ec301 100644
--- a/typescript/sdk/src/core/AbstractHyperlaneModule.ts
+++ b/typescript/sdk/src/core/AbstractHyperlaneModule.ts
@@ -5,7 +5,7 @@ import { Annotated, ProtocolType } from '@hyperlane-xyz/utils';
import { ProtocolTypedTransaction } from '../providers/ProviderType.js';
import { ChainNameOrId } from '../types.js';
-export type HyperlaneModuleArgs<
+export type HyperlaneModuleParams<
TConfig,
TAddressMap extends Record,
> = {
@@ -22,7 +22,7 @@ export abstract class HyperlaneModule<
protected abstract readonly logger: Logger;
protected constructor(
- protected readonly args: HyperlaneModuleArgs,
+ protected readonly args: HyperlaneModuleParams,
) {}
public serialize(): TAddressMap {
@@ -32,7 +32,7 @@ export abstract class HyperlaneModule<
public abstract read(): Promise;
public abstract update(
config: TConfig,
- ): Promise[]>>;
+ ): Promise['transaction'][]>>;
// /*
// Types and static methods can be challenging. Ensure each implementation includes a static create function.
diff --git a/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts b/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts
index 58bf38f27a..2664d0ee23 100644
--- a/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts
+++ b/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts
@@ -8,7 +8,7 @@ import { objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import { TestChainName, testChains } from '../consts/testChains.js';
import { HyperlaneContractsMap } from '../contracts/types.js';
import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
-import { HookConfig } from '../hook/types.js';
+import { DerivedHookConfig } from '../hook/EvmHookReader.js';
import { DerivedIsmConfig } from '../ism/EvmIsmReader.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { AggregationIsmConfig, IsmType } from '../ism/types.js';
@@ -127,7 +127,7 @@ describe('core', async () => {
});
async function deriveCoreConfig(chainName: string, mailboxAddress: string) {
- return await new EvmCoreReader(multiProvider, chainName).deriveCoreConfig(
+ return new EvmCoreReader(multiProvider, chainName).deriveCoreConfig(
mailboxAddress,
);
}
@@ -140,12 +140,12 @@ describe('core', async () => {
);
// Cast because we don't expect the 'string' type
- const defaultIsmOnchain =
+ const { address: _, ...defaultIsmOnchain } =
coreConfigOnChain.defaultIsm as DerivedIsmConfig;
const defaultIsmTest = coreConfig[chainName]
.defaultIsm as DerivedIsmConfig;
- expect(defaultIsmOnchain.type).to.be.equal(defaultIsmTest.type);
+ expect(defaultIsmOnchain).to.deep.equal(defaultIsmTest);
}),
);
});
@@ -158,12 +158,12 @@ describe('core', async () => {
);
// Cast because we don't expect the 'string' type
- const defaultHookOnchain =
- coreConfigOnChain.defaultHook as HookConfig;
+ const { address: _, ...defaultHookOnchain } =
+ coreConfigOnChain.defaultHook as DerivedHookConfig;
const defaultHookTest = coreConfig[chainName]
- .defaultHook as HookConfig;
+ .defaultHook as DerivedHookConfig;
- expect(defaultHookOnchain.type).to.be.equal(defaultHookTest.type);
+ expect(defaultHookOnchain).to.deep.equal(defaultHookTest);
}),
);
});
@@ -174,13 +174,11 @@ describe('core', async () => {
chainName,
contract.mailbox.address,
);
- const requiredHookOnchain = coreConfigOnChain.requiredHook;
+ const { address: _, ...requiredHookOnchain } =
+ coreConfigOnChain.requiredHook as DerivedHookConfig;
const requiredHookTest = coreConfig[chainName].requiredHook;
- // Test all the fields
- objMap(requiredHookTest, (key, value) => {
- expect(requiredHookOnchain[key]).to.be.equal(value);
- });
+ expect(requiredHookOnchain).to.deep.equal(requiredHookTest);
}),
);
});
diff --git a/typescript/sdk/src/core/EvmCoreModule.hardhat-test.ts b/typescript/sdk/src/core/EvmCoreModule.hardhat-test.ts
new file mode 100644
index 0000000000..7189718f18
--- /dev/null
+++ b/typescript/sdk/src/core/EvmCoreModule.hardhat-test.ts
@@ -0,0 +1,164 @@
+import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js';
+import { expect } from 'chai';
+import { constants } from 'ethers';
+import hre from 'hardhat';
+
+import {
+ Mailbox__factory,
+ ProxyAdmin__factory,
+ TestRecipient__factory,
+ TimelockController__factory,
+ ValidatorAnnounce__factory,
+} from '@hyperlane-xyz/core';
+import { objMap } from '@hyperlane-xyz/utils';
+
+import { TestChainName } from '../consts/testChains.js';
+import { MultiProvider } from '../providers/MultiProvider.js';
+import { testCoreConfig } from '../test/testUtils.js';
+
+import { EvmCoreModule } from './EvmCoreModule.js';
+import { CoreConfig } from './types.js';
+
+describe('EvmCoreModule', async () => {
+ const CHAIN = TestChainName.test1;
+ const DELAY = 1892391283182;
+ let config: CoreConfig;
+ let signer: SignerWithAddress;
+ let multiProvider: MultiProvider;
+ let evmCoreModule: EvmCoreModule;
+ let proxyAdminContract: any;
+ let mailboxContract: any;
+ let validatorAnnounceContract: any;
+ let testRecipientContract: any;
+ let timelockControllerContract: any;
+
+ before(async () => {
+ [signer] = await hre.ethers.getSigners();
+ multiProvider = MultiProvider.createTestMultiProvider({ signer });
+ config = {
+ ...testCoreConfig([CHAIN])[CHAIN],
+ upgrade: {
+ timelock: {
+ delay: DELAY,
+ roles: {
+ executor: signer.address,
+ proposer: signer.address,
+ },
+ },
+ },
+ };
+
+ evmCoreModule = await EvmCoreModule.create({
+ chain: CHAIN,
+ config,
+ multiProvider,
+ });
+
+ const {
+ proxyAdmin,
+ mailbox,
+ validatorAnnounce,
+ testRecipient,
+ timelockController,
+ } = evmCoreModule.serialize();
+
+ proxyAdminContract = ProxyAdmin__factory.connect(
+ proxyAdmin!,
+ multiProvider.getProvider(CHAIN),
+ );
+
+ mailboxContract = Mailbox__factory.connect(
+ mailbox!,
+ multiProvider.getProvider(CHAIN),
+ );
+
+ validatorAnnounceContract = ValidatorAnnounce__factory.connect(
+ validatorAnnounce!,
+ multiProvider.getProvider(CHAIN),
+ );
+
+ testRecipientContract = TestRecipient__factory.connect(
+ testRecipient!,
+ multiProvider.getProvider(CHAIN),
+ );
+
+ timelockControllerContract = TimelockController__factory.connect(
+ timelockController!,
+ multiProvider.getProvider(CHAIN),
+ );
+ });
+
+ describe('Create', async () => {
+ it('should create deploy an ICA', () => {
+ const { interchainAccountRouter, interchainAccountIsm } =
+ evmCoreModule.serialize();
+ expect(interchainAccountIsm).to.exist;
+ expect(interchainAccountRouter).to.exist;
+ });
+
+ it('should deploy ISM factories', () => {
+ // Each ISM factory
+ const deployedContracts = evmCoreModule.serialize();
+
+ objMap(deployedContracts as any, (_, address) => {
+ expect(address).to.exist;
+ expect(address).to.not.equal(constants.AddressZero);
+ });
+ });
+
+ it('should deploy proxyAdmin', () => {
+ expect(evmCoreModule.serialize().proxyAdmin).to.exist;
+ });
+
+ it('should set proxyAdmin owner to deployer', async () => {
+ expect(await proxyAdminContract.owner()).to.equal(signer.address);
+ });
+
+ it('should deploy mailbox', async () => {
+ const mailboxAddress = evmCoreModule.serialize().mailbox;
+ expect(mailboxAddress).to.exist;
+
+ // Check that it's actually a mailbox by calling one of it's methods
+ expect(await mailboxContract.localDomain()).to.equal(
+ multiProvider.getChainId(CHAIN),
+ );
+ });
+
+ it('should set mailbox owner to config owner', async () => {
+ expect(await mailboxContract.owner()).to.equal(config.owner);
+ });
+
+ it('should deploy mailbox default Ism', async () => {
+ expect(await mailboxContract.defaultIsm()).to.not.equal(
+ constants.AddressZero,
+ );
+ });
+
+ it('should deploy mailbox default hook', async () => {
+ expect(await mailboxContract.defaultHook()).to.not.equal(
+ constants.AddressZero,
+ );
+ });
+
+ it('should deploy mailbox required hook', async () => {
+ expect(await mailboxContract.requiredHook()).to.not.equal(
+ constants.AddressZero,
+ );
+ });
+
+ it('should deploy validatorAnnounce', async () => {
+ expect(evmCoreModule.serialize().validatorAnnounce).to.exist;
+ expect(await validatorAnnounceContract.owner()).to.equal(signer.address);
+ });
+
+ it('should deploy testRecipient', async () => {
+ expect(evmCoreModule.serialize().testRecipient).to.exist;
+ expect(await testRecipientContract.owner()).to.equal(signer.address);
+ });
+
+ it('should deploy timelock if upgrade is set', async () => {
+ expect(evmCoreModule.serialize().timelockController).to.exist;
+ expect(await timelockControllerContract.getMinDelay()).to.equal(DELAY);
+ });
+ });
+});
diff --git a/typescript/sdk/src/core/EvmCoreModule.ts b/typescript/sdk/src/core/EvmCoreModule.ts
new file mode 100644
index 0000000000..2fbfe4bd6f
--- /dev/null
+++ b/typescript/sdk/src/core/EvmCoreModule.ts
@@ -0,0 +1,275 @@
+import { Mailbox } from '@hyperlane-xyz/core';
+import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils';
+
+import {
+ attachContractsMap,
+ serializeContractsMap,
+} from '../contracts/contracts.js';
+import { HyperlaneAddresses } from '../contracts/types.js';
+import { CoreConfig } from '../core/types.js';
+import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
+import {
+ ProxyFactoryFactories,
+ proxyFactoryFactories,
+} from '../deploy/contracts.js';
+import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
+import { MultiProvider } from '../providers/MultiProvider.js';
+import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
+import { ChainNameOrId } from '../types.js';
+
+import {
+ HyperlaneModule,
+ HyperlaneModuleParams,
+} from './AbstractHyperlaneModule.js';
+import { EvmCoreReader } from './EvmCoreReader.js';
+import { EvmIcaModule } from './EvmIcaModule.js';
+import { HyperlaneCoreDeployer } from './HyperlaneCoreDeployer.js';
+import { CoreFactories } from './contracts.js';
+
+export type DeployedCoreAdresses = HyperlaneAddresses & {
+ testRecipient: Address;
+ timelockController?: Address; // Can be optional because it is only deployed if config.upgrade = true
+ interchainAccountRouter: Address;
+ interchainAccountIsm: Address;
+} & HyperlaneAddresses;
+
+export class EvmCoreModule extends HyperlaneModule<
+ ProtocolType.Ethereum,
+ CoreConfig,
+ DeployedCoreAdresses
+> {
+ protected logger = rootLogger.child({ module: 'EvmCoreModule' });
+ protected coreReader: EvmCoreReader;
+ public readonly chainName: string;
+
+ protected constructor(
+ protected readonly multiProvider: MultiProvider,
+ args: HyperlaneModuleParams,
+ ) {
+ super(args);
+ this.coreReader = new EvmCoreReader(multiProvider, this.args.chain);
+ this.chainName = this.multiProvider.getChainName(this.args.chain);
+ }
+
+ /**
+ * Reads the core configuration from the mailbox address specified in the SDK arguments.
+ * @returns The core config.
+ */
+ public async read(): Promise {
+ return this.coreReader.deriveCoreConfig(this.args.addresses.mailbox);
+ }
+
+ public async update(_config: CoreConfig): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ /**
+ * Deploys the Core contracts.
+ * @remark Most of the contract owners is the Deployer with some being the Proxy Admin.
+ * @returns The created EvmCoreModule instance.
+ */
+ public static async create(params: {
+ chain: ChainNameOrId;
+ config: CoreConfig;
+ multiProvider: MultiProvider;
+ }): Promise {
+ const { chain, config, multiProvider } = params;
+ const addresses = await EvmCoreModule.deploy({
+ config,
+ multiProvider,
+ chain,
+ });
+
+ // Create CoreModule and deploy the Core contracts
+ const module = new EvmCoreModule(multiProvider, {
+ addresses,
+ chain,
+ config,
+ });
+
+ return module;
+ }
+
+ /**
+ * Deploys the core Hyperlane contracts.
+ * @returns The deployed core contract addresses.
+ */
+ static async deploy(params: {
+ config: CoreConfig;
+ multiProvider: MultiProvider;
+ chain: ChainNameOrId;
+ }): Promise {
+ const { config, multiProvider, chain } = params;
+ const chainName = multiProvider.getChainName(chain);
+
+ // Deploy Ism Factories
+ const ismFactoryFactories = await EvmCoreModule.deployIsmFactories({
+ chainName,
+ config,
+ multiProvider,
+ });
+
+ // Deploy IsmFactory to be used in CoreDeployer
+ const ismFactory = new HyperlaneIsmFactory(
+ attachContractsMap(
+ { [chainName]: ismFactoryFactories },
+ proxyFactoryFactories,
+ ),
+ multiProvider,
+ );
+
+ // Initialize Deployer
+ const coreDeployer = new HyperlaneCoreDeployer(multiProvider, ismFactory);
+
+ // Deploy proxyAdmin
+ const proxyAdmin = (
+ await coreDeployer.deployContract(chainName, 'proxyAdmin', [])
+ ).address;
+
+ // Deploy Mailbox
+ const mailbox = await this.deployMailbox({
+ config,
+ coreDeployer,
+ proxyAdmin,
+ multiProvider,
+ chain,
+ });
+
+ // Deploy ICA ISM and Router
+ const { interchainAccountRouter, interchainAccountIsm } = (
+ await EvmIcaModule.create({
+ chain: chainName,
+ multiProvider: multiProvider,
+ config: {
+ mailbox: mailbox.address,
+ owner: await multiProvider.getSigner(chain).getAddress(),
+ },
+ })
+ ).serialize();
+
+ // Deploy Validator announce
+ const validatorAnnounce = (
+ await coreDeployer.deployValidatorAnnounce(chainName, mailbox.address)
+ ).address;
+
+ // Deploy timelock controller if config.upgrade is set
+ let timelockController;
+ if (config.upgrade) {
+ timelockController = (
+ await coreDeployer.deployTimelock(chainName, config.upgrade.timelock)
+ ).address;
+ }
+
+ // Deploy Test Receipient
+ const testRecipient = (
+ await coreDeployer.deployTestRecipient(
+ chainName,
+ await mailbox.defaultIsm(),
+ )
+ ).address;
+
+ // Set Core & extra addresses
+ return {
+ ...ismFactoryFactories,
+ proxyAdmin,
+ mailbox: mailbox.address,
+ interchainAccountRouter,
+ interchainAccountIsm,
+ validatorAnnounce,
+ timelockController,
+ testRecipient,
+ };
+ }
+
+ /**
+ * Deploys the ISM factories for a given chain.
+ * @returns The deployed ISM factories addresses.
+ */
+ static async deployIsmFactories(params: {
+ chainName: string;
+ config: CoreConfig;
+ multiProvider: MultiProvider;
+ }): Promise> {
+ const { chainName, config, multiProvider } = params;
+
+ // ChainMap is still needed for HyperlaneIsmFactory
+ const proxyFactoryDeployer = new HyperlaneProxyFactoryDeployer(
+ multiProvider,
+ );
+ const ismFactoriesFactory = await proxyFactoryDeployer.deploy({
+ [chainName]: config,
+ });
+
+ return serializeContractsMap(ismFactoriesFactory)[chainName];
+ }
+
+ /**
+ * Deploys a Mailbox and its default ISM, hook, and required hook contracts with a given configuration.
+ * @returns The deployed Mailbox contract instance.
+ */
+ static async deployMailbox(params: {
+ config: CoreConfig;
+ proxyAdmin: Address;
+ coreDeployer: HyperlaneCoreDeployer;
+ multiProvider: MultiProvider;
+ chain: ChainNameOrId;
+ }): Promise {
+ const {
+ config,
+ proxyAdmin,
+ coreDeployer: deployer,
+ multiProvider,
+ chain,
+ } = params;
+ const chainName = multiProvider.getChainName(chain);
+
+ const domain = multiProvider.getDomainId(chainName);
+ const mailbox = await deployer.deployProxiedContract(
+ chainName,
+ 'mailbox',
+ 'mailbox',
+ proxyAdmin,
+ [domain],
+ );
+
+ // @todo refactor when 1) IsmModule is ready
+ const deployedDefaultIsm = await deployer.deployIsm(
+ chainName,
+ config.defaultIsm,
+ mailbox.address,
+ );
+
+ // @todo refactor when 1) HookModule is ready, and 2) Hooks Config can handle strings
+ const deployedDefaultHook = await deployer.deployHook(
+ chainName,
+ config.defaultHook,
+ {
+ mailbox: mailbox.address,
+ proxyAdmin,
+ },
+ );
+
+ // @todo refactor when 1) HookModule is ready, and 2) Hooks Config can handle strings
+ const deployedRequiredHook = await deployer.deployHook(
+ chainName,
+ config.requiredHook,
+ {
+ mailbox: mailbox.address,
+ proxyAdmin,
+ },
+ );
+
+ // Initialize Mailbox
+ await multiProvider.handleTx(
+ chain,
+ mailbox.initialize(
+ config.owner,
+ deployedDefaultIsm,
+ deployedDefaultHook.address,
+ deployedRequiredHook.address,
+ multiProvider.getTransactionOverrides(chain),
+ ),
+ );
+ return mailbox;
+ }
+}
diff --git a/typescript/sdk/src/core/EvmIcaModule.hardhat-test.ts b/typescript/sdk/src/core/EvmIcaModule.hardhat-test.ts
new file mode 100644
index 0000000000..cbbb9cbe07
--- /dev/null
+++ b/typescript/sdk/src/core/EvmIcaModule.hardhat-test.ts
@@ -0,0 +1,44 @@
+import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js';
+import { expect } from 'chai';
+import { ethers } from 'ethers';
+import hre from 'hardhat';
+
+import { Mailbox, Mailbox__factory } from '@hyperlane-xyz/core';
+
+import { TestChainName } from '../consts/testChains.js';
+import { MultiProvider } from '../providers/MultiProvider.js';
+
+import { EvmIcaModule } from './EvmIcaModule.js';
+
+describe('EvmIcaModule', async () => {
+ const LOCAL_DOMAIN = 1;
+ let signer: SignerWithAddress;
+ let multiProvider: MultiProvider;
+ let mailbox: Mailbox;
+
+ before(async () => {
+ [signer] = await hre.ethers.getSigners();
+ multiProvider = MultiProvider.createTestMultiProvider({ signer });
+ const Mailbox = new Mailbox__factory(signer);
+ mailbox = await Mailbox.deploy(LOCAL_DOMAIN);
+ });
+ describe('Create', async () => {
+ it('should deploy an ICA with ISM', async () => {
+ const evmIcaModule = await EvmIcaModule.create({
+ chain: TestChainName.test1,
+ config: {
+ mailbox: mailbox.address,
+ owner: signer.address,
+ },
+ multiProvider,
+ });
+
+ const { interchainAccountRouter, interchainAccountIsm } =
+ evmIcaModule.serialize();
+ expect(interchainAccountIsm).to.not.equal(ethers.constants.AddressZero);
+ expect(interchainAccountRouter).to.not.equal(
+ ethers.constants.AddressZero,
+ );
+ });
+ });
+});
diff --git a/typescript/sdk/src/core/EvmIcaModule.ts b/typescript/sdk/src/core/EvmIcaModule.ts
new file mode 100644
index 0000000000..c94e289e97
--- /dev/null
+++ b/typescript/sdk/src/core/EvmIcaModule.ts
@@ -0,0 +1,77 @@
+import { ProtocolType, rootLogger } from '@hyperlane-xyz/utils';
+
+import { serializeContracts } from '../contracts/contracts.js';
+import { HyperlaneAddresses } from '../contracts/types.js';
+import { InterchainAccountDeployer } from '../middleware/account/InterchainAccountDeployer.js';
+import { InterchainAccountFactories } from '../middleware/account/contracts.js';
+import { MultiProvider } from '../providers/MultiProvider.js';
+import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
+import { ProxiedRouterConfig } from '../router/types.js';
+import { ChainNameOrId } from '../types.js';
+
+import {
+ HyperlaneModule,
+ HyperlaneModuleParams,
+} from './AbstractHyperlaneModule.js';
+
+export type InterchainAccountConfig = ProxiedRouterConfig;
+
+export class EvmIcaModule extends HyperlaneModule<
+ ProtocolType.Ethereum,
+ InterchainAccountConfig,
+ HyperlaneAddresses
+> {
+ protected logger = rootLogger.child({ module: 'EvmIcaModule' });
+
+ protected constructor(
+ protected readonly multiProvider: MultiProvider,
+ args: HyperlaneModuleParams<
+ InterchainAccountConfig,
+ HyperlaneAddresses
+ >,
+ ) {
+ super(args);
+ }
+
+ public async read(): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ public async update(
+ _config: InterchainAccountConfig,
+ ): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ /**
+ * Creates a new EvmIcaModule instance by deploying an ICA with an ICA ISM.
+ *
+ * @param chain - The chain on which to deploy the ICA.
+ * @param config - The configuration for the ICA.
+ * @param multiProvider - The MultiProvider instance to use for deployment.
+ * @returns {Promise} - A new EvmIcaModule instance.
+ */
+ public static async create({
+ chain,
+ config,
+ multiProvider,
+ }: {
+ chain: ChainNameOrId;
+ config: InterchainAccountConfig;
+ multiProvider: MultiProvider;
+ }): Promise {
+ const interchainAccountDeployer = new InterchainAccountDeployer(
+ multiProvider,
+ );
+ const deployedContracts = await interchainAccountDeployer.deployContracts(
+ multiProvider.getChainName(chain),
+ config,
+ );
+
+ return new EvmIcaModule(multiProvider, {
+ addresses: serializeContracts(deployedContracts),
+ chain,
+ config,
+ });
+ }
+}
diff --git a/typescript/sdk/src/core/HyperlaneCore.ts b/typescript/sdk/src/core/HyperlaneCore.ts
index 92f0e30230..fbe3ea4987 100644
--- a/typescript/sdk/src/core/HyperlaneCore.ts
+++ b/typescript/sdk/src/core/HyperlaneCore.ts
@@ -8,8 +8,8 @@ import {
AddressBytes32,
ProtocolType,
addressToBytes32,
+ assert,
bytes32ToAddress,
- eqAddress,
messageId,
objFilter,
objMap,
@@ -21,11 +21,13 @@ import { HyperlaneApp } from '../app/HyperlaneApp.js';
import { appFromAddressesMapHelper } from '../contracts/contracts.js';
import { HyperlaneAddressesMap } from '../contracts/types.js';
import { OwnableConfig } from '../deploy/types.js';
+import { DerivedHookConfig, EvmHookReader } from '../hook/EvmHookReader.js';
import { DerivedIsmConfig, EvmIsmReader } from '../ism/EvmIsmReader.js';
-import { IsmType, ModuleType, ismTypeToModuleType } from '../ism/types.js';
+import { BaseMetadataBuilder } from '../ism/metadata/builder.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { RouterConfig } from '../router/types.js';
import { ChainMap, ChainName } from '../types.js';
+import { findMatchingLogEvents } from '../utils/logUtils.js';
import { CoreFactories, coreFactories } from './contracts.js';
import { DispatchedMessage } from './types.js';
@@ -86,12 +88,22 @@ export class HyperlaneCore extends HyperlaneApp {
return this.multiProvider.getChainName(message.parsed.destination);
}
- getRecipientIsmAddress(message: DispatchedMessage): Promise {
+ protected getOrigin(message: DispatchedMessage): ChainName {
+ return this.multiProvider.getChainName(message.parsed.origin);
+ }
+
+ async getRecipientIsmAddress(message: DispatchedMessage): Promise {
const destinationMailbox = this.contractsMap[this.getDestination(message)];
const ethAddress = bytes32ToAddress(message.parsed.recipient);
return destinationMailbox.mailbox.recipientIsm(ethAddress);
}
+ async getHookAddress(message: DispatchedMessage): Promise {
+ const destinationMailbox = this.contractsMap[this.getOrigin(message)];
+ /* TODO: requiredHook() account for here: https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/3693 */
+ return destinationMailbox.mailbox.defaultHook();
+ }
+
async getRecipientIsmConfig(
message: DispatchedMessage,
): Promise {
@@ -101,31 +113,30 @@ export class HyperlaneCore extends HyperlaneApp {
return ismReader.deriveIsmConfig(address);
}
- async buildMetadata(message: DispatchedMessage): Promise {
+ async getHookConfig(message: DispatchedMessage): Promise {
+ const originChain = this.getOrigin(message);
+ const hookReader = new EvmHookReader(this.multiProvider, originChain);
+ const address = await this.getHookAddress(message);
+ const hookConfig = await hookReader.deriveHookConfig(address);
+ assert(hookConfig, `No hook config found for ${address}.`);
+ return hookConfig;
+ }
+
+ async buildMetadata(
+ message: DispatchedMessage,
+ dispatchTx: TransactionReceipt,
+ ): Promise {
const ismConfig = await this.getRecipientIsmConfig(message);
- const destinationChain = this.getDestination(message);
+ const hookConfig = await this.getHookConfig(message);
- switch (ismConfig.type) {
- case IsmType.TRUSTED_RELAYER:
- // eslint-disable-next-line no-case-declarations
- const destinationSigner = await this.multiProvider.getSignerAddress(
- destinationChain,
- );
- if (!eqAddress(destinationSigner, ismConfig.relayer)) {
- this.logger.warn(
- `${destinationChain} signer ${destinationSigner} does not match trusted relayer ${ismConfig.relayer}`,
- );
- }
- }
+ const baseMetadataBuilder = new BaseMetadataBuilder(this);
- // TODO: implement metadata builders for other module types
- const moduleType = ismTypeToModuleType(ismConfig.type);
- switch (moduleType) {
- case ModuleType.NULL:
- return '0x';
- default:
- throw new Error(`Unsupported module type ${moduleType}`);
- }
+ return baseMetadataBuilder.build({
+ ism: ismConfig,
+ hook: hookConfig,
+ message,
+ dispatchTx,
+ });
}
async sendMessage(
@@ -166,8 +177,9 @@ export class HyperlaneCore extends HyperlaneApp {
async relayMessage(
message: DispatchedMessage,
+ dispatchTx: ethers.ContractReceipt,
): Promise {
- const metadata = await this.buildMetadata(message);
+ const metadata = await this.buildMetadata(message, dispatchTx);
const destinationChain = this.getDestination(message);
const mailbox = this.contractsMap[destinationChain].mailbox;
@@ -268,7 +280,7 @@ export class HyperlaneCore extends HyperlaneApp {
async getDispatchTx(
originChain: ChainName,
messageId: string,
- ): Promise {
+ ): Promise {
const mailbox = this.contractsMap[originChain].mailbox;
const filter = mailbox.filters.DispatchId(messageId);
const matchingEvents = await mailbox.queryFilter(filter);
@@ -283,18 +295,11 @@ export class HyperlaneCore extends HyperlaneApp {
sourceTx: ethers.ContractReceipt | ViemTxReceipt,
): DispatchedMessage[] {
const mailbox = Mailbox__factory.createInterface();
- const dispatchLogs = sourceTx.logs
- .map((log) => {
- try {
- return mailbox.parseLog(log);
- } catch (e) {
- return undefined;
- }
- })
- .filter(
- (log): log is ethers.utils.LogDescription =>
- !!log && log.name === 'Dispatch',
- );
+ const dispatchLogs = findMatchingLogEvents(
+ sourceTx.logs,
+ mailbox,
+ 'Dispatch',
+ );
return dispatchLogs.map((log) => {
const message = log.args['message'];
const parsed = parseMessage(message);
diff --git a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts
index 6a06fa88f6..380bcff71e 100644
--- a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts
+++ b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts
@@ -1,5 +1,6 @@
import {
IPostDispatchHook,
+ IPostDispatchHook__factory,
Mailbox,
TestRecipient,
ValidatorAnnounce,
@@ -139,7 +140,7 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer<
await this.configureHook(
chain,
mailbox,
- defaultHook,
+ defaultHook.address,
(_mailbox) => _mailbox.defaultHook(),
(_mailbox, _hook) =>
_mailbox.populateTransaction.setDefaultHook(_hook, { ...overrides }),
@@ -148,7 +149,7 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer<
await this.configureHook(
chain,
mailbox,
- requiredHook,
+ requiredHook.address,
(_mailbox) => _mailbox.requiredHook(),
(_mailbox, _hook) =>
_mailbox.populateTransaction.setRequiredHook(_hook, { ...overrides }),
@@ -184,6 +185,13 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer<
config: HookConfig,
coreAddresses: Partial,
): Promise {
+ if (typeof config === 'string') {
+ return IPostDispatchHook__factory.connect(
+ config,
+ this.multiProvider.getProvider(chain),
+ );
+ }
+
const hooks = await this.hookDeployer.deployContracts(
chain,
config,
diff --git a/typescript/sdk/src/core/schemas.ts b/typescript/sdk/src/core/schemas.ts
new file mode 100644
index 0000000000..22c422326a
--- /dev/null
+++ b/typescript/sdk/src/core/schemas.ts
@@ -0,0 +1,9 @@
+import { HookConfigSchema } from '../hook/schemas.js';
+import { IsmConfigSchema } from '../ism/schemas.js';
+import { OwnableSchema } from '../schemas.js';
+
+export const CoreConfigSchema = OwnableSchema.extend({
+ defaultIsm: IsmConfigSchema,
+ defaultHook: HookConfigSchema,
+ requiredHook: HookConfigSchema,
+});
diff --git a/typescript/sdk/src/core/types.ts b/typescript/sdk/src/core/types.ts
index fa86a50a68..b212906489 100644
--- a/typescript/sdk/src/core/types.ts
+++ b/typescript/sdk/src/core/types.ts
@@ -1,18 +1,16 @@
+import { z } from 'zod';
+
import type { Mailbox } from '@hyperlane-xyz/core';
import type { Address, ParsedMessage } from '@hyperlane-xyz/utils';
import type { UpgradeConfig } from '../deploy/proxy.js';
-import type { CheckerViolation, OwnableConfig } from '../deploy/types.js';
-import { HookConfig } from '../hook/types.js';
+import type { CheckerViolation } from '../deploy/types.js';
import type { IsmConfig } from '../ism/types.js';
import type { ChainName } from '../types.js';
-import { CoreFactories } from './contracts.js';
+import { CoreConfigSchema } from './schemas.js';
-export type CoreConfig = OwnableConfig & {
- defaultIsm: IsmConfig;
- defaultHook: HookConfig;
- requiredHook: HookConfig;
+export type CoreConfig = z.infer & {
remove?: boolean;
upgrade?: UpgradeConfig;
};
diff --git a/typescript/sdk/src/deploy/EvmModuleDeployer.ts b/typescript/sdk/src/deploy/EvmModuleDeployer.ts
new file mode 100644
index 0000000000..fca9e4d4ae
--- /dev/null
+++ b/typescript/sdk/src/deploy/EvmModuleDeployer.ts
@@ -0,0 +1,296 @@
+import { ethers } from 'ethers';
+import { Logger } from 'pino';
+
+import {
+ StaticAddressSetFactory,
+ StaticThresholdAddressSetFactory,
+ TransparentUpgradeableProxy__factory,
+} from '@hyperlane-xyz/core';
+import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js';
+import { Address, rootLogger } from '@hyperlane-xyz/utils';
+
+import { HyperlaneContracts, HyperlaneFactories } from '../contracts/types.js';
+import { MultiProvider } from '../providers/MultiProvider.js';
+import { ChainMap, ChainName } from '../types.js';
+
+import { isProxy, proxyConstructorArgs } from './proxy.js';
+import { ContractVerifier } from './verify/ContractVerifier.js';
+import {
+ ContractVerificationInput,
+ ExplorerLicenseType,
+} from './verify/types.js';
+import { getContractVerificationInput } from './verify/utils.js';
+
+export class EvmModuleDeployer {
+ public verificationInputs: ChainMap = {};
+
+ constructor(
+ protected readonly multiProvider: MultiProvider,
+ protected readonly factories: Factories,
+ protected readonly logger = rootLogger.child({
+ module: 'EvmModuleDeployer',
+ }),
+ protected readonly contractVerifier = new ContractVerifier(
+ multiProvider,
+ {},
+ coreBuildArtifact,
+ ExplorerLicenseType.MIT,
+ ),
+ ) {}
+
+ // Deploys a contract from a factory
+ public async deployContractFromFactory({
+ chain,
+ factory,
+ contractName,
+ constructorArgs,
+ initializeArgs,
+ }: {
+ chain: ChainName;
+ factory: F;
+ contractName: string;
+ constructorArgs: Parameters;
+ initializeArgs?: Parameters>['initialize']>;
+ }): Promise> {
+ this.logger.info(
+ `Deploy ${contractName} on ${chain} with constructor args (${constructorArgs.join(
+ ', ',
+ )})`,
+ );
+ const contract = await this.multiProvider.handleDeploy(
+ chain,
+ factory,
+ constructorArgs,
+ );
+
+ if (initializeArgs) {
+ this.logger.debug(`Initialize ${contractName} on ${chain}`);
+ const overrides = this.multiProvider.getTransactionOverrides(chain);
+ const initTx = await contract.initialize(...initializeArgs, overrides);
+ await this.multiProvider.handleTx(chain, initTx);
+ }
+
+ const verificationInput = getContractVerificationInput(
+ contractName,
+ contract,
+ factory.bytecode,
+ );
+ this.addVerificationArtifacts({ chain, artifacts: [verificationInput] });
+
+ // try verifying contract
+ try {
+ await this.contractVerifier?.verifyContract(chain, verificationInput);
+ } catch (error) {
+ // log error but keep deploying, can also verify post-deployment if needed
+ this.logger.debug(`Error verifying contract: ${error}`);
+ }
+
+ return contract;
+ }
+
+ /**
+ * Deploys a contract with a specified name.
+ *
+ * This function is capable of deploying any contract type defined within the `Factories` type to a specified chain.
+ *
+ * @param {ChainName} chain - The name of the chain on which the contract is to be deployed.
+ * @param {K} contractKey - The key identifying the factory to use for deployment.
+ * @param {string} contractName - The name of the contract to deploy. This must match the contract source code.
+ * @param {Parameters} constructorArgs - Arguments for the contract's constructor.
+ * @param {Parameters>['initialize']>?} initializeArgs - Optional arguments for the contract's initialization function.
+ * @returns {Promise[K]>} A promise that resolves to the deployed contract instance.
+ */
+ public async deployContractWithName({
+ chain,
+ contractKey,
+ contractName,
+ constructorArgs,
+ initializeArgs,
+ }: {
+ chain: ChainName;
+ contractKey: K;
+ contractName: string;
+ constructorArgs: Parameters;
+ initializeArgs?: Parameters<
+ Awaited>['initialize']
+ >;
+ }): Promise[K]> {
+ const contract = await this.deployContractFromFactory({
+ chain,
+ factory: this.factories[contractKey],
+ contractName,
+ constructorArgs,
+ initializeArgs,
+ });
+ return contract;
+ }
+
+ // Deploys a contract with the same name as the contract key
+ public async deployContract({
+ chain,
+ contractKey,
+ constructorArgs,
+ initializeArgs,
+ }: {
+ chain: ChainName;
+ contractKey: K;
+ constructorArgs: Parameters;
+ initializeArgs?: Parameters<
+ Awaited>['initialize']
+ >;
+ }): Promise[K]> {
+ return this.deployContractWithName({
+ chain,
+ contractKey,
+ contractName: contractKey.toString(),
+ constructorArgs,
+ initializeArgs,
+ });
+ }
+
+ // Deploys the Implementation and Proxy for a given contract
+ public async deployProxiedContract({
+ chain,
+ contractKey,
+ contractName,
+ proxyAdmin,
+ constructorArgs,
+ initializeArgs,
+ }: {
+ chain: ChainName;
+ contractKey: K;
+ contractName: string;
+ proxyAdmin: string;
+ constructorArgs: Parameters;
+ initializeArgs?: Parameters[K]['initialize']>;
+ }): Promise[K]> {
+ // Try to initialize the implementation even though it may not be necessary
+ const implementation = await this.deployContractWithName({
+ chain,
+ contractKey,
+ contractName,
+ constructorArgs,
+ initializeArgs,
+ });
+
+ // Initialize the proxy the same way
+ return this.deployProxy({
+ chain,
+ implementation,
+ proxyAdmin,
+ initializeArgs,
+ });
+ }
+
+ // Deploys a proxy for a given implementation contract
+ protected async deployProxy({
+ chain,
+ implementation,
+ proxyAdmin,
+ initializeArgs,
+ }: {
+ chain: ChainName;
+ implementation: C;
+ proxyAdmin: string;
+ initializeArgs?: Parameters;
+ }): Promise {
+ const isProxied = await isProxy(
+ this.multiProvider.getProvider(chain),
+ implementation.address,
+ );
+ if (isProxied) {
+ // if the implementation is already a proxy, do not deploy a new proxy
+ return implementation;
+ }
+
+ const constructorArgs = proxyConstructorArgs(
+ implementation,
+ proxyAdmin,
+ initializeArgs,
+ );
+ const proxy = await this.deployContractFromFactory({
+ chain,
+ factory: new TransparentUpgradeableProxy__factory(),
+ contractName: 'TransparentUpgradeableProxy',
+ constructorArgs,
+ });
+
+ return implementation.attach(proxy.address) as C;
+ }
+
+ // Adds verification artifacts to the verificationInputs map
+ protected addVerificationArtifacts({
+ chain,
+ artifacts,
+ }: {
+ chain: ChainName;
+ artifacts: ContractVerificationInput[];
+ }): void {
+ this.verificationInputs[chain] = this.verificationInputs[chain] || [];
+ artifacts.forEach((artifact) => {
+ this.verificationInputs[chain].push(artifact);
+ });
+ }
+
+ // Static deploy function used by Hook and ISM modules.
+ public static async deployStaticAddressSet({
+ chain,
+ factory,
+ values,
+ logger,
+ threshold = values.length,
+ multiProvider,
+ }: {
+ chain: ChainName;
+ factory: StaticThresholdAddressSetFactory | StaticAddressSetFactory;
+ values: Address[];
+ logger: Logger;
+ threshold?: number;
+ multiProvider: MultiProvider;
+ }): Promise {
+ const address = await factory['getAddress(address[],uint8)'](
+ values,
+ threshold,
+ );
+ const code = await multiProvider.getProvider(chain).getCode(address);
+ if (code === '0x') {
+ logger.debug(
+ `Deploying new ${threshold} of ${values.length} address set to ${chain}`,
+ );
+ const overrides = multiProvider.getTransactionOverrides(chain);
+
+ // estimate gas
+ const estimatedGas = await factory.estimateGas['deploy(address[],uint8)'](
+ values,
+ threshold,
+ overrides,
+ );
+
+ // add 10% buffer
+ const hash = await factory['deploy(address[],uint8)'](values, threshold, {
+ ...overrides,
+ gasLimit: estimatedGas.add(estimatedGas.div(10)), // 10% buffer
+ });
+
+ await multiProvider.handleTx(chain, hash);
+ } else {
+ logger.debug(
+ `Recovered ${threshold} of ${values.length} address set on ${chain}: ${address}`,
+ );
+ }
+
+ // TODO: figure out how to get the constructor arguments for manual deploy TXs
+ // const verificationInput = buildVerificationInput(
+ // NAME,
+ // ADDRESS,
+ // CONSTRUCTOR_ARGS,
+ // );
+ // await this.deployer.verifyContract(
+ // this.chainName,
+ // verificationInput,
+ // logger,
+ // );
+
+ return address;
+ }
+}
diff --git a/typescript/sdk/src/deploy/HyperlaneAppChecker.ts b/typescript/sdk/src/deploy/HyperlaneAppChecker.ts
index fdebf57aba..41419e088c 100644
--- a/typescript/sdk/src/deploy/HyperlaneAppChecker.ts
+++ b/typescript/sdk/src/deploy/HyperlaneAppChecker.ts
@@ -21,12 +21,10 @@ import {
AccessControlViolation,
BytecodeMismatchViolation,
CheckerViolation,
- Owner,
OwnerViolation,
ProxyAdminViolation,
TimelockControllerViolation,
ViolationType,
- resolveOrDeployAccountOwner,
} from './types.js';
export abstract class HyperlaneAppChecker<
@@ -208,19 +206,12 @@ export abstract class HyperlaneAppChecker<
protected async checkOwnership(
chain: ChainName,
- owner: Owner,
+ owner: Address,
ownableOverrides?: Record,
): Promise {
const ownableContracts = await this.ownables(chain);
for (const [name, contract] of Object.entries(ownableContracts)) {
- let expectedOwner = ownableOverrides?.[name] ?? owner;
- if (typeof expectedOwner !== 'string') {
- expectedOwner = await resolveOrDeployAccountOwner(
- this.multiProvider,
- chain,
- expectedOwner,
- );
- }
+ const expectedOwner = ownableOverrides?.[name] ?? owner;
const actual = await contract.owner();
if (!eqAddress(actual, expectedOwner)) {
const violation: OwnerViolation = {
diff --git a/typescript/sdk/src/deploy/HyperlaneDeployer.ts b/typescript/sdk/src/deploy/HyperlaneDeployer.ts
index e6845dda45..0fda4962a5 100644
--- a/typescript/sdk/src/deploy/HyperlaneDeployer.ts
+++ b/typescript/sdk/src/deploy/HyperlaneDeployer.ts
@@ -2,8 +2,6 @@ import { Contract, PopulatedTransaction, ethers } from 'ethers';
import { Logger } from 'pino';
import {
- IPostDispatchHook,
- IPostDispatchHook__factory,
ITransparentUpgradeableProxy,
MailboxClient,
Ownable,
@@ -28,6 +26,7 @@ import {
HyperlaneContractsMap,
HyperlaneFactories,
} from '../contracts/types.js';
+import { HookConfig } from '../hook/types.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { IsmConfig } from '../ism/types.js';
import { moduleMatchesConfig } from '../ism/utils.js';
@@ -65,7 +64,7 @@ export interface DeployerOptions {
}
export abstract class HyperlaneDeployer<
- Config extends object,
+ Config,
Factories extends HyperlaneFactories,
> {
public verificationInputs: ChainMap = {};
@@ -106,6 +105,14 @@ export abstract class HyperlaneDeployer<
this.cachedAddresses = addressesMap;
}
+ async verifyContract(
+ chain: ChainName,
+ input: ContractVerificationInput,
+ logger = this.logger,
+ ): Promise {
+ return this.options.contractVerifier?.verifyContract(chain, input, logger);
+ }
+
abstract deployContracts(
chain: ChainName,
config: Config,
@@ -143,7 +150,7 @@ export abstract class HyperlaneDeployer<
const deployPromise = runWithTimeout(this.chainTimeoutMs, async () => {
const contracts = await this.deployContracts(chain, configMap[chain]);
this.addDeployedContracts(chain, contracts);
- this.logger.info({ chain }, 'Successfully deployed contracts');
+ this.logger.info(`Successfully deployed contracts on ${chain}`);
});
if (this.options.concurrentDeploy) {
deployPromises.push(deployPromise);
@@ -295,32 +302,31 @@ export abstract class HyperlaneDeployer<
protected async configureHook(
chain: ChainName,
contract: C,
- targetHook: IPostDispatchHook,
+ config: HookConfig,
getHook: (contract: C) => Promise,
setHook: (contract: C, hook: Address) => Promise,
): Promise {
+ if (typeof config !== 'string') {
+ throw new Error('Legacy deployer does not support hook objects');
+ }
+
const configuredHook = await getHook(contract);
- if (!eqAddress(targetHook.address, configuredHook)) {
- const result = await this.runIfOwner(chain, contract, async () => {
+ if (!eqAddress(config, configuredHook)) {
+ await this.runIfOwner(chain, contract, async () => {
this.logger.debug(
- `Set hook on ${chain} to ${targetHook.address}, currently is ${configuredHook}`,
+ `Set hook on ${chain} to ${config}, currently is ${configuredHook}`,
);
await this.multiProvider.sendTransaction(
chain,
- setHook(contract, targetHook.address),
+ setHook(contract, config),
);
const actualHook = await getHook(contract);
- if (!eqAddress(targetHook.address, actualHook)) {
+ if (!eqAddress(config, actualHook)) {
throw new Error(
- `Set hook failed on ${chain}, wanted ${targetHook.address}, got ${actualHook}`,
+ `Set hook failed on ${chain}, wanted ${config}, got ${actualHook}`,
);
}
- return true;
});
- // if the signer is not the owner, saving the hook address in the artifacts for later use for sending test messages, etc
- if (!result) {
- this.addDeployedContracts(chain, { customHook: targetHook });
- }
}
}
@@ -336,10 +342,7 @@ export abstract class HyperlaneDeployer<
await this.configureHook(
local,
client,
- IPostDispatchHook__factory.connect(
- config.hook,
- this.multiProvider.getSignerOrProvider(local),
- ),
+ config.hook,
(_client) => _client.hook(),
(_client, _hook) => _client.populateTransaction.setHook(_hook),
);
@@ -723,10 +726,10 @@ export abstract class HyperlaneDeployer<
return ret;
}
- async transferOwnershipOfContracts(
+ async transferOwnershipOfContracts(
chain: ChainName,
- config: OwnableConfig,
- ownables: Partial>,
+ config: OwnableConfig,
+ ownables: Partial>,
): Promise {
const receipts: ethers.ContractReceipt[] = [];
for (const [contractName, ownable] of Object.entries(
@@ -736,7 +739,7 @@ export abstract class HyperlaneDeployer<
continue;
}
const current = await ownable.owner();
- const owner = config.ownerOverrides?.[contractName as K] ?? config.owner;
+ const owner = config.ownerOverrides?.[contractName] ?? config.owner;
if (!eqAddress(current, owner)) {
this.logger.debug(
{ contractName, current, desiredOwner: owner },
diff --git a/typescript/sdk/src/deploy/types.ts b/typescript/sdk/src/deploy/types.ts
index 5c74845a4c..ad78b97b1c 100644
--- a/typescript/sdk/src/deploy/types.ts
+++ b/typescript/sdk/src/deploy/types.ts
@@ -8,44 +8,10 @@ import type {
} from '@hyperlane-xyz/core';
import { Address } from '@hyperlane-xyz/utils';
-import { deployInterchainAccount } from '../middleware/account/InterchainAccount.js';
-import { AccountConfig } from '../middleware/account/types.js';
-import { MultiProvider } from '../providers/MultiProvider.js';
+import { OwnableSchema } from '../schemas.js';
import type { ChainName } from '../types.js';
-import { OwnableConfigSchema } from './schemas.js';
-
-export type Owner = Address | AccountConfig;
-
-/**
- * @remarks ownerOverrides is added outside of the Schema because zod handle generics in a weird way (uses functions)
- * @see https://stackoverflow.com/questions/74907523/creating-zod-schema-for-generic-interface
- */
-export type OwnableConfig = z.infer<
- typeof OwnableConfigSchema
-> & {
- ownerOverrides?: Partial>;
-};
-
-export async function resolveOrDeployAccountOwner(
- multiProvider: MultiProvider,
- chain: ChainName,
- owner: Owner,
-): Promise {
- if (typeof owner === 'string') {
- return owner;
- } else {
- if (!owner.localRouter) {
- throw new Error('localRouter is required for AccountConfig');
- }
- // submits a transaction to deploy an interchain account if the owner is an AccountConfig and the ICA isn't not deployed yet
- return deployInterchainAccount(multiProvider, chain, owner);
- }
-}
-
-export function isOwnableConfig(config: object): config is OwnableConfig {
- return 'owner' in config;
-}
+export type OwnableConfig = z.infer;
export interface CheckerViolation {
chain: ChainName;
diff --git a/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts b/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts
index 0c0cbfc34c..6283d87fa4 100644
--- a/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts
+++ b/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts
@@ -15,7 +15,10 @@ import { MultiProvider } from '../providers/MultiProvider.js';
import { ChainName } from '../types.js';
import { IgpFactories, igpFactories } from './contracts.js';
-import { serializeDifference } from './oracle/types.js';
+import {
+ oracleConfigToOracleData,
+ serializeDifference,
+} from './oracle/types.js';
import { IgpConfig } from './types.js';
export class HyperlaneIgpDeployer extends HyperlaneDeployer<
@@ -122,22 +125,24 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer<
const actual = await gasOracle.remoteGasData(remoteDomain);
+ const desiredData = oracleConfigToOracleData(desired);
+
if (
!actual.gasPrice.eq(desired.gasPrice) ||
!actual.tokenExchangeRate.eq(desired.tokenExchangeRate)
) {
this.logger.info(
- `${chain} -> ${remote}: ${serializeDifference(actual, desired)}`,
+ `${chain} -> ${remote}: ${serializeDifference(actual, desiredData)}`,
);
configsToSet.push({
remoteDomain,
- ...desired,
+ ...desiredData,
});
}
const exampleRemoteGas = (config.overhead[remote] ?? 200_000) + 50_000;
- const exampleRemoteGasCost = desired.tokenExchangeRate
- .mul(desired.gasPrice)
+ const exampleRemoteGasCost = desiredData.tokenExchangeRate
+ .mul(desiredData.gasPrice)
.mul(exampleRemoteGas)
.div(TOKEN_EXCHANGE_RATE_SCALE);
this.logger.info(
diff --git a/typescript/sdk/src/gas/oracle/configure-gas-oracles.hardhat-test.ts b/typescript/sdk/src/gas/oracle/configure-gas-oracles.hardhat-test.ts
index 129ea852d6..8eb66a6edb 100644
--- a/typescript/sdk/src/gas/oracle/configure-gas-oracles.hardhat-test.ts
+++ b/typescript/sdk/src/gas/oracle/configure-gas-oracles.hardhat-test.ts
@@ -11,6 +11,8 @@ import { ChainMap } from '../../types.js';
import { HyperlaneIgpDeployer } from '../HyperlaneIgpDeployer.js';
import { IgpConfig } from '../types.js';
+import { oracleConfigToOracleData } from './types.js';
+
describe('HyperlaneIgpDeployer', () => {
const local = TestChainName.test1;
const remote = TestChainName.test2;
@@ -36,13 +38,15 @@ describe('HyperlaneIgpDeployer', () => {
expect({
gasPrice: deployedConfig.gasPrice,
tokenExchangeRate: deployedConfig.tokenExchangeRate,
- }).to.deep.equal(testConfig[local].oracleConfig![remote]);
+ }).to.deep.equal(
+ oracleConfigToOracleData(testConfig[local].oracleConfig![remote]),
+ );
});
it('should configure new oracle config', async () => {
testConfig[local].oracleConfig![remote] = {
- tokenExchangeRate: utils.parseUnits('2', 'gwei'),
- gasPrice: utils.parseUnits('3', 'gwei'),
+ tokenExchangeRate: utils.parseUnits('2', 'gwei').toString(),
+ gasPrice: utils.parseUnits('3', 'gwei').toString(),
};
const localContracts = await deployer.deployContracts(
@@ -55,6 +59,8 @@ describe('HyperlaneIgpDeployer', () => {
expect({
gasPrice: modifiedConfig.gasPrice,
tokenExchangeRate: modifiedConfig.tokenExchangeRate,
- }).to.deep.equal(testConfig[local].oracleConfig![remote]);
+ }).to.deep.equal(
+ oracleConfigToOracleData(testConfig[local].oracleConfig![remote]),
+ );
});
});
diff --git a/typescript/sdk/src/gas/oracle/types.ts b/typescript/sdk/src/gas/oracle/types.ts
index c4ba2fb457..99669b7ea3 100644
--- a/typescript/sdk/src/gas/oracle/types.ts
+++ b/typescript/sdk/src/gas/oracle/types.ts
@@ -1,21 +1,25 @@
import { ethers } from 'ethers';
-
-import { StorageGasOracle } from '@hyperlane-xyz/core';
+import { z } from 'zod';
import { TOKEN_EXCHANGE_RATE_DECIMALS } from '../../consts/igp.js';
-export enum GasOracleContractType {
- StorageGasOracle = 'StorageGasOracle',
-}
+export const StorageGasOracleConfigSchema = z.object({
+ gasPrice: z.string(),
+ tokenExchangeRate: z.string(),
+});
// Gas data to configure on a single destination chain.
-export type StorageGasOracleConfig = Pick<
- StorageGasOracle.RemoteGasDataConfigStructOutput,
- 'gasPrice' | 'tokenExchangeRate'
+export type StorageGasOracleConfig = z.output<
+ typeof StorageGasOracleConfigSchema
>;
+export type OracleData = {
+ tokenExchangeRate: ethers.BigNumber;
+ gasPrice: ethers.BigNumber;
+};
+
export const formatGasOracleConfig = (
- config: StorageGasOracleConfig,
+ config: OracleData,
): {
tokenExchangeRate: string;
gasPrice: string;
@@ -43,9 +47,17 @@ const serializePercentDifference = (
return diff.isNegative() ? `${diff.toString()}%` : `+${diff.toString()}%`;
};
+// TODO: replace once #3771 is fixed
+export const oracleConfigToOracleData = (
+ config: StorageGasOracleConfig,
+): OracleData => ({
+ gasPrice: ethers.BigNumber.from(config.gasPrice),
+ tokenExchangeRate: ethers.BigNumber.from(config.tokenExchangeRate),
+});
+
export const serializeDifference = (
- actual: StorageGasOracleConfig,
- expected: StorageGasOracleConfig,
+ actual: OracleData,
+ expected: OracleData,
): string => {
const gasPriceDiff = serializePercentDifference(
actual.gasPrice,
diff --git a/typescript/sdk/src/gas/types.ts b/typescript/sdk/src/gas/types.ts
index dacebc2f7f..55114478d2 100644
--- a/typescript/sdk/src/gas/types.ts
+++ b/typescript/sdk/src/gas/types.ts
@@ -1,26 +1,14 @@
import { BigNumber } from 'ethers';
+import { z } from 'zod';
import { InterchainGasPaymaster } from '@hyperlane-xyz/core';
import type { Address } from '@hyperlane-xyz/utils';
-import type { CheckerViolation, OwnableConfig } from '../deploy/types.js';
+import type { CheckerViolation } from '../deploy/types.js';
+import { IgpSchema } from '../hook/schemas.js';
import { ChainMap } from '../types.js';
-import { IgpFactories } from './contracts.js';
-import {
- GasOracleContractType,
- StorageGasOracleConfig,
-} from './oracle/types.js';
-
-export type IgpConfig = OwnableConfig & {
- beneficiary: Address;
- oracleKey: Address;
- overhead: ChainMap;
- // TODO: require this
- oracleConfig?: ChainMap;
- // DEPRECATED
- gasOracleType?: ChainMap;
-};
+export type IgpConfig = z.infer;
export enum IgpViolationType {
Beneficiary = 'Beneficiary',
diff --git a/typescript/sdk/src/hook/EvmHookModule.hardhat-test.ts b/typescript/sdk/src/hook/EvmHookModule.hardhat-test.ts
new file mode 100644
index 0000000000..3a8094eb68
--- /dev/null
+++ b/typescript/sdk/src/hook/EvmHookModule.hardhat-test.ts
@@ -0,0 +1,487 @@
+/* eslint-disable no-console */
+import { expect } from 'chai';
+import hre from 'hardhat';
+
+import {
+ Address,
+ configDeepEquals,
+ normalizeConfig,
+ stringifyObject,
+} from '@hyperlane-xyz/utils';
+
+import { TestChainName, testChains } from '../consts/testChains.js';
+import { HyperlaneAddresses, HyperlaneContracts } from '../contracts/types.js';
+import { TestCoreDeployer } from '../core/TestCoreDeployer.js';
+import { CoreAddresses } from '../core/contracts.js';
+import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
+import { ProxyFactoryFactories } from '../deploy/contracts.js';
+import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
+import { MultiProvider } from '../providers/MultiProvider.js';
+import { randomAddress, randomInt } from '../test/testUtils.js';
+
+import { EvmHookModule } from './EvmHookModule.js';
+import {
+ AggregationHookConfig,
+ DomainRoutingHookConfig,
+ FallbackRoutingHookConfig,
+ HookConfig,
+ HookType,
+ IgpHookConfig,
+ MerkleTreeHookConfig,
+ PausableHookConfig,
+ ProtocolFeeHookConfig,
+} from './types.js';
+
+const hookTypes = Object.values(HookType);
+
+function randomHookType(): HookType {
+ // OP_STACK filtering is temporary until we have a way to deploy the required contracts
+ const filteredHookTypes = hookTypes.filter(
+ (type) => type !== HookType.OP_STACK && type !== HookType.CUSTOM,
+ );
+ return filteredHookTypes[
+ Math.floor(Math.random() * filteredHookTypes.length)
+ ];
+}
+
+function randomProtocolFee(): { maxProtocolFee: string; protocolFee: string } {
+ const maxProtocolFee = Math.random() * 100000000000000;
+ const protocolFee = (Math.random() * maxProtocolFee) / 1000;
+ return {
+ maxProtocolFee: Math.floor(maxProtocolFee).toString(),
+ protocolFee: Math.floor(protocolFee).toString(),
+ };
+}
+
+function randomHookConfig(
+ depth = 0,
+ maxDepth = 2,
+ providedHookType?: HookType,
+): HookConfig {
+ const hookType: HookType = providedHookType ?? randomHookType();
+
+ if (depth >= maxDepth) {
+ if (
+ hookType === HookType.AGGREGATION ||
+ hookType === HookType.ROUTING ||
+ hookType === HookType.FALLBACK_ROUTING
+ ) {
+ return { type: HookType.MERKLE_TREE };
+ }
+ }
+
+ switch (hookType) {
+ case HookType.MERKLE_TREE:
+ return { type: hookType };
+
+ case HookType.AGGREGATION:
+ return {
+ type: hookType,
+ hooks: [
+ randomHookConfig(depth + 1, maxDepth),
+ randomHookConfig(depth + 1, maxDepth),
+ ],
+ };
+
+ case HookType.INTERCHAIN_GAS_PAYMASTER: {
+ const owner = randomAddress();
+ return {
+ owner,
+ type: hookType,
+ beneficiary: randomAddress(),
+ oracleKey: owner,
+ overhead: Object.fromEntries(
+ testChains.map((c) => [c, Math.floor(Math.random() * 100)]),
+ ),
+ oracleConfig: Object.fromEntries(
+ testChains.map((c) => [
+ c,
+ {
+ tokenExchangeRate: randomInt(1234567891234).toString(),
+ gasPrice: randomInt(1234567891234).toString(),
+ },
+ ]),
+ ),
+ };
+ }
+
+ case HookType.PROTOCOL_FEE: {
+ const { maxProtocolFee, protocolFee } = randomProtocolFee();
+ return {
+ owner: randomAddress(),
+ type: hookType,
+ maxProtocolFee,
+ protocolFee,
+ beneficiary: randomAddress(),
+ };
+ }
+
+ case HookType.OP_STACK:
+ return {
+ owner: randomAddress(),
+ type: hookType,
+ nativeBridge: randomAddress(),
+ destinationChain: 'testChain',
+ };
+
+ case HookType.ROUTING:
+ return {
+ owner: randomAddress(),
+ type: hookType,
+ domains: Object.fromEntries(
+ testChains.map((c) => [c, randomHookConfig(depth + 1, maxDepth)]),
+ ),
+ };
+
+ case HookType.FALLBACK_ROUTING:
+ return {
+ owner: randomAddress(),
+ type: hookType,
+ fallback: randomHookConfig(depth + 1, maxDepth),
+ domains: Object.fromEntries(
+ testChains.map((c) => [c, randomHookConfig(depth + 1, maxDepth)]),
+ ),
+ };
+
+ case HookType.PAUSABLE:
+ return {
+ owner: randomAddress(),
+ type: hookType,
+ paused: false,
+ };
+
+ default:
+ throw new Error(`Unsupported Hook type: ${hookType}`);
+ }
+}
+
+describe('EvmHookModule', async () => {
+ let multiProvider: MultiProvider;
+ let coreAddresses: CoreAddresses;
+
+ const chain = TestChainName.test4;
+ let proxyFactoryAddresses: HyperlaneAddresses;
+ let factoryContracts: HyperlaneContracts;
+
+ beforeEach(async () => {
+ const [signer] = await hre.ethers.getSigners();
+ multiProvider = MultiProvider.createTestMultiProvider({ signer });
+
+ const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
+ const contractsMap = await ismFactoryDeployer.deploy(
+ multiProvider.mapKnownChains(() => ({})),
+ );
+
+ // get addresses of factories for the chain
+ factoryContracts = contractsMap[chain];
+ proxyFactoryAddresses = Object.keys(factoryContracts).reduce((acc, key) => {
+ acc[key] =
+ contractsMap[chain][key as keyof ProxyFactoryFactories].address;
+ return acc;
+ }, {} as Record) as HyperlaneAddresses;
+
+ // legacy HyperlaneIsmFactory is required to do a core deploy
+ const legacyIsmFactory = new HyperlaneIsmFactory(
+ contractsMap,
+ multiProvider,
+ );
+
+ // core deployer for tests
+ const testCoreDeployer = new TestCoreDeployer(
+ multiProvider,
+ legacyIsmFactory,
+ );
+
+ // mailbox and proxy admin for the core deploy
+ const { mailbox, proxyAdmin, validatorAnnounce } = (
+ await testCoreDeployer.deployApp()
+ ).getContracts(chain);
+
+ coreAddresses = {
+ mailbox: mailbox.address,
+ proxyAdmin: proxyAdmin.address,
+ validatorAnnounce: validatorAnnounce.address,
+ };
+ });
+
+ // Helper method for checking whether Hook module matches a given config
+ async function hookModuleMatchesConfig({
+ hook,
+ config,
+ }: {
+ hook: EvmHookModule;
+ config: HookConfig;
+ }): Promise {
+ const normalizedDerivedConfig = normalizeConfig(await hook.read());
+ const normalizedConfig = normalizeConfig(config);
+ const matches = configDeepEquals(normalizedDerivedConfig, normalizedConfig);
+ if (!matches) {
+ console.error(
+ 'Derived config:\n',
+ stringifyObject(normalizedDerivedConfig),
+ );
+ console.error('Expected config:\n', stringifyObject(normalizedConfig));
+ }
+ return matches;
+ }
+
+ // hook module and config for testing
+ let testHook: EvmHookModule;
+ let testConfig: HookConfig;
+
+ // expect that the hook matches the config after all tests
+ afterEach(async () => {
+ expect(
+ await hookModuleMatchesConfig({ hook: testHook, config: testConfig }),
+ ).to.be.true;
+ });
+
+ // create a new Hook and verify that it matches the config
+ async function createHook(
+ config: HookConfig,
+ ): Promise<{ hook: EvmHookModule; initialHookAddress: Address }> {
+ const hook = await EvmHookModule.create({
+ chain,
+ config,
+ proxyFactoryFactories: proxyFactoryAddresses,
+ coreAddresses,
+ multiProvider,
+ });
+ testConfig = config;
+ testHook = hook;
+ return { hook, initialHookAddress: hook.serialize().deployedHook };
+ }
+
+ describe('create', async () => {
+ it('deploys a hook of type CUSTOM', async () => {
+ const config: HookConfig = randomAddress();
+ await createHook(config);
+ });
+
+ it('deploys a hook of type MERKLE_TREE', async () => {
+ const config: MerkleTreeHookConfig = {
+ type: HookType.MERKLE_TREE,
+ };
+ await createHook(config);
+ });
+
+ it('deploys a hook of type INTERCHAIN_GAS_PAYMASTER', async () => {
+ const owner = randomAddress();
+ const config: IgpHookConfig = {
+ owner,
+ type: HookType.INTERCHAIN_GAS_PAYMASTER,
+ beneficiary: randomAddress(),
+ oracleKey: owner,
+ overhead: Object.fromEntries(
+ testChains.map((c) => [c, Math.floor(Math.random() * 100)]),
+ ),
+ oracleConfig: Object.fromEntries(
+ testChains.map((c) => [
+ c,
+ {
+ tokenExchangeRate: randomInt(1234567891234).toString(),
+ gasPrice: randomInt(1234567891234).toString(),
+ },
+ ]),
+ ),
+ };
+ await createHook(config);
+ });
+
+ it('deploys a hook of type PROTOCOL_FEE', async () => {
+ const { maxProtocolFee, protocolFee } = randomProtocolFee();
+ const config: ProtocolFeeHookConfig = {
+ owner: randomAddress(),
+ type: HookType.PROTOCOL_FEE,
+ maxProtocolFee,
+ protocolFee,
+ beneficiary: randomAddress(),
+ };
+ await createHook(config);
+ });
+
+ it('deploys a hook of type ROUTING', async () => {
+ const config: DomainRoutingHookConfig = {
+ owner: randomAddress(),
+ type: HookType.ROUTING,
+ domains: Object.fromEntries(
+ testChains
+ .filter((c) => c !== TestChainName.test4)
+ .map((c) => [
+ c,
+ {
+ type: HookType.MERKLE_TREE,
+ },
+ ]),
+ ),
+ };
+ await createHook(config);
+ });
+
+ it('deploys a hook of type FALLBACK_ROUTING', async () => {
+ const config: FallbackRoutingHookConfig = {
+ owner: randomAddress(),
+ type: HookType.FALLBACK_ROUTING,
+ fallback: { type: HookType.MERKLE_TREE },
+ domains: Object.fromEntries(
+ testChains
+ .filter((c) => c !== TestChainName.test4)
+ .map((c) => [
+ c,
+ {
+ type: HookType.MERKLE_TREE,
+ },
+ ]),
+ ),
+ };
+ await createHook(config);
+ });
+
+ it('deploys a hook of type AGGREGATION', async () => {
+ const config: AggregationHookConfig = {
+ type: HookType.AGGREGATION,
+ hooks: [{ type: HookType.MERKLE_TREE }, { type: HookType.MERKLE_TREE }],
+ };
+ await createHook(config);
+ });
+
+ it('deploys a hook of type PAUSABLE', async () => {
+ const config: PausableHookConfig = {
+ owner: randomAddress(),
+ type: HookType.PAUSABLE,
+ paused: false,
+ };
+ await createHook(config);
+ });
+
+ // it('deploys a hook of type OP_STACK', async () => {
+ // need to setup deploying/mocking IL1CrossDomainMessenger before this test can be enabled
+ // const config: OpStackHookConfig = {
+ // owner: randomAddress(),
+ // type: HookType.OP_STACK,
+ // nativeBridge: randomAddress(),
+ // destinationChain: 'testChain',
+ // };
+ // await createHook(config);
+ // });
+
+ for (let i = 0; i < 16; i++) {
+ it(`deploys a random hook config #${i}`, async () => {
+ // random config with depth 0-2
+ const config = randomHookConfig();
+ await createHook(config);
+ });
+ }
+
+ it('regression test #1', async () => {
+ const config: HookConfig = {
+ type: HookType.AGGREGATION,
+ hooks: [
+ {
+ owner: '0xebe67f0a423fd1c4af21debac756e3238897c665',
+ type: HookType.INTERCHAIN_GAS_PAYMASTER,
+ beneficiary: '0xfe3be5940327305aded56f20359761ef85317554',
+ oracleKey: '0xebe67f0a423fd1c4af21debac756e3238897c665',
+ overhead: {
+ test1: 18,
+ test2: 85,
+ test3: 23,
+ test4: 69,
+ },
+ oracleConfig: {
+ test1: {
+ tokenExchangeRate: '1032586497157',
+ gasPrice: '1026942205817',
+ },
+ test2: {
+ tokenExchangeRate: '81451154935',
+ gasPrice: '1231220057593',
+ },
+ test3: {
+ tokenExchangeRate: '31347320275',
+ gasPrice: '21944956734',
+ },
+ test4: {
+ tokenExchangeRate: '1018619796544',
+ gasPrice: '1124484183261',
+ },
+ },
+ },
+ {
+ owner: '0xcc803fc9e6551b9eaaebfabbdd5af3eccea252ff',
+ type: HookType.ROUTING,
+ domains: {
+ test1: {
+ type: HookType.MERKLE_TREE,
+ },
+ test2: {
+ owner: '0x7e43dfa88c4a5d29a8fcd69883b7f6843d465ca3',
+ type: HookType.INTERCHAIN_GAS_PAYMASTER,
+ beneficiary: '0x762e71a849a3825613cf5cbe70bfff27d0fe7766',
+ oracleKey: '0x7e43dfa88c4a5d29a8fcd69883b7f6843d465ca3',
+ overhead: {
+ test1: 46,
+ test2: 34,
+ test3: 47,
+ test4: 24,
+ },
+ oracleConfig: {
+ test1: {
+ tokenExchangeRate: '1132883204938',
+ gasPrice: '1219466305935',
+ },
+ test2: {
+ tokenExchangeRate: '938422264723',
+ gasPrice: '229134538568',
+ },
+ test3: {
+ tokenExchangeRate: '69699594189',
+ gasPrice: '475781234236',
+ },
+ test4: {
+ tokenExchangeRate: '1027245678936',
+ gasPrice: '502686418976',
+ },
+ },
+ },
+ test3: {
+ type: HookType.MERKLE_TREE,
+ },
+ test4: {
+ owner: '0xa1ce72b70566f2cba6000bfe6af50f0f358f49d7',
+ type: HookType.INTERCHAIN_GAS_PAYMASTER,
+ beneficiary: '0x9796c0c49c61fe01eb1a8ba56d09b831f6da8603',
+ oracleKey: '0xa1ce72b70566f2cba6000bfe6af50f0f358f49d7',
+ overhead: {
+ test1: 71,
+ test2: 16,
+ test3: 37,
+ test4: 13,
+ },
+ oracleConfig: {
+ test1: {
+ tokenExchangeRate: '443874625350',
+ gasPrice: '799154764503',
+ },
+ test2: {
+ tokenExchangeRate: '915348561750',
+ gasPrice: '1124345797215',
+ },
+ test3: {
+ tokenExchangeRate: '930832717805',
+ gasPrice: '621743941770',
+ },
+ test4: {
+ tokenExchangeRate: '147394981623',
+ gasPrice: '766494385983',
+ },
+ },
+ },
+ },
+ },
+ ],
+ };
+ await createHook(config);
+ });
+ });
+});
diff --git a/typescript/sdk/src/hook/EvmHookModule.ts b/typescript/sdk/src/hook/EvmHookModule.ts
index c6e01bc87c..612043dd43 100644
--- a/typescript/sdk/src/hook/EvmHookModule.ts
+++ b/typescript/sdk/src/hook/EvmHookModule.ts
@@ -1,53 +1,654 @@
-import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils';
+import { BigNumber, ethers } from 'ethers';
+import {
+ DomainRoutingHook,
+ DomainRoutingHook__factory,
+ FallbackDomainRoutingHook,
+ IL1CrossDomainMessenger__factory,
+ IPostDispatchHook__factory,
+ InterchainGasPaymaster,
+ OPStackHook,
+ OPStackIsm__factory,
+ PausableHook,
+ ProtocolFee,
+ StaticAggregationHook,
+ StaticAggregationHookFactory__factory,
+ StaticAggregationHook__factory,
+ StorageGasOracle,
+} from '@hyperlane-xyz/core';
+import {
+ Address,
+ ProtocolType,
+ addressToBytes32,
+ assert,
+ configDeepEquals,
+ rootLogger,
+} from '@hyperlane-xyz/utils';
+
+import { TOKEN_EXCHANGE_RATE_SCALE } from '../consts/igp.js';
import { HyperlaneAddresses } from '../contracts/types.js';
import {
HyperlaneModule,
- HyperlaneModuleArgs,
+ HyperlaneModuleParams,
} from '../core/AbstractHyperlaneModule.js';
-import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js';
+import { CoreAddresses } from '../core/contracts.js';
+import { EvmModuleDeployer } from '../deploy/EvmModuleDeployer.js';
+import { ProxyFactoryFactories } from '../deploy/contracts.js';
+import { ContractVerifier } from '../deploy/verify/ContractVerifier.js';
+import { IgpFactories, igpFactories } from '../gas/contracts.js';
+import { IgpConfig } from '../gas/types.js';
+import { EvmIsmModule } from '../ism/EvmIsmModule.js';
+import { IsmType, OpStackIsmConfig } from '../ism/types.js';
import { MultiProvider } from '../providers/MultiProvider.js';
-import { EthersV5Transaction } from '../providers/ProviderType.js';
+import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
+import { ChainNameOrId } from '../types.js';
import { EvmHookReader } from './EvmHookReader.js';
-import { HookFactories } from './contracts.js';
-import { HookConfig } from './types.js';
+import { DeployedHook, HookFactories, hookFactories } from './contracts.js';
+import {
+ AggregationHookConfig,
+ DomainRoutingHookConfig,
+ FallbackRoutingHookConfig,
+ HookConfig,
+ HookType,
+ IgpHookConfig,
+ OpStackHookConfig,
+ PausableHookConfig,
+ ProtocolFeeHookConfig,
+} from './types.js';
+
+type HookModuleAddresses = {
+ deployedHook: Address;
+ mailbox: Address;
+ proxyAdmin: Address;
+};
-// WIP example implementation of EvmHookModule
export class EvmHookModule extends HyperlaneModule<
ProtocolType.Ethereum,
HookConfig,
- HyperlaneAddresses & {
- deployedHook: Address;
- }
+ HyperlaneAddresses & HookModuleAddresses
> {
- protected logger = rootLogger.child({ module: 'EvmHookModule' });
- protected reader: EvmHookReader;
+ protected readonly logger = rootLogger.child({ module: 'EvmHookModule' });
+ protected readonly reader: EvmHookReader;
+ protected readonly deployer: EvmModuleDeployer;
+
+ // Adding these to reduce how often we need to grab from MultiProvider.
+ public readonly chain: string;
+ // We use domainId here because MultiProvider.getDomainId() will always
+ // return a number, and EVM the domainId and chainId are the same.
+ public readonly domainId: number;
+
+ // Transaction overrides for the chain
+ protected readonly txOverrides: Partial;
protected constructor(
protected readonly multiProvider: MultiProvider,
- protected readonly deployer: HyperlaneDeployer,
- args: HyperlaneModuleArgs<
+ args: HyperlaneModuleParams<
HookConfig,
- HyperlaneAddresses & {
- deployedHook: Address;
- }
+ HyperlaneAddresses & HookModuleAddresses
>,
+ contractVerifier?: ContractVerifier,
) {
super(args);
- this.reader = new EvmHookReader(multiProvider, args.chain);
+
+ this.reader = new EvmHookReader(multiProvider, this.args.chain);
+ this.deployer = new EvmModuleDeployer(
+ multiProvider,
+ {
+ ...hookFactories,
+ ...igpFactories,
+ },
+ this.logger,
+ contractVerifier,
+ );
+
+ this.chain = this.multiProvider.getChainName(this.args.chain);
+ this.domainId = this.multiProvider.getDomainId(this.chain);
+
+ this.txOverrides = this.multiProvider.getTransactionOverrides(this.chain);
}
public async read(): Promise {
- return await this.reader.deriveHookConfig(this.args.addresses.deployedHook);
+ if (typeof this.args.config === 'string') {
+ return this.args.addresses.deployedHook;
+ } else {
+ const hookConfig = await this.reader.deriveHookConfig(
+ this.args.addresses.deployedHook,
+ );
+ assert(
+ hookConfig,
+ `No hook config found for ${this.args.addresses.deployedHook}`,
+ );
+ return hookConfig;
+ }
}
- public async update(_config: HookConfig): Promise {
+ public async update(_config: HookConfig): Promise {
throw new Error('Method not implemented.');
}
// manually write static create function
- public static create(_config: HookConfig): Promise {
- throw new Error('not implemented');
+ public static async create({
+ chain,
+ config,
+ proxyFactoryFactories,
+ coreAddresses,
+ multiProvider,
+ }: {
+ chain: ChainNameOrId;
+ config: HookConfig;
+ proxyFactoryFactories: HyperlaneAddresses;
+ coreAddresses: CoreAddresses;
+ multiProvider: MultiProvider;
+ }): Promise {
+ // instantiate new EvmHookModule
+ const module = new EvmHookModule(multiProvider, {
+ addresses: {
+ ...proxyFactoryFactories,
+ ...coreAddresses,
+ deployedHook: ethers.constants.AddressZero,
+ },
+ chain,
+ config,
+ });
+
+ // deploy hook and assign address to module
+ const deployedHook = await module.deploy({ config });
+ module.args.addresses.deployedHook = deployedHook.address;
+
+ return module;
+ }
+
+ // Compute delta between current and target domain configurations
+ protected async computeRoutingHooksToSet({
+ currentDomains,
+ targetDomains,
+ }: {
+ currentDomains: DomainRoutingHookConfig['domains'];
+ targetDomains: DomainRoutingHookConfig['domains'];
+ }): Promise {
+ const routingHookUpdates: DomainRoutingHook.HookConfigStruct[] = [];
+
+ // Iterate over the target domains and compare with the current configuration
+ for (const [dest, targetDomainConfig] of Object.entries(targetDomains)) {
+ const destDomain = this.multiProvider.tryGetDomainId(dest);
+ if (!destDomain) {
+ this.logger.warn(`Domain not found in MultiProvider: ${dest}`);
+ continue;
+ }
+
+ // If the domain is not in the current config or the config has changed, deploy a new hook
+ // TODO: in-place updates per domain as a future optimization
+ if (!configDeepEquals(currentDomains[dest], targetDomainConfig)) {
+ const domainHook = await this.deploy({
+ config: targetDomainConfig,
+ });
+
+ routingHookUpdates.push({
+ destination: destDomain,
+ hook: domainHook.address,
+ });
+ }
+ }
+
+ return routingHookUpdates;
+ }
+
+ // Updates a routing hook
+ protected async updateRoutingHook({
+ current,
+ target,
+ }: {
+ current: DomainRoutingHookConfig | FallbackRoutingHookConfig;
+ target: DomainRoutingHookConfig | FallbackRoutingHookConfig;
+ }): Promise {
+ // Deploy a new fallback hook if the fallback config has changed
+ if (
+ target.type === HookType.FALLBACK_ROUTING &&
+ !configDeepEquals(
+ target.fallback,
+ (current as FallbackRoutingHookConfig).fallback,
+ )
+ ) {
+ const hook = await this.deploy({ config: target });
+ this.args.addresses.deployedHook = hook.address;
+ }
+
+ const routingUpdates = await this.computeRoutingHooksToSet({
+ currentDomains: current.domains,
+ targetDomains: target.domains,
+ });
+
+ // Return if no updates are required
+ if (routingUpdates.length === 0) {
+ return [];
+ }
+
+ // Create tx for setting hooks
+ return [
+ {
+ annotation: 'Updating routing hooks...',
+ chainId: this.domainId,
+ to: this.args.addresses.deployedHook,
+ data: DomainRoutingHook__factory.createInterface().encodeFunctionData(
+ 'setHooks',
+ [routingUpdates],
+ ),
+ },
+ ];
+ }
+
+ protected async deploy({
+ config,
+ }: {
+ config: HookConfig;
+ }): Promise {
+ // If it's an address, just return a base Hook
+ if (typeof config === 'string') {
+ // TODO: https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3773
+ // we can remove the ts-ignore once we have a proper type for address Hooks
+ // @ts-ignore
+ return IPostDispatchHook__factory.connect(
+ config,
+ this.multiProvider.getSignerOrProvider(this.args.chain),
+ );
+ }
+
+ switch (config.type) {
+ case HookType.MERKLE_TREE:
+ return this.deployer.deployContract({
+ chain: this.chain,
+ contractKey: HookType.MERKLE_TREE,
+ constructorArgs: [this.args.addresses.mailbox],
+ });
+ case HookType.INTERCHAIN_GAS_PAYMASTER:
+ return this.deployIgpHook({ config });
+ case HookType.AGGREGATION:
+ return this.deployAggregationHook({ config });
+ case HookType.PROTOCOL_FEE:
+ return this.deployProtocolFeeHook({ config });
+ case HookType.OP_STACK:
+ return this.deployOpStackHook({ config });
+ case HookType.ROUTING:
+ case HookType.FALLBACK_ROUTING:
+ return this.deployRoutingHook({ config });
+ case HookType.PAUSABLE: {
+ return this.deployPausableHook({ config });
+ }
+ default:
+ throw new Error(`Unsupported hook config: ${config}`);
+ }
+ }
+
+ protected async deployProtocolFeeHook({
+ config,
+ }: {
+ config: ProtocolFeeHookConfig;
+ }): Promise {
+ this.logger.debug('Deploying ProtocolFeeHook...');
+ return this.deployer.deployContract({
+ chain: this.chain,
+ contractKey: HookType.PROTOCOL_FEE,
+ constructorArgs: [
+ config.maxProtocolFee,
+ config.protocolFee,
+ config.beneficiary,
+ config.owner,
+ ],
+ });
+ }
+
+ protected async deployPausableHook({
+ config,
+ }: {
+ config: PausableHookConfig;
+ }): Promise {
+ this.logger.debug('Deploying PausableHook...');
+ const hook = await this.deployer.deployContract({
+ chain: this.chain,
+ contractKey: HookType.PAUSABLE,
+ constructorArgs: [],
+ });
+
+ // transfer ownership
+ await this.multiProvider.handleTx(
+ this.chain,
+ hook.transferOwnership(config.owner, this.txOverrides),
+ );
+
+ return hook;
+ }
+
+ protected async deployAggregationHook({
+ config,
+ }: {
+ config: AggregationHookConfig;
+ }): Promise {
+ this.logger.debug('Deploying AggregationHook...');
+
+ // deploy subhooks
+ const aggregatedHooks = [];
+ for (const hookConfig of config.hooks) {
+ const { address } = await this.deploy({ config: hookConfig });
+ aggregatedHooks.push(address);
+ }
+
+ // deploy aggregation hook
+ this.logger.debug(
+ `Deploying aggregation hook of type ${config.hooks.map((h) =>
+ typeof h === 'string' ? h : h.type,
+ )}...`,
+ );
+ const signer = this.multiProvider.getSigner(this.chain);
+ const factory = StaticAggregationHookFactory__factory.connect(
+ this.args.addresses.staticAggregationHookFactory,
+ signer,
+ );
+ const address = await EvmModuleDeployer.deployStaticAddressSet({
+ chain: this.chain,
+ factory,
+ values: aggregatedHooks,
+ logger: this.logger,
+ multiProvider: this.multiProvider,
+ });
+
+ // return aggregation hook
+ return StaticAggregationHook__factory.connect(address, signer);
+ }
+
+ protected async deployOpStackHook({
+ config,
+ }: {
+ config: OpStackHookConfig;
+ }): Promise {
+ const chain = this.chain;
+ const mailbox = this.args.addresses.mailbox;
+ this.logger.debug(
+ 'Deploying OPStackHook for %s to %s...',
+ chain,
+ config.destinationChain,
+ );
+
+ // fetch l2 messenger address from l1 messenger
+ const l1Messenger = IL1CrossDomainMessenger__factory.connect(
+ config.nativeBridge,
+ this.multiProvider.getSignerOrProvider(chain),
+ );
+ const l2Messenger: Address = await l1Messenger.OTHER_MESSENGER();
+ // deploy opstack ism
+ const ismConfig: OpStackIsmConfig = {
+ type: IsmType.OP_STACK,
+ origin: chain,
+ nativeBridge: l2Messenger,
+ };
+
+ // deploy opstack ism
+ const opStackIsmAddress = (
+ await EvmIsmModule.create({
+ chain: config.destinationChain,
+ config: ismConfig,
+ proxyFactoryFactories: this.args.addresses,
+ mailbox: mailbox,
+ multiProvider: this.multiProvider,
+ })
+ ).serialize().deployedIsm;
+
+ // connect to ISM
+ const opstackIsm = OPStackIsm__factory.connect(
+ opStackIsmAddress,
+ this.multiProvider.getSignerOrProvider(config.destinationChain),
+ );
+
+ // deploy opstack hook
+ const hook = await this.deployer.deployContract({
+ chain,
+ contractKey: HookType.OP_STACK,
+ constructorArgs: [
+ mailbox,
+ this.multiProvider.getDomainId(config.destinationChain),
+ addressToBytes32(opstackIsm.address),
+ config.nativeBridge,
+ ],
+ });
+
+ // set authorized hook on opstack ism
+ const authorizedHook = await opstackIsm.authorizedHook();
+ if (authorizedHook === addressToBytes32(hook.address)) {
+ this.logger.debug(
+ 'Authorized hook already set on ism %s',
+ opstackIsm.address,
+ );
+ return hook;
+ } else if (
+ authorizedHook !== addressToBytes32(ethers.constants.AddressZero)
+ ) {
+ this.logger.debug(
+ 'Authorized hook mismatch on ism %s, expected %s, got %s',
+ opstackIsm.address,
+ addressToBytes32(hook.address),
+ authorizedHook,
+ );
+ throw new Error('Authorized hook mismatch');
+ }
+
+ // check if mismatch and redeploy hook
+ this.logger.debug(
+ 'Setting authorized hook %s on ism % on destination %s',
+ hook.address,
+ opstackIsm.address,
+ config.destinationChain,
+ );
+ await this.multiProvider.handleTx(
+ config.destinationChain,
+ opstackIsm.setAuthorizedHook(
+ addressToBytes32(hook.address),
+ this.multiProvider.getTransactionOverrides(config.destinationChain),
+ ),
+ );
+
+ return hook;
+ }
+
+ protected async deployRoutingHook({
+ config,
+ }: {
+ config: DomainRoutingHookConfig | FallbackRoutingHookConfig;
+ }): Promise {
+ // originally set owner to deployer so we can set hooks
+ const deployerAddress = await this.multiProvider.getSignerAddress(
+ this.chain,
+ );
+
+ let routingHook: DomainRoutingHook | FallbackDomainRoutingHook;
+ if (config.type === HookType.FALLBACK_ROUTING) {
+ // deploy fallback hook
+ const fallbackHook = await this.deploy({ config: config.fallback });
+ // deploy routing hook with fallback
+ routingHook = await this.deployer.deployContract({
+ chain: this.chain,
+ contractKey: HookType.FALLBACK_ROUTING,
+ constructorArgs: [
+ this.args.addresses.mailbox,
+ deployerAddress,
+ fallbackHook.address,
+ ],
+ });
+ } else {
+ // deploy routing hook
+ routingHook = await this.deployer.deployContract({
+ chain: this.chain,
+ contractKey: HookType.ROUTING,
+ constructorArgs: [this.args.addresses.mailbox, deployerAddress],
+ });
+ }
+
+ // compute the hooks that need to be set
+ const hooksToSet = await this.computeRoutingHooksToSet({
+ currentDomains: {},
+ targetDomains: config.domains,
+ });
+
+ // set hooks
+ await this.multiProvider.handleTx(
+ this.chain,
+ routingHook.setHooks(hooksToSet, this.txOverrides),
+ );
+
+ // transfer ownership
+ await this.multiProvider.handleTx(
+ this.chain,
+ routingHook.transferOwnership(config.owner, this.txOverrides),
+ );
+
+ // return a fully configured routing hook
+ return routingHook;
+ }
+
+ protected async deployIgpHook({
+ config,
+ }: {
+ config: IgpHookConfig;
+ }): Promise {
+ this.logger.debug('Deploying IGP as hook...');
+
+ // Deploy the StorageGasOracle
+ const storageGasOracle = await this.deployStorageGasOracle({
+ config,
+ });
+
+ // Deploy the InterchainGasPaymaster
+ const interchainGasPaymaster = await this.deployInterchainGasPaymaster({
+ storageGasOracle,
+ config,
+ });
+
+ return interchainGasPaymaster;
+ }
+
+ protected async deployInterchainGasPaymaster({
+ storageGasOracle,
+ config,
+ }: {
+ storageGasOracle: StorageGasOracle;
+ config: IgpConfig;
+ }): Promise {
+ const deployerAddress = await this.multiProvider.getSignerAddress(
+ this.chain,
+ );
+
+ const igp = await this.deployer.deployProxiedContract({
+ chain: this.chain,
+ contractKey: HookType.INTERCHAIN_GAS_PAYMASTER,
+ contractName: HookType.INTERCHAIN_GAS_PAYMASTER,
+ proxyAdmin: this.args.addresses.proxyAdmin,
+ constructorArgs: [],
+ initializeArgs: [deployerAddress, config.beneficiary],
+ });
+
+ const gasParamsToSet: InterchainGasPaymaster.GasParamStruct[] = [];
+ for (const [remote, gasOverhead] of Object.entries(config.overhead)) {
+ // Note: non-EVM remotes actually *are* supported, provided that the remote domain is in the MultiProvider.
+ // Previously would check core metadata for non EVMs and fallback to multiprovider for custom EVMs
+ const remoteDomain = this.multiProvider.tryGetDomainId(remote);
+ if (!remoteDomain) {
+ this.logger.warn(
+ `Skipping overhead ${this.chain} -> ${remote}. Expected if the remote is a non-EVM chain.`,
+ );
+ continue;
+ }
+
+ this.logger.debug(
+ `Setting gas params for ${this.chain} -> ${remote}: gasOverhead = ${gasOverhead} gasOracle = ${storageGasOracle.address}`,
+ );
+ gasParamsToSet.push({
+ remoteDomain,
+ config: {
+ gasOverhead,
+ gasOracle: storageGasOracle.address,
+ },
+ });
+ }
+
+ if (gasParamsToSet.length > 0) {
+ await this.multiProvider.handleTx(
+ this.chain,
+ igp.setDestinationGasConfigs(gasParamsToSet, this.txOverrides),
+ );
+ }
+
+ // Transfer igp to the configured owner
+ await this.multiProvider.handleTx(
+ this.chain,
+ igp.transferOwnership(config.owner, this.txOverrides),
+ );
+
+ return igp;
+ }
+
+ protected async deployStorageGasOracle({
+ config,
+ }: {
+ config: IgpConfig;
+ }): Promise {
+ const gasOracle = await this.deployer.deployContract({
+ chain: this.chain,
+ contractKey: 'storageGasOracle',
+ constructorArgs: [],
+ });
+
+ if (!config.oracleConfig) {
+ this.logger.debug('No oracle config provided, skipping...');
+ return gasOracle;
+ }
+
+ this.logger.info(`Configuring gas oracle from ${this.chain}...`);
+ const configsToSet: Array = [];
+
+ for (const [remote, desired] of Object.entries(config.oracleConfig)) {
+ // Note: non-EVM remotes actually *are* supported, provided that the remote domain is in the MultiProvider.
+ // Previously would check core metadata for non EVMs and fallback to multiprovider for custom EVMs
+ const remoteDomain = this.multiProvider.tryGetDomainId(remote);
+ if (!remoteDomain) {
+ this.logger.warn(
+ `Skipping gas oracle ${this.chain} -> ${remote}.` +
+ ' Expected if the remote is a non-EVM chain or the remote domain is not the in the MultiProvider.',
+ );
+ continue;
+ }
+
+ configsToSet.push({
+ remoteDomain,
+ ...desired,
+ });
+
+ // Log an example remote gas cost
+ const exampleRemoteGas = (config.overhead[remote] ?? 200_000) + 50_000;
+ const exampleRemoteGasCost = BigNumber.from(desired.tokenExchangeRate)
+ .mul(desired.gasPrice)
+ .mul(exampleRemoteGas)
+ .div(TOKEN_EXCHANGE_RATE_SCALE);
+ this.logger.info(
+ `${
+ this.chain
+ } -> ${remote}: ${exampleRemoteGas} remote gas cost: ${ethers.utils.formatEther(
+ exampleRemoteGasCost,
+ )}`,
+ );
+ }
+
+ if (configsToSet.length > 0) {
+ await this.multiProvider.handleTx(
+ this.chain,
+ gasOracle.setRemoteGasDataConfigs(configsToSet, this.txOverrides),
+ );
+ }
+
+ // Transfer gas oracle to the configured owner
+ await this.multiProvider.handleTx(
+ this.chain,
+ gasOracle.transferOwnership(config.oracleKey, this.txOverrides),
+ );
+
+ return gasOracle;
}
}
diff --git a/typescript/sdk/src/hook/EvmHookReader.test.ts b/typescript/sdk/src/hook/EvmHookReader.test.ts
index 77af11b990..5650b12acc 100644
--- a/typescript/sdk/src/hook/EvmHookReader.test.ts
+++ b/typescript/sdk/src/hook/EvmHookReader.test.ts
@@ -1,5 +1,6 @@
import { expect } from 'chai';
import { ethers } from 'ethers';
+import { randomBytes } from 'ethers/lib/utils.js';
import sinon from 'sinon';
import {
@@ -117,11 +118,13 @@ describe('EvmHookReader', () => {
it('should derive pausable config correctly', async () => {
const mockAddress = generateRandomAddress();
const mockOwner = generateRandomAddress();
+ const mockPaused = randomBytes(1)[0] % 2 === 0;
// Mocking the connect method + returned what we need from contract object
const mockContract = {
hookType: sandbox.stub().resolves(OnchainHookType.PAUSABLE),
owner: sandbox.stub().resolves(mockOwner),
+ paused: sandbox.stub().resolves(mockPaused),
};
sandbox
.stub(PausableHook__factory, 'connect')
@@ -132,6 +135,7 @@ describe('EvmHookReader', () => {
const expectedConfig: WithAddress = {
owner: mockOwner,
+ paused: mockPaused,
address: mockAddress,
type: HookType.PAUSABLE,
};
@@ -182,6 +186,27 @@ describe('EvmHookReader', () => {
expect(config).to.deep.equal(hookConfig);
});
+ it('should return an empty config if deriving fails', async () => {
+ const mockAddress = generateRandomAddress();
+ const mockOwner = generateRandomAddress();
+
+ // Mocking the connect method + returned what we need from contract object
+ const mockContract = {
+ // No type
+ owner: sandbox.stub().resolves(mockOwner),
+ };
+ sandbox
+ .stub(MerkleTreeHook__factory, 'connect')
+ .returns(mockContract as unknown as MerkleTreeHook);
+ sandbox
+ .stub(IPostDispatchHook__factory, 'connect')
+ .returns(mockContract as unknown as IPostDispatchHook);
+
+ // top-level method infers hook type
+ const hookConfig = await evmHookReader.deriveHookConfig(mockAddress);
+ expect(hookConfig).to.be.undefined;
+ });
+
/*
Testing for more nested hook types can be done manually by reading from existing contracts onchain.
Examples of nested hook types include:
diff --git a/typescript/sdk/src/hook/EvmHookReader.ts b/typescript/sdk/src/hook/EvmHookReader.ts
index c5db5165c1..b57fa8ede1 100644
--- a/typescript/sdk/src/hook/EvmHookReader.ts
+++ b/typescript/sdk/src/hook/EvmHookReader.ts
@@ -20,6 +20,7 @@ import {
assert,
concurrentMap,
eqAddress,
+ getLogLevel,
rootLogger,
} from '@hyperlane-xyz/utils';
@@ -42,10 +43,12 @@ import {
RoutingHookConfig,
} from './types.js';
-export type DerivedHookConfig = WithAddress;
+export type DerivedHookConfig = WithAddress