Create new CLI package (#2573)

### Description

- Create new CLI package
- Upgrade prettier, typescript, and lint packages
- Update TSConfig output target to ES2020
- Migrate code from hyp-deploy repo to `typescript/cli/src`

### Related issues

Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2566
Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2786

### Backward compatibility

Yes

### Testing

Tested all commands locally
Tested core deployments with and without pre-existing addresses (artifacts)
Created e2e tests for deploying core, warp, and sending messages

---------

Co-authored-by: Nam Chu Hoai <nambrot@googlemail.com>
pull/2814/head
J M Rossy 1 year ago committed by GitHub
parent 9b1824aff5
commit fd48f137c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .eslintrc
  2. 3
      .github/CODEOWNERS
  3. 19
      .github/workflows/node.yml
  4. 1
      Dockerfile
  5. 4
      README.md
  6. 11
      package.json
  7. 9
      solidity/package.json
  8. 2
      typescript/cli/.eslintignore
  9. 5
      typescript/cli/.eslintrc
  10. 5
      typescript/cli/.gitignore
  11. 57
      typescript/cli/README.md
  12. 164
      typescript/cli/ci-test.sh
  13. 40
      typescript/cli/cli.ts
  14. 22
      typescript/cli/examples/anvil-chains.yaml
  15. 44
      typescript/cli/examples/chain-config.yaml
  16. 20
      typescript/cli/examples/contract-artifacts.yaml
  17. 20
      typescript/cli/examples/multisig-ism.yaml
  18. 26
      typescript/cli/examples/warp-tokens.yaml
  19. 54
      typescript/cli/logger.ts
  20. 58
      typescript/cli/package.json
  21. 73
      typescript/cli/src/commands/chains.ts
  22. 171
      typescript/cli/src/commands/config.ts
  23. 120
      typescript/cli/src/commands/deploy.ts
  24. 47
      typescript/cli/src/commands/options.ts
  25. 141
      typescript/cli/src/commands/send.ts
  26. 38
      typescript/cli/src/commands/status.ts
  27. 22
      typescript/cli/src/config/artifacts.ts
  28. 108
      typescript/cli/src/config/chain.ts
  29. 120
      typescript/cli/src/config/multisig.ts
  30. 126
      typescript/cli/src/config/warp.ts
  31. 4
      typescript/cli/src/consts.ts
  32. 51
      typescript/cli/src/context.ts
  33. 64
      typescript/cli/src/deploy/TestRecipientDeployer.ts
  34. 444
      typescript/cli/src/deploy/core.ts
  35. 27
      typescript/cli/src/deploy/types.ts
  36. 46
      typescript/cli/src/deploy/utils.ts
  37. 361
      typescript/cli/src/deploy/warp.ts
  38. 142
      typescript/cli/src/send/message.ts
  39. 196
      typescript/cli/src/send/transfer.ts
  40. 36
      typescript/cli/src/status/message.ts
  41. 44
      typescript/cli/src/utils/balances.ts
  42. 88
      typescript/cli/src/utils/chains.ts
  43. 178
      typescript/cli/src/utils/files.ts
  44. 18
      typescript/cli/src/utils/keys.ts
  45. 6
      typescript/cli/src/utils/time.ts
  46. 10
      typescript/cli/tsconfig.json
  47. 2
      typescript/helloworld/.eslintrc
  48. 19
      typescript/helloworld/package.json
  49. 16
      typescript/infra/package.json
  50. 2
      typescript/infra/src/config/environment.ts
  51. 2
      typescript/infra/src/roles.ts
  52. 13
      typescript/sdk/package.json
  53. 5
      typescript/sdk/src/app/HyperlaneApp.ts
  54. 26
      typescript/sdk/src/consts/agentStartBlocks.ts
  55. 7
      typescript/sdk/src/consts/environments/index.ts
  56. 7
      typescript/sdk/src/core/HyperlaneCore.ts
  57. 4
      typescript/sdk/src/core/MultiProtocolCore.test.ts
  58. 7
      typescript/sdk/src/core/MultiProtocolCore.ts
  59. 2
      typescript/sdk/src/core/adapters/EvmCoreAdapter.ts
  60. 4
      typescript/sdk/src/core/adapters/SealevelCoreAdapter.ts
  61. 2
      typescript/sdk/src/core/adapters/types.ts
  62. 1
      typescript/sdk/src/index.ts
  63. 1
      typescript/sdk/src/ism/HyperlaneIsmFactory.ts
  64. 15
      typescript/sdk/src/metadata/ChainMetadataManager.ts
  65. 21
      typescript/token/package.json
  66. 2
      typescript/token/tsconfig.json
  67. 7
      typescript/utils/package.json
  68. 2
      typescript/utils/src/types.ts
  69. 913
      yarn.lock

@ -31,7 +31,7 @@
"@typescript-eslint/no-require-imports": ["warn"],
"@typescript-eslint/no-unused-vars": [
"error",
{
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
@ -48,4 +48,4 @@
}
]
}
}
}

@ -22,5 +22,8 @@ typescript/token @yorhodes @jmrossy @tkporter @aroralanuk
## Hello World
typescript/helloworld @yorhodes @nambrot
## CLI
typescript/cli @jmrossy @yorhodes
## Infra
typescript/infra @tkporter @nambrot

@ -122,6 +122,25 @@ jobs:
- name: infra
run: yarn workspace @hyperlane-xyz/infra run test
test-cli:
runs-on: ubuntu-latest
needs: [yarn-build]
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Install Foundry
uses: onbjerg/foundry-toolchain@v1
- uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}
- name: test
run: ./typescript/cli/ci-test.sh
test-env:
runs-on: ubuntu-latest
needs: [yarn-build]

@ -15,6 +15,7 @@ COPY typescript/utils/package.json ./typescript/utils/
COPY typescript/sdk/package.json ./typescript/sdk/
COPY typescript/helloworld/package.json ./typescript/helloworld/
COPY typescript/token/package.json ./typescript/token/
COPY typescript/cli/package.json ./typescript/cli/
COPY typescript/infra/package.json ./typescript/infra/
COPY solidity/package.json ./solidity/

@ -7,8 +7,8 @@
[codecov-badge]: https://img.shields.io/codecov/c/github/hyperlane-xyz/hyperlane-monorepo
[foundry]: https://getfoundry.sh/
[foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg
[license]: https://opensource.org/licenses/MIT
[license-badge]: https://img.shields.io/badge/License-MIT-blue.svg
[license]: https://www.apache.org/licenses/LICENSE-2.0
[license-badge]: https://img.shields.io/badge/License-Apache-blue.svg
## Versioning

@ -3,14 +3,14 @@
"description": "A yarn workspace of core Hyperlane packages",
"version": "0.0.0",
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^3.2.0",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.5.0",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"husky": "^8.0.0",
"lint-staged": "^12.4.3",
"prettier": "^2.4.1"
"prettier": "^2.8.8"
},
"packageManager": "yarn@3.2.0",
"private": true,
@ -37,6 +37,7 @@
"lodash": "^4.17.21",
"recursive-readdir": "^2.2.3",
"underscore": "^1.13",
"undici": "^5.11"
"undici": "^5.11",
"@trivago/prettier-plugin-sort-imports/@babel/parser": "^7.22.7"
}
}

@ -1,10 +1,10 @@
{
"name": "@hyperlane-xyz/core",
"description": "Core solidity contracts for Hyperlane",
"version": "1.5.1",
"version": "1.5.4-beta0",
"dependencies": {
"@eth-optimism/contracts": "^0.6.0",
"@hyperlane-xyz/utils": "1.5.1",
"@hyperlane-xyz/utils": "1.5.4-beta0",
"@openzeppelin/contracts": "^4.8.0",
"@openzeppelin/contracts-upgradeable": "^4.8.0"
},
@ -18,7 +18,7 @@
"ethers": "^5.7.2",
"hardhat": "^2.16.1",
"hardhat-gas-reporter": "^1.0.9",
"prettier": "^2.4.1",
"prettier": "^2.8.8",
"prettier-plugin-solidity": "^1.0.0-beta.5",
"solhint": "^3.3.2",
"solhint-plugin-prettier": "^0.0.5",
@ -56,5 +56,6 @@
"gas-ci": "yarn gas --check --tolerance 2 || (echo 'Manually update gas snapshot' && exit 1)",
"slither": "slither ."
},
"types": "dist/index.d.ts"
"types": "dist/index.d.ts",
"stableVersion": "1.5.3"
}

@ -0,0 +1,2 @@
node_modules
dist

@ -0,0 +1,5 @@
{
"rules": {
"no-console": ["off"]
}
}

@ -0,0 +1,5 @@
.env*
/dist
/cache
/configs
/artifacts

@ -0,0 +1,57 @@
# Hyperlane CLI
The Hyperlane CLI is a command-line tool written in Typescript that facilitates common operations on Hyperlane, such as deploying the core contracts and/or warp routes to new chains.
## Hyperlane overview
Hyperlane is an interchain messaging protocol that allows applications to communicate between blockchains.
Developers can use Hyperlane to share state between blockchains, allowing them to build interchain applications that live natively across multiple chains.
To read more about interchain applications, how the protocol works, and how to integrate with Hyperlane, please see the [documentation](https://docs.hyperlane.xyz).
## Setup
Node 16 or newer is required.
**Option 1: Global install:**
```bash
# Install with NPM
npm install -g @hyperlane-xyz/cli
# Or uninstall old versions
npm uninstall -g @hyperlane-xyz/cli
```
**Option 2: Temp install:**
```bash
# Run via NPM's npx command
npx @hyperlane-xyz/cli
# Or via Yarn's dlx command
yarn dlx @hyperlane-xyz/cli
```
**Option 3: Run from source:**
```bash
git clone https://github.com/hyperlane-xyz/hyperlane-monorepo.git
cd hyperlane-monorepo
yarn install && yarn build
cd typescript/cli
yarn hyperlane
```
## Common commands
View help: `hyperlane --help`
Create a core deployment config: `hyperlane config create`
Run hyperlane core deployments: `hyperlane deploy core`
Run warp route deployments: `hyperlane deploy warp`
View SDK contract addresses: `hyperlane chains addresses`
Send test message: `hyperlane send message`

@ -0,0 +1,164 @@
#!/usr/bin/env bash
# Optional cleanup for previous runs, useful when running locally
pkill -f anvil
docker ps -aq | xargs docker stop | xargs docker rm
rm -rf /tmp/anvil*
rm -rf /tmp/relayer
# Setup directories for anvil chains
for CHAIN in anvil1 anvil2
do
mkdir -p /tmp/$CHAIN /tmp/$CHAIN/state /tmp/$CHAIN/validator /tmp/relayer
chmod -R 777 /tmp/relayer /tmp/$CHAIN
done
# Optional: remove the --block-time 1 to speedup tests for local runs
anvil --chain-id 31337 -p 8545 --state /tmp/anvil1/state --block-time 1 > /dev/null &
anvil --chain-id 31338 -p 8555 --state /tmp/anvil2/state --block-time 1 > /dev/null &
sleep 1
set -e
echo "{}" > /tmp/empty-artifacts.json
echo "Deploying contracts to anvil1"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy core \
--chains ./examples/anvil-chains.yaml \
--artifacts /tmp/empty-artifacts.json \
--out /tmp \
--ism ./examples/multisig-ism.yaml \
--origin anvil1 --remotes anvil2 \
--key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--yes
CORE_ARTIFACTS_PATH=`find /tmp/core-deployment* -type f -exec ls -t1 {} + | head -1`
echo "Deploying contracts to anvil2"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy core \
--chains ./examples/anvil-chains.yaml \
--artifacts $CORE_ARTIFACTS_PATH \
--out /tmp \
--ism ./examples/multisig-ism.yaml \
--origin anvil2 --remotes anvil1 \
--key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--yes
CORE_ARTIFACTS_PATH=`find /tmp/core-deployment* -type f -exec ls -t1 {} + | head -1`
echo "Core artifacts:"
cat $CORE_ARTIFACTS_PATH
AGENT_CONFIG_FILENAME=`ls -t1 /tmp | grep agent-config | head -1`
echo "Deploying warp routes"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy warp \
--chains ./examples/anvil-chains.yaml \
--core $CORE_ARTIFACTS_PATH \
--config ./examples/warp-tokens.yaml \
--out /tmp \
--key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--yes
echo "Sending test message"
yarn workspace @hyperlane-xyz/cli run hyperlane send message \
--origin anvil1 \
--destination anvil2 \
--chains ./examples/anvil-chains.yaml \
--core $CORE_ARTIFACTS_PATH \
--quick \
--key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
| tee /tmp/message1
MESSAGE1_ID=`cat /tmp/message1 | grep "Message ID" | grep -E -o '0x[0-9a-f]+'`
echo "Message 1 ID: $MESSAGE1_ID"
WARP_ARTIFACTS_FILE=`find /tmp/warp-deployment* -type f -exec ls -t1 {} + | head -1`
ANVIL1_ROUTER=`cat $WARP_ARTIFACTS_FILE | jq -r ".anvil1.router"`
echo "Sending test warp transfer"
yarn workspace @hyperlane-xyz/cli run hyperlane send transfer \
--origin anvil1 \
--destination anvil2 \
--chains ./examples/anvil-chains.yaml \
--core $CORE_ARTIFACTS_PATH \
--router $ANVIL1_ROUTER \
--type native \
--quick \
--key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
| tee /tmp/message2
MESSAGE2_ID=`cat /tmp/message2 | grep "Message ID" | grep -E -o '0x[0-9a-f]+'`
echo "Message 2 ID: $MESSAGE2_ID"
if [[ $OSTYPE == 'darwin'* ]]; then
# Required because the -net=host driver only works on linux
DOCKER_CONNECTION_URL="http://host.docker.internal"
else
DOCKER_CONNECTION_URL="http://127.0.0.1"
fi
for i in "anvil1 8545 ANVIL1" "anvil2 8555 ANVIL2"
do
set -- $i
echo "Running validator on $1"
docker run \
--mount type=bind,source="/tmp",target=/data --net=host \
-e CONFIG_FILES=/data/${AGENT_CONFIG_FILENAME} -e HYP_VALIDATOR_ORIGINCHAINNAME=$1 \
-e HYP_VALIDATOR_REORGPERIOD=0 -e HYP_VALIDATOR_INTERVAL=1 \
-e HYP_BASE_CHAINS_${3}_CONNECTION_URL=${DOCKER_CONNECTION_URL}:${2} \
-e HYP_VALIDATOR_VALIDATOR_TYPE=hexKey \
-e HYP_VALIDATOR_VALIDATOR_KEY=0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 \
-e HYP_VALIDATOR_CHECKPOINTSYNCER_TYPE=localStorage \
-e HYP_VALIDATOR_CHECKPOINTSYNCER_PATH=/data/${1}/validator \
-e HYP_BASE_TRACING_LEVEL=debug -e HYP_BASE_TRACING_FMT=compact \
gcr.io/abacus-labs-dev/hyperlane-agent:main ./validator > /tmp/${1}/validator-logs.txt &
done
echo "Validator running, sleeping to let it sync"
sleep 15
echo "Done sleeping"
echo "Validator Announcement:"
cat /tmp/anvil1/validator/announcement.json
echo "Running relayer"
# Won't work on anything but linux due to -net=host
# Replace CONNECTION_URL with host.docker.internal on mac
docker run \
--mount type=bind,source="/tmp",target=/data --net=host \
-e CONFIG_FILES=/data/${AGENT_CONFIG_FILENAME} \
-e HYP_BASE_CHAINS_ANVIL1_CONNECTION_URL=${DOCKER_CONNECTION_URL}:8545 \
-e HYP_BASE_CHAINS_ANVIL2_CONNECTION_URL=${DOCKER_CONNECTION_URL}:8555 \
-e HYP_BASE_TRACING_LEVEL=debug -e HYP_BASE_TRACING_FMT=compact \
-e HYP_RELAYER_RELAYCHAINS=anvil1,anvil2 \
-e HYP_RELAYER_ALLOWLOCALCHECKPOINTSYNCERS=true -e HYP_RELAYER_DB=/data/relayer \
-e HYP_RELAYER_GASPAYMENTENFORCEMENT='[{"type":"none"}]' \
-e HYP_BASE_CHAINS_ANVIL1_SIGNER_TYPE=hexKey \
-e HYP_BASE_CHAINS_ANVIL1_SIGNER_KEY=0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97 \
-e HYP_BASE_CHAINS_ANVIL2_SIGNER_TYPE=hexKey \
-e HYP_BASE_CHAINS_ANVIL2_SIGNER_KEY=0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97 \
gcr.io/abacus-labs-dev/hyperlane-agent:main ./relayer > /tmp/relayer/relayer-logs.txt &
sleep 5
echo "Done running relayer, checking message delivery statuses"
for i in "1 $MESSAGE1_ID" "2 $MESSAGE2_ID"
do
set -- $i
echo "Checking delivery status of $1: $2"
yarn workspace @hyperlane-xyz/cli run hyperlane status \
--id $2 \
--destination anvil2 \
--chains ./examples/anvil-chains.yaml \
--core $CORE_ARTIFACTS_PATH \
| tee /tmp/message-status-$1
if ! grep -q "$2 was delivered" /tmp/message-status-$1; then
echo "ERROR: Message $1 was not delivered"
exit 1
else
echo "Message $1 was delivered!"
fi
done
docker ps -aq | xargs docker stop | xargs docker rm
pkill -f anvil

@ -0,0 +1,40 @@
#! /usr/bin/env node
import chalk from 'chalk';
import yargs from 'yargs';
import { errorRed } from './logger.js';
import { chainsCommand } from './src/commands/chains.js';
import { configCommand } from './src/commands/config.js';
import { deployCommand } from './src/commands/deploy.js';
import { sendCommand } from './src/commands/send.js';
import { statusCommand } from './src/commands/status.js';
// From yargs code:
const MISSING_PARAMS_ERROR = 'Not enough non-option arguments';
console.log(chalk.blue('Hyperlane'), chalk.magentaBright('CLI'));
try {
await yargs(process.argv.slice(2))
.scriptName('hyperlane')
// TODO get version num from package.json
.version(false)
.command(chainsCommand)
.command(configCommand)
.command(deployCommand)
.command(sendCommand)
.command(statusCommand)
.demandCommand()
.strict()
.help()
.fail((msg, err, yargs) => {
if (msg && !msg.includes(MISSING_PARAMS_ERROR)) errorRed('Error: ' + msg);
console.log('');
yargs.showHelp();
console.log('');
if (err) errorRed(err.toString());
process.exit(1);
}).argv;
} catch (error: any) {
errorRed('Error: ' + error.message);
}

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

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

@ -0,0 +1,20 @@
# Artifacts representing contract addresses
# Typically not written by hand but generated as JSON by the deploy command
# Consists of a map of chain names to contract names to addresses
---
anvil1:
storageGasOracle: '0xD9A9966E7dA9a7f0032bF449FB12696a638E673C'
validatorAnnounce: '0x9bBdef63594D5FFc2f370Fe52115DdFFe97Bc524'
proxyAdmin: '0x90f9a2E9eCe93516d65FdaB726a3c62F5960a1b9'
mailbox: '0x35231d4c2D8B8ADcB5617A638A0c4548684c7C70'
interchainGasPaymaster: '0x6cA0B6D22da47f091B7613223cD4BB03a2d77918'
defaultIsmInterchainGasPaymaster: '0x56f52c0A1ddcD557285f7CBc782D3d83096CE1Cc'
multisigIsm: '0x9bDE63104EE030d9De419EEd6bA7D14b86D6fE3f'
testRecipient: '0x36FdA966CfffF8a9Cdc814f546db0e6378bFef35'
interchainAccountIsm: '0x5e8ee6840caa4f367aff1FF28aA36D5B1b836d35'
aggregationIsmFactory: '0xc864fa3B662613cA5051f41e157d0a997f9a5A87'
routingIsmFactory: '0x1fdfD1486b8339638C6b92f8a96D698D8182D2b1'
interchainQueryRouter: '0xA837e38C3F7D509DF3a7a0fCf65E3814DB6c2618'
interchainAccountRouter: '0x9521291A43ebA3aD3FD24d610F4b7F7543C8d761'
merkleRootMultisigIsmFactory: '0x82140b2ddAd4E4dd7e1D6757Fb5F9485c230B79d'
messageIdMultisigIsmFactory: '0x1079056da3EC7D55521F27e1E094015C0d39Cc65'

@ -0,0 +1,20 @@
# A config for a multisig Interchain Security Module (ISM)
# Schema: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/ism/types.ts
#
# Valid module types:
# routing
# aggregation
# legacy_multisig
# merkle_root_multisig
# message_id_multisig
---
anvil1:
type: 'legacy_multisig' # TODO: update for v3
threshold: 1 # Number: Signatures required to approve a message
validators: # Array: List of validator addresses
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
anvil2:
type: 'legacy_multisig' # TODO: update for v3
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'

@ -0,0 +1,26 @@
# A config for a Warp Route deployment
# Typically used with the 'hyperlane deploy warp' command
#
# Token Types:
# native
# collateral
# synthetic
# collateralUri
# syntheticUri
# fastCollateral
# fastSynthetic
---
base:
chainName: anvil1
type: native
# address: 0x123... # Required for collateral types
# isNft: true # If the token is an NFT (ERC721), set to true
# owner: 0x123 # Optional owner address for synthetic token
# mailbox: 0x123 # Optional mailbox address route
# interchainGasPaymaster: 0x123 # Optional interchainGasPaymaster address
synthetics:
- chainName: anvil2
# You can optionally set the token metadata, otherwise the base token's will be used
# name: "MySyntheticToken"
# symbol: "MST"
# totalSupply: 10000000

@ -0,0 +1,54 @@
// This file isn't in the src dir so it it's imported before others after import sort
// See bigint hack below and https://github.com/trivago/prettier-plugin-sort-imports/issues/112
import chalk from 'chalk';
import debug from 'debug';
// Workaround for bug in bigint-buffer which solana-web3.js depends on
// https://github.com/no2chem/bigint-buffer/issues/31#issuecomment-1752134062
const defaultWarn = console.warn;
console.warn = (...args: any) => {
if (
args &&
typeof args[0] === 'string' &&
args[0]?.includes('bigint: Failed to load bindings')
)
return;
defaultWarn(...args);
};
const HYPERLANE_NS = 'hyperlane';
// Default root logger for use in utils/scripts
export const logger = debug(HYPERLANE_NS);
export const error = debug(`${HYPERLANE_NS}:ERROR`);
export function createLogger(namespace: string, isError = false) {
return isError ? error.extend(namespace) : logger.extend(namespace);
}
// Ensure hyperlane logging is enabled by forcing inclusion of hyperlane namespace
const activeNamespaces = debug.disable();
const otherNamespaces = activeNamespaces
.split(',')
.filter((ns) => ns.includes(HYPERLANE_NS));
const hypNamespaces = `${HYPERLANE_NS},${HYPERLANE_NS}:*`;
debug.enable(
otherNamespaces ? `${otherNamespaces},${hypNamespaces}` : `${hypNamespaces}`,
);
// Change Debug's output format to remove prefixes + postfixes
function formatArgs(this: debug.Debugger, args: any[]) {
args.push(debug.humanize(this.diff));
args.pop();
}
debug.formatArgs = formatArgs;
// Colored logs directly to console
export const logBlue = (...args: any) => console.log(chalk.blue(...args));
export const logPink = (...args: any) =>
console.log(chalk.magentaBright(...args));
export const logGray = (...args: any) => console.log(chalk.gray(...args));
export const logGreen = (...args: any) => console.log(chalk.green(...args));
export const logRed = (...args: any) => console.log(chalk.red(...args));
export const errorRed = (...args: any) => console.error(chalk.red(...args));
export const log = (...args: any) => console.log(...args);

@ -0,0 +1,58 @@
{
"name": "@hyperlane-xyz/cli",
"version": "1.5.4-beta0",
"description": "A command-line utility for common Hyperlane operations",
"dependencies": {
"@hyperlane-xyz/hyperlane-token": "1.5.4-beta0",
"@hyperlane-xyz/sdk": "1.5.4-beta0",
"@inquirer/prompts": "^3.0.0",
"chalk": "^5.3.0",
"ethers": "^5.7.2",
"yaml": "^2.3.1",
"yargs": "^17.7.2",
"zod": "^3.21.2"
},
"devDependencies": {
"@types/node": "^18.14.5",
"@types/yargs": "^17.0.24",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"prettier": "^2.8.8",
"typescript": "^5.1.6"
},
"scripts": {
"hyperlane": "node ./dist/cli.js",
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf ./dist",
"lint": "eslint . --ext .ts",
"prettier": "prettier --write ./src ./examples"
},
"files": [
"./dist",
"./examples"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"type": "module",
"bin": {
"hyperlane": "./dist/cli.js"
},
"repository": {
"type": "git",
"url": "https://github.com/hyperlane-xyz/hyperlane-monorepo"
},
"license": "Apache 2.0",
"homepage": "https://www.hyperlane.xyz",
"keywords": [
"Hyperlane",
"CLI",
"Permissionless",
"Deployment",
"Typescript"
],
"packageManager": "yarn@3.2.0",
"stableVersion": "1.5.3"
}

@ -0,0 +1,73 @@
import { CommandModule } from 'yargs';
import {
Chains,
CoreChainName,
Mainnets,
Testnets,
chainMetadata,
hyperlaneContractAddresses,
} from '@hyperlane-xyz/sdk';
import { log, logBlue, logGray } from '../../logger.js';
/**
* Parent command
*/
export const chainsCommand: CommandModule = {
command: 'chains',
describe: 'View information about core Hyperlane chains',
builder: (yargs) =>
yargs
.command(listCommand)
.command(addressesCommand)
.version(false)
.demandCommand(),
handler: () => log('Command required'),
};
/**
* List command
*/
const listCommand: CommandModule = {
command: 'list',
describe: 'List all core chains included in the Hyperlane SDK',
handler: () => {
logBlue('Hyperlane core mainnet chains:');
logGray('------------------------------');
log(Mainnets.map((chain) => chainMetadata[chain].displayName).join(', '));
log('');
logBlue('Hyperlane core testnet chains:');
logGray('------------------------------');
log(Testnets.map((chain) => chainMetadata[chain].displayName).join(', '));
},
};
/**
* Addresses command
*/
const addressesCommand: CommandModule = {
command: 'addresses',
describe: 'Display the addresses of core Hyperlane contracts',
builder: (yargs) =>
yargs.options({
name: {
type: 'string',
description: 'Chain to display addresses for',
choices: Object.values(Chains),
alias: 'chain',
},
}),
handler: (args) => {
const name = args.name as CoreChainName | undefined;
if (name && hyperlaneContractAddresses[name]) {
logBlue('Hyperlane contract addresses for:', name);
logGray('---------------------------------');
log(JSON.stringify(hyperlaneContractAddresses[name], null, 2));
} else {
logBlue('Hyperlane core contract addresses:');
logGray('----------------------------------');
log(JSON.stringify(hyperlaneContractAddresses, null, 2));
}
},
};

@ -0,0 +1,171 @@
import { CommandModule } from 'yargs';
import { log, logGreen } from '../../logger.js';
import { createChainConfig, readChainConfig } from '../config/chain.js';
import {
createMultisigConfig,
readMultisigConfig,
} from '../config/multisig.js';
import { createWarpConfig, readWarpRouteConfig } from '../config/warp.js';
import { FileFormat } from '../utils/files.js';
import {
chainsCommandOption,
fileFormatOption,
outputFileOption,
} from './options.js';
/**
* Parent command
*/
export const configCommand: CommandModule = {
command: 'config',
describe: 'Create or validate Hyperlane configs',
builder: (yargs) =>
yargs
.command(createCommand)
.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(createChainCommand)
.command(createMultisigCommand)
.command(createWarpCommand)
.version(false)
.demandCommand(),
handler: () => log('Command required'),
};
const createChainCommand: CommandModule = {
command: 'chain',
describe: 'Create a new, minimal Hyperlane chain config (aka chain metadata)',
builder: (yargs) =>
yargs.options({
output: outputFileOption('./configs/chain-config.yaml'),
format: fileFormatOption,
}),
handler: async (argv: any) => {
const format: FileFormat = argv.format;
const outPath: string = argv.output;
await createChainConfig({ format, outPath });
process.exit(0);
},
};
const createMultisigCommand: CommandModule = {
command: 'multisig',
describe: 'Create a new Multisig ISM config',
builder: (yargs) =>
yargs.options({
output: outputFileOption('./configs/multisig-ism.yaml'),
format: fileFormatOption,
chains: chainsCommandOption,
}),
handler: async (argv: any) => {
const format: FileFormat = argv.format;
const outPath: string = argv.output;
const chainConfigPath: string = argv.chains;
await createMultisigConfig({ format, outPath, chainConfigPath });
process.exit(0);
},
};
const createWarpCommand: CommandModule = {
command: 'warp',
describe: 'Create a new Warp Route tokens config',
builder: (yargs) =>
yargs.options({
output: outputFileOption('./configs/warp-tokens.yaml'),
format: fileFormatOption,
chains: chainsCommandOption,
}),
handler: async (argv: any) => {
const format: FileFormat = argv.format;
const outPath: string = argv.output;
const chainConfigPath: string = argv.chains;
await createWarpConfig({ format, outPath, chainConfigPath });
process.exit(0);
},
};
/**
* Validate commands
*/
const validateCommand: CommandModule = {
command: 'validate',
describe: 'Validate a config in a YAML or JSON file',
builder: (yargs) =>
yargs
.command(validateChainCommand)
.command(validateMultisigCommand)
.command(validateWarpCommand)
.version(false)
.demandCommand(),
handler: () => log('Command required'),
};
const validateChainCommand: CommandModule = {
command: 'chain',
describe: 'Validate a chain config in a YAML or JSON file',
builder: (yargs) =>
yargs.options({
path: {
type: 'string',
description: 'Input file path',
demandOption: true,
},
}),
handler: async (argv) => {
const path = argv.path as string;
readChainConfig(path);
process.exit(0);
},
};
const validateMultisigCommand: CommandModule = {
command: 'multisig',
describe: 'Validate a multisig ism config in a YAML or JSON file',
builder: (yargs) =>
yargs.options({
path: {
type: 'string',
description: 'Input file path',
demandOption: true,
},
}),
handler: async (argv) => {
const path = argv.path as string;
readMultisigConfig(path);
logGreen('Config is valid');
process.exit(0);
},
};
const validateWarpCommand: CommandModule = {
command: 'warp',
describe: 'Validate a Warp Route config in a YAML or JSON file',
builder: (yargs) =>
yargs.options({
path: {
type: 'string',
description: 'Input file path',
demandOption: true,
},
}),
handler: async (argv) => {
const path = argv.path as string;
readWarpRouteConfig(path);
logGreen('Config is valid');
process.exit(0);
},
};

@ -0,0 +1,120 @@
import { CommandModule } from 'yargs';
import { log, logGray } from '../../logger.js';
import { runCoreDeploy } from '../deploy/core.js';
import { runWarpDeploy } from '../deploy/warp.js';
import {
chainsCommandOption,
coreArtifactsOption,
keyCommandOption,
outDirCommandOption,
skipConfirmationOption,
} from './options.js';
/**
* Parent command
*/
export const deployCommand: CommandModule = {
command: 'deploy',
describe: 'Permisionslessly deploy a Hyperlane contracts or extensions',
builder: (yargs) =>
yargs
.command(coreCommand)
.command(warpCommand)
.version(false)
.demandCommand(),
handler: () => log('Command required'),
};
/**
* Core command
*/
const coreCommand: CommandModule = {
command: 'core',
describe: 'Deploy core Hyperlane contracts',
builder: (yargs) =>
yargs.options({
key: keyCommandOption,
chains: chainsCommandOption,
out: outDirCommandOption,
artifacts: coreArtifactsOption,
ism: {
type: 'string',
description:
'A path to a JSON or YAML file with ISM configs (e.g. Multisig)',
},
origin: {
type: 'string',
description: 'Name of chain to which contracts will be deployed',
},
remotes: {
type: 'string',
description:
'Comma separated list of chain names to which origin will be connected',
},
yes: skipConfirmationOption,
}),
handler: async (argv: any) => {
logGray('Hyperlane permissionless core deployment');
logGray('----------------------------------------');
const key: string = argv.key || process.env.HYP_KEY;
const chainConfigPath: string = argv.chains;
const outPath: string = argv.out;
const origin: string | undefined = argv.origin;
const remotes: string[] | undefined = argv.remotes
? argv.remotes.split(',').map((r: string) => r.trim())
: undefined;
const artifactsPath: string = argv.artifacts;
const ismConfigPath: string = argv.ism;
const skipConfirmation: boolean = argv.yes;
await runCoreDeploy({
key,
chainConfigPath,
artifactsPath,
ismConfigPath,
outPath,
origin,
remotes,
skipConfirmation,
});
process.exit(0);
},
};
/**
* Warp command
*/
const warpCommand: CommandModule = {
command: 'warp',
describe: 'Deploy Warp Route contracts',
builder: (yargs) =>
yargs.options({
key: keyCommandOption,
chains: chainsCommandOption,
out: outDirCommandOption,
core: coreArtifactsOption,
config: {
type: 'string',
description: 'A path to a JSON or YAML file with a warp config.',
},
yes: skipConfirmationOption,
}),
handler: async (argv: any) => {
const key: string = argv.key || process.env.HYP_KEY;
const chainConfigPath: string = argv.chains;
const warpConfigPath: string | undefined = argv.config;
const coreArtifactsPath: string | undefined = argv.core;
const outPath: string = argv.out;
const skipConfirmation: boolean = argv.yes;
await runWarpDeploy({
key,
chainConfigPath,
warpConfigPath,
coreArtifactsPath,
outPath,
skipConfirmation,
});
process.exit(0);
},
};

@ -0,0 +1,47 @@
// A set of common options
import { Options } from 'yargs';
export const keyCommandOption: Options = {
type: 'string',
description:
'A hex private key or seed phrase for transaction signing. Or use the HYP_KEY env var',
};
export const chainsCommandOption: Options = {
type: 'string',
description: 'A path to a JSON or YAML file with chain configs',
default: './configs/chain-config.yaml',
};
export const outDirCommandOption: Options = {
type: 'string',
description: 'A folder name output artifacts into',
default: './artifacts',
};
export const coreArtifactsOption: Options = {
type: 'string',
description: 'File path to core deployment output artifacts',
};
export const fileFormatOption: Options = {
type: 'string',
alias: 'f',
description: 'Output file format',
choices: ['json', 'yaml'],
default: 'yaml',
};
export const outputFileOption = (defaultPath: string): Options => ({
type: 'string',
alias: 'o',
description: 'Output file path',
default: defaultPath,
});
export const skipConfirmationOption: Options = {
type: 'boolean',
alias: 'y',
description: 'Skip confirmation prompts',
default: false,
};

@ -0,0 +1,141 @@
import { CommandModule, Options } from 'yargs';
import { TokenType } from '@hyperlane-xyz/hyperlane-token';
import { log } from '../../logger.js';
import { sendTestMessage } from '../send/message.js';
import { sendTestTransfer } from '../send/transfer.js';
import {
chainsCommandOption,
coreArtifactsOption,
keyCommandOption,
} from './options.js';
/**
* Parent command
*/
export const sendCommand: CommandModule = {
command: 'send',
describe: 'Send a test message or transfer',
builder: (yargs) =>
yargs
.command(messageCommand)
.command(transferCommand)
.version(false)
.demandCommand(),
handler: () => log('Command required'),
};
/**
* Message command
*/
const messageOptions: { [k: string]: Options } = {
key: keyCommandOption,
chains: chainsCommandOption,
core: coreArtifactsOption,
origin: {
type: 'string',
description: 'Origin chain to send message from',
demandOption: true,
},
destination: {
type: 'string',
description: 'Destination chain to send message to',
demandOption: true,
},
timeout: {
type: 'number',
description: 'Timeout in seconds',
default: 5 * 60,
},
quick: {
type: 'boolean',
description: 'Skip wait for message to be delivered',
default: false,
},
};
const messageCommand: CommandModule = {
command: 'message',
describe: 'Send a test message to a remote chain',
builder: (yargs) => yargs.options(messageOptions),
handler: async (argv: any) => {
const key: string = argv.key || process.env.HYP_KEY;
const chainConfigPath: string = argv.chains;
const coreArtifactsPath: string = argv.core;
const origin: string = argv.origin;
const destination: string = argv.destination;
const timeoutSec: number = argv.timeout;
const skipWaitForDelivery: boolean = argv.quick;
await sendTestMessage({
key,
chainConfigPath,
coreArtifactsPath,
origin,
destination,
timeoutSec,
skipWaitForDelivery,
});
process.exit(0);
},
};
/**
* Transfer command
*/
const transferCommand: CommandModule = {
command: 'transfer',
describe: 'Send a test token transfer on a warp route',
builder: (yargs) =>
yargs.options({
...messageOptions,
router: {
type: 'string',
description: 'The address of the token router contract',
demandOption: true,
},
type: {
type: 'string',
description: 'Warp token type (native of collateral)',
default: TokenType.collateral,
choices: [TokenType.collateral, TokenType.native],
},
wei: {
type: 'string',
description: 'Amount in wei to send',
default: 1,
},
recipient: {
type: 'string',
description: 'Token recipient address (defaults to sender)',
},
}),
handler: async (argv: any) => {
const key: string = argv.key || process.env.HYP_KEY;
const chainConfigPath: string = argv.chains;
const coreArtifactsPath: string = argv.core;
const origin: string = argv.origin;
const destination: string = argv.destination;
const timeoutSec: number = argv.timeout;
const routerAddress: string = argv.router;
const tokenType: TokenType = argv.type;
const wei: string = argv.wei;
const recipient: string | undefined = argv.recipient;
const skipWaitForDelivery: boolean = argv.quick;
await sendTestTransfer({
key,
chainConfigPath,
coreArtifactsPath,
origin,
destination,
routerAddress,
tokenType,
wei,
recipient,
timeoutSec,
skipWaitForDelivery,
});
process.exit(0);
},
};

@ -0,0 +1,38 @@
import { CommandModule } from 'yargs';
import { checkMessageStatus } from '../status/message.js';
import { chainsCommandOption, coreArtifactsOption } from './options.js';
export const statusCommand: CommandModule = {
command: 'status',
describe: 'Check status of a message',
builder: (yargs) =>
yargs.options({
chains: chainsCommandOption,
core: coreArtifactsOption,
id: {
type: 'string',
description: 'Message ID',
demandOption: true,
},
destination: {
type: 'string',
description: 'Destination chain name',
demandOption: true,
},
}),
handler: async (argv: any) => {
const chainConfigPath: string = argv.chains;
const coreArtifactsPath: string = argv.core;
const messageId: string = argv.id;
const destination: string = argv.destination;
await checkMessageStatus({
chainConfigPath,
coreArtifactsPath,
messageId,
destination,
});
process.exit(0);
},
};

@ -0,0 +1,22 @@
import { z } from 'zod';
import { HyperlaneContractsMap } from '@hyperlane-xyz/sdk';
import { readYamlOrJson } from '../utils/files.js';
const DeploymentArtifactsSchema = z
.object({})
.catchall(z.object({}).catchall(z.string()));
export function readDeploymentArtifacts(filePath: string) {
const artifacts = readYamlOrJson<HyperlaneContractsMap<any>>(filePath);
if (!artifacts) throw new Error(`No artifacts found at ${filePath}`);
const result = DeploymentArtifactsSchema.safeParse(artifacts);
if (!result.success) {
const firstIssue = result.error.issues[0];
throw new Error(
`Invalid artifacts: ${firstIssue.path} => ${firstIssue.message}`,
);
}
return artifacts;
}

@ -0,0 +1,108 @@
import { confirm, input, select } from '@inquirer/prompts';
import fs from 'fs';
import {
ChainMap,
ChainMetadata,
ChainMetadataSchema,
} from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { errorRed, log, logBlue, logGreen } from '../../logger.js';
import { getMultiProvider } from '../context.js';
import { FileFormat, mergeYamlOrJson, readYamlOrJson } from '../utils/files.js';
export function readChainConfig(filePath: string) {
log(`Reading file configs in ${filePath}`);
const chainToMetadata = readYamlOrJson<ChainMap<ChainMetadata>>(filePath);
if (
!chainToMetadata ||
typeof chainToMetadata !== 'object' ||
!Object.keys(chainToMetadata).length
) {
errorRed(`No configs found in ${filePath}`);
process.exit(1);
}
for (const [chain, metadata] of Object.entries(chainToMetadata)) {
const parseResult = ChainMetadataSchema.safeParse(metadata);
if (!parseResult.success) {
errorRed(
`Chain config for ${chain} is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/chain-config.yaml for an example`,
);
errorRed(JSON.stringify(parseResult.error.errors));
process.exit(1);
}
if (metadata.name !== chain) {
errorRed(`Chain ${chain} name does not match key`);
process.exit(1);
}
}
// Ensure multiprovider accepts this metadata
getMultiProvider(chainToMetadata);
logGreen(`All chain configs in ${filePath} are valid`);
return chainToMetadata;
}
export function readChainConfigIfExists(filePath: string) {
if (!fs.existsSync(filePath)) {
log('No chain config file provided');
return {};
} else {
return readChainConfig(filePath);
}
}
export async function createChainConfig({
format,
outPath,
}: {
format: FileFormat;
outPath: string;
}) {
logBlue('Creating a new chain config');
const name = await input({
message: 'Enter chain name (one word, lower case)',
});
const chainId = await input({ message: 'Enter chain id (number)' });
const skipDomain = await confirm({
message: 'Will the domainId match the chainId (recommended)?',
});
let domainId: string;
if (skipDomain) {
domainId = chainId;
} else {
domainId = await input({
message: 'Enter domain id (number, often matches chainId)',
});
}
const protocol = await select({
message: 'Select protocol type',
choices: Object.values(ProtocolType).map((protocol) => ({
name: protocol,
value: protocol,
})),
});
const rpcUrl = await input({ message: 'Enter http or https rpc url' });
const metadata: ChainMetadata = {
name,
chainId: parseInt(chainId, 10),
domainId: parseInt(domainId, 10),
protocol,
rpcUrls: [{ http: rpcUrl }],
};
const parseResult = ChainMetadataSchema.safeParse(metadata);
if (parseResult.success) {
logGreen(`Chain config is valid, writing to file ${outPath}`);
mergeYamlOrJson(outPath, { [name]: metadata }, format);
} else {
errorRed(
`Chain config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/chain-config.yaml for an example`,
);
errorRed(JSON.stringify(parseResult.error.errors));
throw new Error('Invalid chain config');
}
}

@ -0,0 +1,120 @@
import { confirm, input, select } from '@inquirer/prompts';
import { z } from 'zod';
import { ChainMap, ModuleType, MultisigIsmConfig } from '@hyperlane-xyz/sdk';
import { objMap } from '@hyperlane-xyz/utils';
import { errorRed, log, logBlue, logGreen } from '../../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
import { FileFormat, mergeYamlOrJson, readYamlOrJson } from '../utils/files.js';
import { readChainConfigIfExists } from './chain.js';
const MultisigConfigMapSchema = z.object({}).catchall(
z.object({
type: z.string(),
threshold: z.number(),
validators: z.array(z.string()),
}),
);
export type MultisigConfigMap = z.infer<typeof MultisigConfigMapSchema>;
export function readMultisigConfig(filePath: string) {
const config = readYamlOrJson(filePath);
if (!config) throw new Error(`No multisig config found at ${filePath}`);
const result = MultisigConfigMapSchema.safeParse(config);
if (!result.success) {
const firstIssue = result.error.issues[0];
throw new Error(
`Invalid multisig config: ${firstIssue.path} => ${firstIssue.message}`,
);
}
const parsedConfig = result.data;
const formattedConfig = objMap(parsedConfig, (_, config) => ({
...config,
type: humanReadableIsmTypeToEnum(config.type),
}));
logGreen(`All multisig configs in ${filePath} are valid`);
return formattedConfig as ChainMap<MultisigIsmConfig>;
}
export function isValidMultisigConfig(config: any) {
return MultisigConfigMapSchema.safeParse(config).success;
}
function humanReadableIsmTypeToEnum(type: string): ModuleType {
for (const [key, value] of Object.entries(ModuleType)) {
if (key.toLowerCase() === type) return parseInt(value.toString(), 10);
}
throw new Error(`Invalid ISM type ${type}`);
}
export async function createMultisigConfig({
format,
outPath,
chainConfigPath,
}: {
format: FileFormat;
outPath: string;
chainConfigPath: string;
}) {
logBlue('Creating a new multisig config');
const customChains = readChainConfigIfExists(chainConfigPath);
const chains = await runMultiChainSelectionStep(customChains);
const result: MultisigConfigMap = {};
let lastConfig: MultisigConfigMap['string'] | undefined = undefined;
let repeat = false;
for (const chain of chains) {
log(`Setting values for chain ${chain}`);
if (lastConfig && repeat) {
result[chain] = lastConfig;
continue;
}
// TODO consider using default and not offering options here
// legacy_multisig is being deprecated in v3
// Default should probably be aggregation(message_id, merkle_root)
const moduleType = await select({
message: 'Select multisig type',
choices: [
// { value: 'routing, name: 'routing' }, // TODO add support
// { value: 'aggregation, name: 'aggregation' }, // TODO add support
{ value: 'legacy_multisig', name: 'legacy multisig' },
{ value: 'merkle_root_multisig', name: 'merkle root multisig' },
{ value: 'message_id_multisig', name: 'message id multisig' },
],
pageSize: 5,
});
const thresholdInput = await input({
message: 'Enter threshold of signers (number)',
});
const threshold = parseInt(thresholdInput, 10);
const validatorsInput = await input({
message: 'Enter validator addresses (comma separated list)',
});
const validators = validatorsInput.split(',').map((v) => v.trim());
lastConfig = {
type: moduleType,
threshold,
validators,
};
result[chain] = lastConfig;
repeat = await confirm({
message: 'Use this same config for remaining chains?',
});
}
if (isValidMultisigConfig(result)) {
logGreen(`Multisig config is valid, writing to file ${outPath}`);
mergeYamlOrJson(outPath, result, format);
} else {
errorRed(
`Multisig config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/multisig-ism.yaml for an example`,
);
throw new Error('Invalid multisig config');
}
}

@ -0,0 +1,126 @@
import { confirm, input } from '@inquirer/prompts';
import { ethers } from 'ethers';
import { z } from 'zod';
import { TokenType } from '@hyperlane-xyz/hyperlane-token';
import { errorRed, logBlue, logGreen } from '../../logger.js';
import {
runMultiChainSelectionStep,
runSingleChainSelectionStep,
} from '../utils/chains.js';
import { FileFormat, readYamlOrJson, writeYamlOrJson } from '../utils/files.js';
import { readChainConfigIfExists } from './chain.js';
const ConnectionConfigSchema = {
mailbox: z.string().optional(),
interchainGasPaymaster: z.string().optional(),
interchainSecurityModule: z.string().optional(),
foreignDeployment: z.string().optional(),
};
export const WarpRouteConfigSchema = z.object({
base: z.object({
type: z.literal(TokenType.native).or(z.literal(TokenType.collateral)),
chainName: z.string(),
address: z.string().optional(),
isNft: z.boolean().optional(),
name: z.string().optional(),
symbol: z.string().optional(),
decimals: z.number().optional(),
...ConnectionConfigSchema,
}),
synthetics: z
.array(
z.object({
chainName: z.string(),
name: z.string().optional(),
symbol: z.string().optional(),
totalSupply: z.number().optional(),
...ConnectionConfigSchema,
}),
)
.nonempty(),
});
type InferredType = z.infer<typeof WarpRouteConfigSchema>;
// A workaround for Zod's terrible typing for nonEmpty arrays
export type WarpRouteConfig = {
base: InferredType['base'];
synthetics: Array<InferredType['synthetics'][0]>;
};
export function readWarpRouteConfig(filePath: string) {
const config = readYamlOrJson(filePath);
if (!config) throw new Error(`No warp config found at ${filePath}`);
const result = WarpRouteConfigSchema.safeParse(config);
if (!result.success) {
const firstIssue = result.error.issues[0];
throw new Error(
`Invalid warp config: ${firstIssue.path} => ${firstIssue.message}`,
);
}
return result.data;
}
export function isValidWarpRouteConfig(config: any) {
return WarpRouteConfigSchema.safeParse(config).success;
}
export async function createWarpConfig({
format,
outPath,
chainConfigPath,
}: {
format: FileFormat;
outPath: string;
chainConfigPath: string;
}) {
logBlue('Creating a new warp route config');
const customChains = readChainConfigIfExists(chainConfigPath);
const baseChain = await runSingleChainSelectionStep(
customChains,
'Select base chain with the original token to warp',
);
const isNative = await confirm({
message:
'Are you creating a route for the native token of the base chain (e.g. Ether on Ethereum)?',
});
const baseType = isNative ? TokenType.native : TokenType.collateral;
const baseAddress = isNative
? ethers.constants.AddressZero
: await input({ message: 'Enter the token address' });
const isNft = isNative
? false
: await confirm({ message: 'Is this an NFT (i.e. ERC-721)?' });
const syntheticChains = await runMultiChainSelectionStep(
customChains,
'Select the chains to which the base token will be connected',
);
// TODO add more prompts here to support customizing the token metadata
const result: WarpRouteConfig = {
base: {
chainName: baseChain,
type: baseType,
address: baseAddress,
isNft,
},
synthetics: syntheticChains.map((chain) => ({ chainName: chain })),
};
if (isValidWarpRouteConfig(result)) {
logGreen(`Warp Route config is valid, writing to file ${outPath}`);
writeYamlOrJson(outPath, result, format);
} else {
errorRed(
`Warp config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/warp-tokens.yaml for an example`,
);
throw new Error('Invalid multisig config');
}
}

@ -0,0 +1,4 @@
// TODO revisit these rough balance requirements with more precise measurements
export const MINIMUM_CORE_DEPLOY_BALANCE = '500000000000000000'; // 0.5 ETH
export const MINIMUM_WARP_DEPLOY_BALANCE = '200000000000000000'; // 0.2 Eth
export const MINIMUM_TEST_SEND_BALANCE = '10000000000000000'; // 0.01 ETH

@ -0,0 +1,51 @@
import { ethers } from 'ethers';
import {
ChainMap,
ChainMetadata,
HyperlaneContractsMap,
MultiProvider,
chainMetadata,
hyperlaneEnvironments,
} from '@hyperlane-xyz/sdk';
import { objMerge } from '@hyperlane-xyz/utils';
import { readChainConfigIfExists } from './config/chain.js';
import { keyToSigner } from './utils/keys.js';
export const sdkContractAddressesMap = {
...hyperlaneEnvironments.testnet,
...hyperlaneEnvironments.mainnet,
};
export function getMergedContractAddresses(
artifacts?: HyperlaneContractsMap<any>,
) {
return objMerge(
sdkContractAddressesMap,
artifacts || {},
) as HyperlaneContractsMap<any>;
}
export function getContext(chainConfigPath: string) {
const customChains = readChainConfigIfExists(chainConfigPath);
const multiProvider = getMultiProvider(customChains);
return { customChains, multiProvider };
}
export function getContextWithSigner(key: string, chainConfigPath: string) {
const signer = keyToSigner(key);
const customChains = readChainConfigIfExists(chainConfigPath);
const multiProvider = getMultiProvider(customChains, signer);
return { signer, customChains, multiProvider };
}
export function getMultiProvider(
customChains: ChainMap<ChainMetadata>,
signer?: ethers.Signer,
) {
const chainConfigs = { ...chainMetadata, ...customChains };
const mp = new MultiProvider(chainConfigs);
if (signer) mp.setSharedSigner(signer);
return mp;
}

@ -0,0 +1,64 @@
import debug from 'debug';
import { TestRecipient, TestRecipient__factory } from '@hyperlane-xyz/core';
import {
ChainName,
HyperlaneDeployer,
MultiProvider,
} from '@hyperlane-xyz/sdk';
import { Address, eqAddress } from '@hyperlane-xyz/utils';
export type TestRecipientConfig = {
interchainSecurityModule: Address;
};
export type TestRecipientContracts = {
testRecipient: TestRecipient;
};
export type TestRecipientAddresses = {
testRecipient: Address;
};
export const testRecipientFactories = {
testRecipient: new TestRecipient__factory(),
};
// TODO move this and related configs to the SDK
export class TestRecipientDeployer extends HyperlaneDeployer<
TestRecipientConfig,
typeof testRecipientFactories
> {
constructor(multiProvider: MultiProvider) {
super(multiProvider, testRecipientFactories, {
logger: debug('hyperlane:TestRecipientDeployer'),
});
}
async deployContracts(
chain: ChainName,
config: TestRecipientConfig,
): Promise<TestRecipientContracts> {
const testRecipient = await this.deployContract(chain, 'testRecipient', []);
try {
this.logger(`Checking ISM ${chain}`);
const ism = await testRecipient.interchainSecurityModule();
this.logger(`Found ISM for on ${chain}: ${ism}`);
if (!eqAddress(ism, config.interchainSecurityModule)) {
this.logger(
`Current ISM does not match config. Updating to ${config.interchainSecurityModule}`,
);
const tx = testRecipient.setInterchainSecurityModule(
config.interchainSecurityModule,
);
await this.multiProvider.handleTx(chain, tx);
}
} catch (error) {
this.logger(`Failed to check/update ISM for ${chain}: ${error}`);
this.logger('Leaving ISM as is and continuing.');
}
return {
testRecipient,
};
}
}

@ -0,0 +1,444 @@
import { confirm } from '@inquirer/prompts';
import { ethers } from 'ethers';
import {
ChainMap,
ChainName,
CoreConfig,
DeployedIsm,
GasOracleContractType,
HyperlaneAddresses,
HyperlaneAddressesMap,
HyperlaneContractsMap,
HyperlaneCoreDeployer,
HyperlaneDeploymentArtifacts,
HyperlaneIgpDeployer,
HyperlaneIsmFactory,
HyperlaneIsmFactoryDeployer,
ModuleType,
MultiProvider,
MultisigIsmConfig,
OverheadIgpConfig,
RoutingIsmConfig,
agentStartBlocks,
buildAgentConfig,
defaultMultisigIsmConfigs,
multisigIsmVerificationCost,
serializeContractsMap,
} from '@hyperlane-xyz/sdk';
import { Address, objFilter, objMerge } from '@hyperlane-xyz/utils';
import { log, logBlue, logGray, logGreen, logRed } from '../../logger.js';
import { readDeploymentArtifacts } from '../config/artifacts.js';
import { readMultisigConfig } from '../config/multisig.js';
import { MINIMUM_CORE_DEPLOY_BALANCE } from '../consts.js';
import {
getContextWithSigner,
getMergedContractAddresses,
sdkContractAddressesMap,
} from '../context.js';
import { runOriginAndRemotesSelectionStep } from '../utils/chains.js';
import {
prepNewArtifactsFiles,
runFileSelectionStep,
writeJson,
} from '../utils/files.js';
import {
TestRecipientConfig,
TestRecipientDeployer,
} from './TestRecipientDeployer.js';
import { runPreflightChecks } from './utils.js';
export async function runCoreDeploy({
key,
chainConfigPath,
ismConfigPath,
artifactsPath,
outPath,
origin,
remotes,
skipConfirmation,
}: {
key: string;
chainConfigPath: string;
ismConfigPath: string;
artifactsPath?: string;
outPath: string;
origin?: string;
remotes?: string[];
skipConfirmation: boolean;
}) {
const { customChains, multiProvider, signer } = getContextWithSigner(
key,
chainConfigPath,
);
if (!origin || !remotes?.length) {
({ origin, remotes } = await runOriginAndRemotesSelectionStep(
customChains,
));
}
const selectedChains = [origin, ...remotes];
const artifacts = await runArtifactStep(selectedChains, artifactsPath);
const multisigConfig = await runIsmStep(selectedChains, ismConfigPath);
const deploymentParams: DeployParams = {
origin,
remotes,
signer,
multiProvider,
artifacts,
multisigConfig,
outPath,
skipConfirmation,
};
await runDeployPlanStep(deploymentParams);
await runPreflightChecks({
...deploymentParams,
minBalanceWei: MINIMUM_CORE_DEPLOY_BALANCE,
});
await executeDeploy(deploymentParams);
}
async function runArtifactStep(
selectedChains: ChainName[],
artifactsPath?: string,
) {
if (!artifactsPath) {
logBlue(
'\n',
'Deployments can be totally new or can use some existing contract addresses.',
);
const isResume = await confirm({
message: 'Do you want use some existing contract addresses?',
});
if (!isResume) return undefined;
artifactsPath = await runFileSelectionStep(
'./artifacts',
'contract artifacts',
'core-deployment',
);
}
const artifacts = readDeploymentArtifacts(artifactsPath);
const artifactChains = Object.keys(artifacts).filter((c) =>
selectedChains.includes(c),
);
log(`Found existing artifacts for chains: ${artifactChains.join(', ')}`);
return artifacts;
}
async function runIsmStep(selectedChains: ChainName[], ismConfigPath?: string) {
const defaultConfigChains = Object.keys(defaultMultisigIsmConfigs);
const configRequired = !!selectedChains.find(
(c) => !defaultConfigChains.includes(c),
);
if (!configRequired) return;
if (!ismConfigPath) {
logBlue(
'\n',
'Hyperlane instances requires an Interchain Security Module (ISM).',
);
logGray(
'Note, only Multisig ISM configs are currently supported in the CLI',
'Example config: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/cli/typescript/cli/examples/multisig-ism.yaml',
);
ismConfigPath = await runFileSelectionStep(
'./configs',
'ISM config',
'ism',
);
}
const configs = readMultisigConfig(ismConfigPath);
const multisigConfigChains = Object.keys(configs).filter((c) =>
selectedChains.includes(c),
);
log(`Found configs for chains: ${multisigConfigChains.join(', ')}`);
return configs;
}
interface DeployParams {
origin: string;
remotes: string[];
signer: ethers.Signer;
multiProvider: MultiProvider;
artifacts?: HyperlaneContractsMap<any>;
multisigConfig?: ChainMap<MultisigIsmConfig>;
outPath: string;
skipConfirmation: boolean;
}
async function runDeployPlanStep({
origin,
remotes,
signer,
artifacts,
skipConfirmation,
}: DeployParams) {
const address = await signer.getAddress();
logBlue('\nDeployment plan');
logGray('===============');
log(`Transaction signer and owner of new contracts will be ${address}`);
log(`Deploying to ${origin} and connecting it to ${remotes.join(', ')}`);
const numContracts = Object.keys(
Object.values(sdkContractAddressesMap)[0],
).length;
log(`There are ${numContracts} contracts for each chain`);
if (artifacts)
log('But contracts with an address in the artifacts file will be skipped');
for (const chain of [origin, ...remotes]) {
const chainArtifacts = artifacts?.[chain] || {};
const numRequired = numContracts - Object.keys(chainArtifacts).length;
log(`${chain} will require ${numRequired} of ${numContracts}`);
}
log('The default interchain security module will be a Multisig.');
if (skipConfirmation) return;
const isConfirmed = await confirm({
message: 'Is this deployment plan correct?',
});
if (!isConfirmed) throw new Error('Deployment cancelled');
}
async function executeDeploy({
origin,
remotes,
signer,
multiProvider,
outPath,
artifacts = {},
multisigConfig = {},
}: DeployParams) {
logBlue('All systems ready, captain! Beginning deployment...');
const [contractsFilePath, agentFilePath] = prepNewArtifactsFiles(outPath, [
{ filename: 'core-deployment', description: 'Contract addresses' },
{ filename: 'agent-config', description: 'Agent configs' },
]);
const owner = await signer.getAddress();
const selectedChains = [origin, ...remotes];
const mergedContractAddrs = getMergedContractAddresses(artifacts);
// 1. Deploy ISM factories to all deployable chains that don't have them.
log('Deploying ISM factory contracts');
const ismFactoryDeployer = new HyperlaneIsmFactoryDeployer(multiProvider);
ismFactoryDeployer.cacheAddressesMap(mergedContractAddrs);
const ismFactoryContracts = await ismFactoryDeployer.deploy(selectedChains);
artifacts = writeMergedAddresses(
contractsFilePath,
artifacts,
ismFactoryContracts,
);
logGreen('ISM factory contracts deployed');
// 2. Deploy IGPs to all deployable chains.
log('Deploying IGP contracts');
const igpConfig = buildIgpConfigMap(owner, selectedChains, multisigConfig);
const igpDeployer = new HyperlaneIgpDeployer(multiProvider);
igpDeployer.cacheAddressesMap(artifacts);
const igpContracts = await igpDeployer.deploy(igpConfig);
artifacts = writeMergedAddresses(contractsFilePath, artifacts, igpContracts);
logGreen('IGP contracts deployed');
// Build an IsmFactory that covers all chains so that we can
// use it to deploy ISMs to remote chains.
const ismFactory = HyperlaneIsmFactory.fromAddressesMap(
mergedContractAddrs,
multiProvider,
);
// 3. Deploy ISM contracts to remote deployable chains
log('Deploying ISMs');
const ismContracts: ChainMap<{ multisigIsm: DeployedIsm }> = {};
const defaultIsms: ChainMap<Address> = {};
for (const ismOrigin of selectedChains) {
if (artifacts[ismOrigin].multisigIsm) {
log(`ISM contract recovered, skipping ISM deployment to ${ismOrigin}`);
defaultIsms[ismOrigin] = artifacts[ismOrigin].multisigIsm;
continue;
}
log(`Deploying ISM to ${ismOrigin}`);
const ismConfig = buildIsmConfig(
owner,
selectedChains.filter((r) => r !== ismOrigin),
multisigConfig,
);
ismContracts[ismOrigin] = {
multisigIsm: await ismFactory.deploy(ismOrigin, ismConfig),
};
defaultIsms[ismOrigin] = ismContracts[ismOrigin].multisigIsm.address;
}
artifacts = writeMergedAddresses(contractsFilePath, artifacts, ismContracts);
logGreen('ISM contracts deployed');
// 4. Deploy core contracts to origin chain
log(`Deploying core contracts to ${origin}`);
const coreDeployer = new HyperlaneCoreDeployer(multiProvider, ismFactory);
coreDeployer.cacheAddressesMap(artifacts);
const coreConfig = buildCoreConfigMap(owner, origin, defaultIsms);
const coreContracts = await coreDeployer.deploy(coreConfig);
artifacts = writeMergedAddresses(contractsFilePath, artifacts, coreContracts);
logGreen('Core contracts deployed');
// 5. Deploy TestRecipients to all deployable chains
log('Deploying test recipient contracts');
const testRecipientConfig = buildTestRecipientConfigMap(
selectedChains,
artifacts,
);
const testRecipientDeployer = new TestRecipientDeployer(multiProvider);
testRecipientDeployer.cacheAddressesMap(artifacts);
const testRecipients = await testRecipientDeployer.deploy(
testRecipientConfig,
);
artifacts = writeMergedAddresses(
contractsFilePath,
artifacts,
testRecipients,
);
logGreen('Test recipient contracts deployed');
log('Writing agent configs');
await writeAgentConfig(
agentFilePath,
artifacts,
origin,
remotes,
multiProvider,
);
logGreen('Agent configs written');
logBlue('Deployment is complete!');
logBlue(`Contract address artifacts are in ${contractsFilePath}`);
logBlue(`Agent configs are in ${agentFilePath}`);
}
function buildIsmConfig(
owner: Address,
remotes: ChainName[],
multisigIsmConfigs: ChainMap<MultisigIsmConfig>,
): RoutingIsmConfig {
const mergedMultisigIsmConfig: ChainMap<MultisigIsmConfig> = objMerge(
defaultMultisigIsmConfigs,
multisigIsmConfigs,
);
return {
owner,
type: ModuleType.ROUTING,
domains: Object.fromEntries(
remotes.map((remote) => [remote, mergedMultisigIsmConfig[remote]]),
),
};
}
function buildCoreConfigMap(
owner: Address,
origin: ChainName,
defaultIsms: ChainMap<Address>,
): ChainMap<CoreConfig> {
const configMap: ChainMap<CoreConfig> = {};
configMap[origin] = {
owner,
defaultIsm: defaultIsms[origin],
};
return configMap;
}
function buildTestRecipientConfigMap(
chains: ChainName[],
addressesMap: HyperlaneAddressesMap<any>,
): ChainMap<TestRecipientConfig> {
return chains.reduce<ChainMap<TestRecipientConfig>>((config, chain) => {
const interchainSecurityModule =
// TODO revisit assumption that multisigIsm is always the ISM
addressesMap[chain].multisigIsm ??
addressesMap[chain].interchainSecurityModule ??
ethers.constants.AddressZero;
if (interchainSecurityModule === ethers.constants.AddressZero) {
logRed('Error: No ISM for TestRecipient, deploying with zero address');
}
config[chain] = { interchainSecurityModule };
return config;
}, {});
}
function buildIgpConfigMap(
owner: Address,
selectedChains: ChainName[],
multisigIsmConfigs: ChainMap<MultisigIsmConfig>,
): ChainMap<OverheadIgpConfig> {
const mergedMultisigIsmConfig: ChainMap<MultisigIsmConfig> = objMerge(
defaultMultisigIsmConfigs,
multisigIsmConfigs,
);
const configMap: ChainMap<OverheadIgpConfig> = {};
for (const origin of selectedChains) {
const overhead: ChainMap<number> = {};
const gasOracleType: ChainMap<GasOracleContractType> = {};
for (const remote of selectedChains) {
if (origin === remote) continue;
overhead[remote] = multisigIsmVerificationCost(
mergedMultisigIsmConfig[remote].threshold,
mergedMultisigIsmConfig[remote].validators.length,
);
gasOracleType[remote] = GasOracleContractType.StorageGasOracle;
}
configMap[origin] = {
owner,
beneficiary: owner,
gasOracleType,
overhead,
oracleKey: owner,
};
}
return configMap;
}
function writeMergedAddresses(
filePath: string,
aAddresses: HyperlaneAddressesMap<any>,
bContracts: HyperlaneContractsMap<any>,
): HyperlaneAddressesMap<any> {
const bAddresses = serializeContractsMap(bContracts);
const mergedAddresses = objMerge(aAddresses, bAddresses);
writeJson(filePath, mergedAddresses);
return mergedAddresses;
}
async function writeAgentConfig(
filePath: string,
artifacts: HyperlaneAddressesMap<any>,
origin: ChainName,
remotes: ChainName[],
multiProvider: MultiProvider,
) {
const selectedChains = [origin, ...remotes];
const startBlocks: ChainMap<number> = { ...agentStartBlocks };
startBlocks[origin] = await multiProvider
.getProvider(origin)
.getBlockNumber();
const mergedAddressesMap: HyperlaneAddressesMap<any> = objMerge(
sdkContractAddressesMap,
artifacts,
);
const filteredAddressesMap = objFilter(
mergedAddressesMap,
(chain, v): v is HyperlaneAddresses<any> =>
selectedChains.includes(chain) &&
!!v.mailbox &&
!!v.interchainGasPaymaster &&
!!v.validatorAnnounce,
) as ChainMap<HyperlaneDeploymentArtifacts>;
const agentConfig = buildAgentConfig(
Object.keys(filteredAddressesMap),
multiProvider,
filteredAddressesMap,
startBlocks,
);
writeJson(filePath, agentConfig);
}

@ -0,0 +1,27 @@
import type { ERC20Metadata, TokenType } from '@hyperlane-xyz/hyperlane-token';
import type { Address } from '@hyperlane-xyz/utils';
export type MinimalTokenMetadata = Omit<ERC20Metadata, 'totalSupply'>;
// Types below must match the Warp UI token config schema
// It is used to generate the configs for the Warp UI
// https://github.com/hyperlane-xyz/hyperlane-warp-ui-template/blob/main/src/features/tokens/types.ts
interface BaseWarpUITokenConfig extends MinimalTokenMetadata {
type: TokenType.collateral | TokenType.native;
chainId: number;
logoURI?: string;
isNft?: boolean;
}
interface CollateralTokenConfig extends BaseWarpUITokenConfig {
type: TokenType.collateral;
address: Address;
hypCollateralAddress: Address;
}
interface NativeTokenConfig extends BaseWarpUITokenConfig {
type: TokenType.native;
hypNativeAddress: Address;
}
export type WarpUITokenConfig = CollateralTokenConfig | NativeTokenConfig;

@ -0,0 +1,46 @@
import { ethers } from 'ethers';
import { ChainName, MultiProvider } from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { log, logGreen } from '../../logger.js';
import { assertNativeBalances } from '../utils/balances.js';
import { assertSigner } from '../utils/keys.js';
export async function runPreflightChecks({
origin,
remotes,
signer,
multiProvider,
minBalanceWei,
}: {
origin: ChainName;
remotes: ChainName[];
signer: ethers.Signer;
multiProvider: MultiProvider;
minBalanceWei: string;
}) {
log('Running pre-flight checks...');
if (!origin || !remotes?.length) throw new Error('Invalid chain selection');
if (remotes.includes(origin))
throw new Error('Origin and remotes must be distinct');
for (const chain of [origin, ...remotes]) {
const metadata = multiProvider.tryGetChainMetadata(chain);
if (!metadata) throw new Error(`No chain config found for ${chain}`);
if (metadata.protocol !== ProtocolType.Ethereum)
throw new Error('Only Ethereum chains are supported for now');
}
logGreen('Chains are valid ✅');
assertSigner(signer);
logGreen('Signer is valid ✅');
await assertNativeBalances(
multiProvider,
signer,
[origin, ...remotes],
minBalanceWei,
);
logGreen('Balances are sufficient ✅');
}

@ -0,0 +1,361 @@
import { confirm, input } from '@inquirer/prompts';
import { ethers } from 'ethers';
import {
ERC20__factory,
ERC721__factory,
HypERC20Deployer,
HypERC721Deployer,
TokenConfig,
TokenFactories,
TokenType,
} from '@hyperlane-xyz/hyperlane-token';
import {
ChainMap,
ChainName,
ConnectionClientConfig,
HyperlaneContractsMap,
MultiProvider,
RouterConfig,
chainMetadata as defaultChainMetadata,
} from '@hyperlane-xyz/sdk';
import { Address, objMap } from '@hyperlane-xyz/utils';
import { log, logBlue, logGray, logGreen } from '../../logger.js';
import { readDeploymentArtifacts } from '../config/artifacts.js';
import { WarpRouteConfig, readWarpRouteConfig } from '../config/warp.js';
import { MINIMUM_WARP_DEPLOY_BALANCE } from '../consts.js';
import {
getContextWithSigner,
getMergedContractAddresses,
} from '../context.js';
import {
prepNewArtifactsFiles,
runFileSelectionStep,
writeJson,
} from '../utils/files.js';
import { MinimalTokenMetadata, WarpUITokenConfig } from './types.js';
import { runPreflightChecks } from './utils.js';
export async function runWarpDeploy({
key,
chainConfigPath,
warpConfigPath,
coreArtifactsPath,
outPath,
skipConfirmation,
}: {
key: string;
chainConfigPath: string;
warpConfigPath?: string;
coreArtifactsPath?: string;
outPath: string;
skipConfirmation: boolean;
}) {
const { multiProvider, signer } = getContextWithSigner(key, chainConfigPath);
if (!warpConfigPath) {
warpConfigPath = await runFileSelectionStep(
'./configs',
'Warp config',
'warp',
);
}
const warpRouteConfig = readWarpRouteConfig(warpConfigPath);
const artifacts = coreArtifactsPath
? readDeploymentArtifacts(coreArtifactsPath)
: undefined;
const configs = await runBuildConfigStep({
warpRouteConfig,
artifacts,
multiProvider,
signer,
});
const deploymentParams = {
...configs,
signer,
multiProvider,
outPath,
skipConfirmation,
};
await runDeployPlanStep(deploymentParams);
await runPreflightChecks({
...deploymentParams,
minBalanceWei: MINIMUM_WARP_DEPLOY_BALANCE,
});
await executeDeploy(deploymentParams);
}
async function runBuildConfigStep({
warpRouteConfig,
multiProvider,
signer,
artifacts,
}: {
warpRouteConfig: WarpRouteConfig;
multiProvider: MultiProvider;
signer: ethers.Signer;
artifacts?: HyperlaneContractsMap<any>;
}) {
log('Assembling token configs');
const { base, synthetics } = warpRouteConfig;
const { type: baseType, chainName: baseChainName, isNft } = base;
const owner = await signer.getAddress();
const baseMetadata = await fetchBaseTokenMetadata(base, multiProvider);
log(
`Using base token metadata: Name: ${baseMetadata.name}, Symbol: ${baseMetadata.symbol}, Decimals: ${baseMetadata.decimals}`,
);
const mergedContractAddrs = getMergedContractAddresses(artifacts);
// Create configs that coalesce together values from the config file,
// the artifacts, and the SDK as a fallback
const configMap: ChainMap<TokenConfig & RouterConfig> = {
[baseChainName]: {
type: baseType,
token:
baseType === TokenType.collateral
? base.address!
: ethers.constants.AddressZero,
owner,
mailbox: base.mailbox || mergedContractAddrs[baseChainName].mailbox,
interchainSecurityModule:
base.interchainSecurityModule ||
mergedContractAddrs[baseChainName].interchainSecurityModule ||
mergedContractAddrs[baseChainName].multisigIsm,
interchainGasPaymaster:
base.interchainGasPaymaster ||
mergedContractAddrs[baseChainName].defaultIsmInterchainGasPaymaster,
foreignDeployment: base.foreignDeployment,
name: baseMetadata.name,
symbol: baseMetadata.symbol,
decimals: baseMetadata.decimals,
},
};
for (const synthetic of synthetics) {
const sChainName = synthetic.chainName;
configMap[sChainName] = {
type: TokenType.synthetic,
name: synthetic.name || baseMetadata.name,
symbol: synthetic.symbol || baseMetadata.symbol,
totalSupply: synthetic.totalSupply || 0,
owner,
mailbox: synthetic.mailbox || mergedContractAddrs[sChainName].mailbox,
interchainSecurityModule:
synthetic.interchainSecurityModule ||
mergedContractAddrs[sChainName].interchainSecurityModule ||
mergedContractAddrs[sChainName].multisigIsm,
interchainGasPaymaster:
synthetic.interchainGasPaymaster ||
mergedContractAddrs[sChainName].defaultIsmInterchainGasPaymaster,
foreignDeployment: synthetic.foreignDeployment,
};
}
// Request input for any address fields that are missing
const requiredRouterFields: Array<keyof ConnectionClientConfig> = [
'mailbox',
'interchainSecurityModule',
'interchainGasPaymaster',
];
let hasShownInfo = false;
for (const [chain, token] of Object.entries(configMap)) {
for (const field of requiredRouterFields) {
if (token[field]) continue;
if (!hasShownInfo) {
logBlue(
'Some router fields are missing. Please enter them now, add them to your warp config, or use the --core flag to use deployment artifacts.',
);
hasShownInfo = true;
}
const value = await input({
message: `Enter ${field} for ${getTokenName(token)} token on ${chain}`,
});
if (!value) throw new Error(`Field ${field} required`);
token[field] = value.trim();
}
}
log('Token configs ready');
return {
configMap,
metadata: baseMetadata,
origin: baseChainName,
remotes: synthetics.map(({ chainName }) => chainName),
isNft: !!isNft,
};
}
interface DeployParams {
configMap: ChainMap<TokenConfig & RouterConfig>;
isNft: boolean;
metadata: MinimalTokenMetadata;
origin: ChainName;
remotes: ChainName[];
signer: ethers.Signer;
multiProvider: MultiProvider;
outPath: string;
skipConfirmation: boolean;
}
async function runDeployPlanStep({
configMap,
isNft,
origin,
remotes,
signer,
skipConfirmation,
}: DeployParams) {
const address = await signer.getAddress();
const baseToken = configMap[origin];
const baseName = getTokenName(baseToken);
logBlue('\nDeployment plan');
logGray('===============');
log(`Transaction signer and owner of new contracts will be ${address}`);
log(`Deploying a warp route with a base of ${baseName} token on ${origin}`);
log(`Connecting it to new synthetic tokens on ${remotes.join(', ')}`);
log(`Using token standard ${isNft ? 'ERC721' : 'ERC20'}`);
if (skipConfirmation) return;
const isConfirmed = await confirm({
message: 'Is this deployment plan correct?',
});
if (!isConfirmed) throw new Error('Deployment cancelled');
}
async function executeDeploy(params: DeployParams) {
logBlue('All systems ready, captain! Beginning deployment...');
const { configMap, isNft, multiProvider, outPath } = params;
const [contractsFilePath, tokenConfigPath] = prepNewArtifactsFiles(outPath, [
{ filename: 'warp-deployment', description: 'Contract addresses' },
{ filename: 'warp-ui-token-config', description: 'Warp UI token config' },
]);
const deployer = isNft
? new HypERC721Deployer(multiProvider)
: new HypERC20Deployer(multiProvider);
const deployedContracts = await deployer.deploy(configMap);
logGreen('Hyp token deployments complete');
log('Writing deployment artifacts');
writeTokenDeploymentArtifacts(contractsFilePath, deployedContracts, params);
writeWarpUiTokenConfig(tokenConfigPath, deployedContracts, params);
logBlue('Deployment is complete!');
logBlue(`Contract address artifacts are in ${contractsFilePath}`);
logBlue(`Warp UI token config is in ${tokenConfigPath}`);
}
// TODO move into token classes in the SDK
async function fetchBaseTokenMetadata(
base: WarpRouteConfig['base'],
multiProvider: MultiProvider,
): Promise<MinimalTokenMetadata> {
const { type, name, symbol, chainName, address, decimals, isNft } = base;
// Skip fetching metadata if it's already provided in the config
if (name && symbol && decimals) {
return { name, symbol, decimals };
}
if (type === TokenType.native) {
return (
multiProvider.getChainMetadata(base.chainName).nativeToken ||
defaultChainMetadata.ethereum.nativeToken!
);
} else if (base.type === TokenType.collateral && address) {
log(`Fetching token metadata for ${address} on ${chainName}}`);
const provider = multiProvider.getProvider(chainName);
if (isNft) {
const erc721Contract = ERC721__factory.connect(address, provider);
const [name, symbol] = await Promise.all([
erc721Contract.name(),
erc721Contract.symbol(),
]);
return { name, symbol, decimals: 0 };
} else {
const erc20Contract = ERC20__factory.connect(address, provider);
const [name, symbol, decimals] = await Promise.all([
erc20Contract.name(),
erc20Contract.symbol(),
erc20Contract.decimals(),
]);
return { name, symbol, decimals };
}
} else {
throw new Error(`Unsupported token: ${base}`);
}
}
function getTokenName(token: TokenConfig) {
return token.type === TokenType.native ? 'native' : token.name;
}
function writeTokenDeploymentArtifacts(
filePath: string,
contracts: HyperlaneContractsMap<TokenFactories>,
{ configMap }: DeployParams,
) {
const artifacts: ChainMap<{
router: Address;
tokenType: TokenType;
}> = objMap(contracts, (chain, contract) => {
return {
router: contract.router.address,
tokenType: configMap[chain].type,
};
});
writeJson(filePath, artifacts);
}
function writeWarpUiTokenConfig(
filePath: string,
contracts: HyperlaneContractsMap<TokenFactories>,
{ configMap, isNft, metadata, origin, multiProvider }: DeployParams,
) {
const baseConfig = configMap[origin];
const hypTokenAddr =
contracts[origin]?.router?.address || configMap[origin]?.foreignDeployment;
if (!hypTokenAddr) {
throw Error(
'No base Hyperlane token address deployed and no foreign deployment specified',
);
}
const commonFields = {
chainId: multiProvider.getChainId(origin),
name: metadata.name,
symbol: metadata.symbol,
decimals: metadata.decimals,
};
let tokenConfig: WarpUITokenConfig;
if (baseConfig.type === TokenType.collateral) {
tokenConfig = {
...commonFields,
type: TokenType.collateral,
address: baseConfig.token,
hypCollateralAddress: hypTokenAddr,
isNft,
};
} else if (baseConfig.type === TokenType.native) {
tokenConfig = {
...commonFields,
type: TokenType.native,
hypNativeAddress: hypTokenAddr,
};
} else {
throw new Error(`Unsupported token type: ${baseConfig.type}`);
}
writeJson(filePath, tokenConfig);
}

@ -0,0 +1,142 @@
import { BigNumber, ethers } from 'ethers';
import {
ChainName,
HyperlaneContractsMap,
HyperlaneCore,
HyperlaneIgp,
MultiProvider,
} from '@hyperlane-xyz/sdk';
import { addressToBytes32, timeout } from '@hyperlane-xyz/utils';
import { errorRed, log, logBlue, logGreen } from '../../logger.js';
import { readDeploymentArtifacts } from '../config/artifacts.js';
import { MINIMUM_TEST_SEND_BALANCE } from '../consts.js';
import {
getContextWithSigner,
getMergedContractAddresses,
} from '../context.js';
import { runPreflightChecks } from '../deploy/utils.js';
const GAS_AMOUNT = 300_000;
// TODO improve the UX here by making params optional and
// prompting for missing values
export async function sendTestMessage({
key,
chainConfigPath,
coreArtifactsPath,
origin,
destination,
timeoutSec,
skipWaitForDelivery,
}: {
key: string;
chainConfigPath: string;
coreArtifactsPath: string;
origin: ChainName;
destination: ChainName;
timeoutSec: number;
skipWaitForDelivery: boolean;
}) {
const { signer, multiProvider } = getContextWithSigner(key, chainConfigPath);
const coreArtifacts = coreArtifactsPath
? readDeploymentArtifacts(coreArtifactsPath)
: undefined;
await runPreflightChecks({
origin,
remotes: [destination],
multiProvider,
signer,
minBalanceWei: MINIMUM_TEST_SEND_BALANCE,
});
await timeout(
executeDelivery({
origin,
destination,
multiProvider,
signer,
coreArtifacts,
skipWaitForDelivery,
}),
timeoutSec * 1000,
'Timed out waiting for messages to be delivered',
);
}
async function executeDelivery({
origin,
destination,
multiProvider,
signer,
coreArtifacts,
skipWaitForDelivery,
}: {
origin: ChainName;
destination: ChainName;
multiProvider: MultiProvider;
signer: ethers.Signer;
coreArtifacts?: HyperlaneContractsMap<any>;
skipWaitForDelivery: boolean;
}) {
const mergedContractAddrs = getMergedContractAddresses(coreArtifacts);
const core = HyperlaneCore.fromAddressesMap(
mergedContractAddrs,
multiProvider,
);
const mailbox = core.getContracts(origin).mailbox;
const igp = HyperlaneIgp.fromAddressesMap(mergedContractAddrs, multiProvider);
const igpContract = igp.getContracts(origin).defaultIsmInterchainGasPaymaster;
const destinationDomain = multiProvider.getDomainId(destination);
const signerAddress = await signer.getAddress();
let txReceipt: ethers.ContractReceipt;
try {
const recipient = mergedContractAddrs[destination].testRecipient;
if (!recipient) {
throw new Error(`Unable to find TestRecipient for ${destination}`);
}
log('Dispatching message');
const messageTx = await mailbox.dispatch(
destinationDomain,
addressToBytes32(recipient),
'0x48656c6c6f21', // Hello!
);
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}`);
// TODO requires update for v3
const value = await igp.quoteGasPaymentForDefaultIsmIgp(
origin,
destination,
BigNumber.from(GAS_AMOUNT),
);
log(`Paying for gas with ${value} wei`);
const paymentTx = await igpContract.payForGas(
message.id,
destinationDomain,
GAS_AMOUNT,
signerAddress,
{ value },
);
await paymentTx.wait();
} catch (e) {
errorRed(
`Encountered error sending message from ${origin} to ${destination}`,
);
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!');
}

@ -0,0 +1,196 @@
import { BigNumber, ethers } from 'ethers';
import {
ERC20__factory,
EvmHypCollateralAdapter,
HypERC20Collateral__factory,
TokenType,
} from '@hyperlane-xyz/hyperlane-token';
import {
ChainName,
HyperlaneContractsMap,
HyperlaneCore,
MultiProtocolProvider,
MultiProvider,
} from '@hyperlane-xyz/sdk';
import { Address, timeout } from '@hyperlane-xyz/utils';
import { log, logBlue, logGreen } from '../../logger.js';
import { readDeploymentArtifacts } from '../config/artifacts.js';
import { MINIMUM_TEST_SEND_BALANCE } from '../consts.js';
import {
getContextWithSigner,
getMergedContractAddresses,
} from '../context.js';
import { runPreflightChecks } from '../deploy/utils.js';
import { assertNativeBalances, assertTokenBalance } from '../utils/balances.js';
// TODO improve the UX here by making params optional and
// prompting for missing values
export async function sendTestTransfer({
key,
chainConfigPath,
coreArtifactsPath,
origin,
destination,
routerAddress,
tokenType,
wei,
recipient,
timeoutSec,
skipWaitForDelivery,
}: {
key: string;
chainConfigPath: string;
coreArtifactsPath: string;
origin: ChainName;
destination: ChainName;
routerAddress: Address;
tokenType: TokenType;
wei: string;
recipient?: string;
timeoutSec: number;
skipWaitForDelivery: boolean;
}) {
const { signer, multiProvider } = getContextWithSigner(key, chainConfigPath);
const artifacts = coreArtifactsPath
? readDeploymentArtifacts(coreArtifactsPath)
: undefined;
if (tokenType === TokenType.collateral) {
await assertTokenBalance(
multiProvider,
signer,
origin,
routerAddress,
wei.toString(),
);
} else if (tokenType === TokenType.native) {
await assertNativeBalances(multiProvider, signer, [origin], wei.toString());
} else {
throw new Error(
'Only collateral and native token types are currently supported in the CLI. For synthetic transfers, try the Warp UI.',
);
}
await runPreflightChecks({
origin,
remotes: [destination],
multiProvider,
signer,
minBalanceWei: MINIMUM_TEST_SEND_BALANCE,
});
await timeout(
executeDelivery({
origin,
destination,
routerAddress,
tokenType,
wei,
recipient,
signer,
multiProvider,
artifacts,
skipWaitForDelivery,
}),
timeoutSec * 1000,
'Timed out waiting for messages to be delivered',
);
}
async function executeDelivery({
origin,
destination,
routerAddress,
tokenType,
wei,
recipient,
multiProvider,
signer,
artifacts,
skipWaitForDelivery,
}: {
origin: ChainName;
destination: ChainName;
routerAddress: Address;
tokenType: TokenType;
wei: string;
recipient?: string;
multiProvider: MultiProvider;
signer: ethers.Signer;
artifacts?: HyperlaneContractsMap<any>;
skipWaitForDelivery: boolean;
}) {
const signerAddress = await signer.getAddress();
recipient ||= signerAddress;
const mergedContractAddrs = getMergedContractAddresses(artifacts);
const core = HyperlaneCore.fromAddressesMap(
mergedContractAddrs,
multiProvider,
);
const provider = multiProvider.getProvider(origin);
const connectedSigner = signer.connect(provider);
if (tokenType === TokenType.collateral) {
const wrappedToken = await getWrappedToken(routerAddress, provider);
const token = ERC20__factory.connect(wrappedToken, connectedSigner);
const approval = await token.allowance(signerAddress, routerAddress);
if (approval.lt(wei)) {
const approveTx = await token.approve(routerAddress, wei);
await approveTx.wait();
}
}
// TODO move next section into MultiProtocolTokenApp when it exists
const adapter = new EvmHypCollateralAdapter(
origin,
MultiProtocolProvider.fromMultiProvider(multiProvider),
{ token: routerAddress },
);
const destinationDomain = multiProvider.getDomainId(destination);
const gasPayment = await adapter.quoteGasPayment(destinationDomain);
const txValue =
tokenType === TokenType.native
? BigNumber.from(gasPayment).add(wei).toString()
: gasPayment;
const transferTx = await adapter.populateTransferRemoteTx({
weiAmountOrId: wei,
destination: destinationDomain,
recipient,
txValue,
});
const txResponse = await connectedSigner.sendTransaction(transferTx);
const txReceipt = await multiProvider.handleTx(origin, txResponse);
const message = core.getDispatchedMessages(txReceipt)[0];
logBlue(`Sent message from ${origin} to ${recipient} on ${destination}.`);
logBlue(`Message ID: ${message.id}`);
if (skipWaitForDelivery) return;
// Max wait 10 minutes
await core.waitForMessageProcessed(txReceipt, 10000, 60);
logGreen(`Transfer sent to destination chain!`);
}
async function getWrappedToken(
address: Address,
provider: ethers.providers.Provider,
): Promise<Address> {
try {
const contract = HypERC20Collateral__factory.connect(address, provider);
const wrappedToken = await contract.wrappedToken();
if (ethers.utils.isAddress(wrappedToken)) return wrappedToken;
else throw new Error('Invalid wrapped token address');
} catch (error) {
log('Error getting wrapped token', error);
throw new Error(
`Could not get wrapped token from router address ${address}`,
);
}
}

@ -0,0 +1,36 @@
import { ChainName, HyperlaneCore } from '@hyperlane-xyz/sdk';
import { log, logBlue, logGreen } from '../../logger.js';
import { readDeploymentArtifacts } from '../config/artifacts.js';
import { getContext, getMergedContractAddresses } from '../context.js';
export async function checkMessageStatus({
chainConfigPath,
coreArtifactsPath,
messageId,
destination,
}: {
chainConfigPath: string;
coreArtifactsPath: string;
messageId: string;
destination: ChainName;
}) {
const { multiProvider } = getContext(chainConfigPath);
const coreArtifacts = coreArtifactsPath
? readDeploymentArtifacts(coreArtifactsPath)
: undefined;
const mergedContractAddrs = getMergedContractAddresses(coreArtifacts);
const core = HyperlaneCore.fromAddressesMap(
mergedContractAddrs,
multiProvider,
);
const mailbox = core.getContracts(destination).mailbox;
log(`Checking status of message ${messageId} on ${destination}`);
const delivered = await mailbox.delivered(messageId);
if (delivered) {
logGreen(`Message ${messageId} was delivered`);
} else {
logBlue(`Message ${messageId} was not yet delivered`);
}
}

@ -0,0 +1,44 @@
import { ethers } from 'ethers';
import { ERC20__factory } from '@hyperlane-xyz/hyperlane-token';
import { ChainName, MultiProvider } from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';
export async function assertNativeBalances(
multiProvider: MultiProvider,
signer: ethers.Signer,
chains: ChainName[],
minBalanceWei: string,
) {
const address = await signer.getAddress();
const minBalance = ethers.utils.formatEther(minBalanceWei.toString());
await Promise.all(
chains.map(async (chain) => {
const balanceWei = await multiProvider
.getProvider(chain)
.getBalance(address);
const balance = ethers.utils.formatEther(balanceWei);
if (balanceWei.lte(minBalanceWei))
throw new Error(
`${address} has insufficient balance on ${chain}. At least ${minBalance} required but found ${balance.toString()} ETH`,
);
}),
);
}
export async function assertTokenBalance(
multiProvider: MultiProvider,
signer: ethers.Signer,
chain: ChainName,
token: Address,
minBalanceWei: string,
) {
const address = await signer.getAddress();
const provider = multiProvider.getProvider(chain);
const tokenContract = ERC20__factory.connect(token, provider);
const balanceWei = await tokenContract.balanceOf(address);
if (balanceWei.lte(minBalanceWei))
throw new Error(
`${address} has insufficient balance on ${chain} for token ${token}. At least ${minBalanceWei} wei required but found ${balanceWei.toString()} wei`,
);
}

@ -0,0 +1,88 @@
import { Separator, checkbox } from '@inquirer/prompts';
import select from '@inquirer/select';
import chalk from 'chalk';
import {
ChainMap,
ChainMetadata,
mainnetChainsMetadata,
testnetChainsMetadata,
} from '@hyperlane-xyz/sdk';
import { log, logBlue } from '../../logger.js';
// A special value marker to indicate user selected
// a new chain in the list
const NEW_CHAIN_MARKER = '__new__';
export async function runOriginAndRemotesSelectionStep(
customChains: ChainMap<ChainMetadata>,
) {
const origin = await runSingleChainSelectionStep(
customChains,
'Select origin chain (the chain to which you will deploy now)',
);
const remotes = await runMultiChainSelectionStep(
customChains,
'Select remote chains the origin will send messages to',
);
return { origin, remotes };
}
export async function runSingleChainSelectionStep(
customChains: ChainMap<ChainMetadata>,
message = 'Select chain',
) {
const choices = getChainChoices(customChains);
const origin = (await select({
message,
choices,
pageSize: 20,
})) as string;
handleNewChain([origin]);
return origin;
}
export async function runMultiChainSelectionStep(
customChains: ChainMap<ChainMetadata>,
message = 'Select chains',
) {
const choices = getChainChoices(customChains);
const remotes = (await checkbox({
message,
choices,
pageSize: 20,
})) as string[];
handleNewChain(remotes);
if (!remotes?.length) throw new Error('No remote chains selected');
return remotes;
}
function getChainChoices(customChains: ChainMap<ChainMetadata>) {
const chainsToChoices = (chains: ChainMetadata[]) =>
chains.map((c) => ({ name: c.name, value: c.name }));
const choices: Parameters<typeof select>['0']['choices'] = [
new Separator('--Custom Chains--'),
...chainsToChoices(Object.values(customChains)),
{ name: '(New custom chain)', value: NEW_CHAIN_MARKER },
new Separator('--Mainnet Chains--'),
...chainsToChoices(mainnetChainsMetadata),
new Separator('--Testnet Chains--'),
...chainsToChoices(testnetChainsMetadata),
];
return choices;
}
function handleNewChain(chainNames: string[]) {
if (chainNames.includes(NEW_CHAIN_MARKER)) {
logBlue(
'To use a new chain, use the --config argument add them to that file',
);
log(
chalk.blue('Use the'),
chalk.magentaBright('hyperlane config create'),
chalk.blue('command to create new configs'),
);
process.exit(0);
}
}

@ -0,0 +1,178 @@
import { input } from '@inquirer/prompts';
import select from '@inquirer/select';
import fs from 'fs';
import path from 'path';
import { parse as yamlParse, stringify as yamlStringify } from 'yaml';
import { objMerge } from '@hyperlane-xyz/utils';
import { logBlue } from '../../logger.js';
import { getTimestampForFilename } from './time.js';
export type FileFormat = 'yaml' | 'json';
export function readFileAtPath(filepath: string) {
if (!fs.existsSync(filepath)) {
throw Error(`File doesn't exist at ${filepath}`);
}
return fs.readFileSync(filepath, 'utf8');
}
export function writeFileAtPath(filepath: string, value: string) {
const dirname = path.dirname(filepath);
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true });
}
fs.writeFileSync(filepath, value);
}
export function readJson<T>(filepath: string): T {
return JSON.parse(readFileAtPath(filepath)) as T;
}
export function tryReadJson<T>(filepath: string): T | null {
try {
return readJson(filepath) as T;
} catch (error) {
return null;
}
}
export function writeJson(filepath: string, obj: any) {
writeFileAtPath(filepath, JSON.stringify(obj, null, 2) + '\n');
}
export function mergeJson<T extends Record<string, any>>(
filepath: string,
obj: T,
) {
if (fs.existsSync(filepath)) {
const previous = readJson<T>(filepath);
writeJson(filepath, objMerge(previous, obj));
} else {
writeJson(filepath, obj);
}
}
export function readYaml<T>(filepath: string): T {
return yamlParse(readFileAtPath(filepath)) as T;
}
export function tryReadYamlAtPath<T>(filepath: string): T | null {
try {
return readYaml(filepath);
} catch (error) {
return null;
}
}
export function writeYaml(filepath: string, obj: any) {
writeFileAtPath(filepath, yamlStringify(obj, null, 2) + '\n');
}
export function mergeYaml<T extends Record<string, any>>(
filepath: string,
obj: T,
) {
if (fs.existsSync(filepath)) {
const previous = readYaml<T>(filepath);
writeYaml(filepath, objMerge(previous, obj));
} else {
writeYaml(filepath, obj);
}
}
export function readYamlOrJson<T>(filepath: string, format?: FileFormat): T {
return resolveYamlOrJson(filepath, readJson, readYaml, format);
}
export function writeYamlOrJson(
filepath: string,
obj: Record<string, any>,
format?: FileFormat,
) {
return resolveYamlOrJson(
filepath,
(f: string) => writeJson(f, obj),
(f: string) => writeYaml(f, obj),
format,
);
}
export function mergeYamlOrJson(
filepath: string,
obj: Record<string, any>,
format?: FileFormat,
) {
return resolveYamlOrJson(
filepath,
(f: string) => mergeJson(f, obj),
(f: string) => mergeYaml(f, obj),
format,
);
}
function resolveYamlOrJson(
filepath: string,
jsonFn: any,
yamlFn: any,
format?: FileFormat,
) {
if (format === 'json' || filepath.endsWith('.json')) {
return jsonFn(filepath);
} else if (
format === 'yaml' ||
filepath.endsWith('.yaml') ||
filepath.endsWith('.yml')
) {
return yamlFn(filepath);
} else {
throw new Error(`Invalid file format for ${filepath}`);
}
}
export function prepNewArtifactsFiles(
outPath: string,
files: Array<{ filename: string; description: string }>,
) {
const timestamp = getTimestampForFilename();
const newPaths: string[] = [];
for (const file of files) {
const filePath = path.join(outPath, `${file.filename}-${timestamp}.json`);
// Write empty object to ensure permissions are okay
writeJson(filePath, {});
newPaths.push(filePath);
logBlue(`${file.description} will be written to ${filePath}`);
}
return newPaths;
}
export async function runFileSelectionStep(
folderPath: string,
description: string,
pattern?: string,
) {
let filenames = fs.readdirSync(folderPath);
if (pattern) {
filenames = filenames.filter((f) => f.includes(pattern));
}
let filename = (await select({
message: `Select ${description} file`,
choices: [
...filenames.map((f) => ({ name: f, value: f })),
{ name: '(Other file)', value: null },
],
pageSize: 20,
})) as string;
if (filename) return path.join(folderPath, filename);
filename = await input({
message: `Enter ${description} filepath`,
});
if (filename) return filename;
else throw new Error(`No filepath entered ${description}`);
}

@ -0,0 +1,18 @@
import { ethers } from 'ethers';
import { ensure0x } from '@hyperlane-xyz/utils';
export function keyToSigner(key: string) {
if (!key) throw new Error('No key provided');
const formattedKey = key.trim().toLowerCase();
if (ethers.utils.isHexString(ensure0x(formattedKey)))
return new ethers.Wallet(ensure0x(formattedKey));
else if (formattedKey.split(' ').length >= 6)
return ethers.Wallet.fromMnemonic(formattedKey);
else throw new Error('Invalid key format');
}
export function assertSigner(signer: ethers.Signer) {
if (!signer || !ethers.Signer.isSigner(signer))
throw new Error('Signer is invalid');
}

@ -0,0 +1,6 @@
export function getTimestampForFilename() {
const now = new Date();
return `${now.getFullYear()}-${
now.getMonth() + 1
}-${now.getDate()}-${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}`;
}

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist/",
"rootDir": "."
},
"include": ["./cli.ts", "./logger.ts", "./src/**/*.ts", "./src/*.d.ts", "./examples/**/*.ts"],
}

@ -29,7 +29,7 @@
"@typescript-eslint/no-require-imports": ["warn"],
"@typescript-eslint/no-unused-vars": [
"error",
{
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"

@ -1,33 +1,33 @@
{
"name": "@hyperlane-xyz/helloworld",
"description": "A basic skeleton of an Hyperlane app",
"version": "1.5.1",
"version": "1.5.4-beta0",
"dependencies": {
"@hyperlane-xyz/sdk": "1.5.1",
"@hyperlane-xyz/sdk": "1.5.4-beta0",
"@openzeppelin/contracts-upgradeable": "^4.8.0",
"ethers": "^5.7.2"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.2.1",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"@trivago/prettier-plugin-sort-imports": "^3.2.0",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@typechain/ethers-v5": "10.0.0",
"@typechain/hardhat": "^6.0.0",
"@types/mocha": "^9.1.0",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"chai": "^4.3.0",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.5.0",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"ethereum-waffle": "^3.4.4",
"hardhat": "^2.16.1",
"hardhat-gas-reporter": "^1.0.9",
"prettier": "^2.4.1",
"prettier": "^2.8.8",
"prettier-plugin-solidity": "^1.0.0-beta.5",
"solhint": "^3.3.2",
"solhint-plugin-prettier": "^0.0.5",
"solidity-coverage": "^0.8.3",
"ts-node": "^10.8.0",
"ts-node": "^10.9.1",
"typechain": "8.0.0",
"typescript": "^5.1.6"
},
@ -47,7 +47,7 @@
"packageManager": "yarn@3.2.0",
"repository": {
"type": "git",
"url": "https://github.com/hyperlane-xyz/hyperlane-app-template"
"url": "https://github.com/hyperlane-xyz/hyperlane-monorepo"
},
"scripts": {
"build": "hardhat compile && tsc",
@ -65,5 +65,6 @@
"lodash": "^4.17.21",
"async": "^2.6.4",
"undici": "^5.11"
}
},
"stableVersion": "1.5.3"
}

@ -11,10 +11,10 @@
"@ethersproject/experimental": "^5.7.0",
"@ethersproject/hardware-wallets": "^5.7.0",
"@ethersproject/providers": "^5.7.2",
"@hyperlane-xyz/helloworld": "1.5.1",
"@hyperlane-xyz/hyperlane-token": "1.5.1",
"@hyperlane-xyz/sdk": "1.5.1",
"@hyperlane-xyz/utils": "1.5.1",
"@hyperlane-xyz/helloworld": "1.5.4-beta0",
"@hyperlane-xyz/hyperlane-token": "1.5.4-beta0",
"@hyperlane-xyz/sdk": "1.5.4-beta0",
"@hyperlane-xyz/utils": "1.5.4-beta0",
"@nomiclabs/hardhat-etherscan": "^3.0.3",
"@safe-global/api-kit": "^1.3.0",
"@safe-global/protocol-kit": "^1.2.0",
@ -24,7 +24,7 @@
"dotenv": "^10.0.0",
"prom-client": "^14.0.1",
"prompts": "^2.4.2",
"yargs": "^17.4.1"
"yargs": "^17.7.2"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.2.1",
@ -33,13 +33,13 @@
"@types/mocha": "^9.1.0",
"@types/node": "^16.9.1",
"@types/prompts": "^2.0.14",
"@types/yargs": "^17.0.10",
"@types/yargs": "^17.0.24",
"chai": "^4.3.4",
"ethereum-waffle": "^3.4.4",
"ethers": "^5.7.2",
"hardhat": "^2.16.1",
"prettier": "^2.4.1",
"ts-node": "^10.8.0",
"prettier": "^2.8.8",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
},
"private": true,

@ -27,7 +27,7 @@ import { LiquidityLayerRelayerConfig } from './middleware';
export const EnvironmentNames = Object.keys(environments);
export type DeployEnvironment = keyof typeof environments;
export type EnvironmentChain<E extends DeployEnvironment> = Extract<
keyof typeof environments[E],
keyof (typeof environments)[E],
ChainName
>;

@ -21,5 +21,5 @@ export const ALL_AGENT_ROLES = [
Role.Relayer,
Role.Scraper,
] as const;
export type AgentRole = typeof ALL_AGENT_ROLES[number];
export type AgentRole = (typeof ALL_AGENT_ROLES)[number];
export type AgentChainNames = Record<AgentRole, string[]>;

@ -1,10 +1,10 @@
{
"name": "@hyperlane-xyz/sdk",
"description": "The official SDK for the Hyperlane Network",
"version": "1.5.1",
"version": "1.5.4-beta0",
"dependencies": {
"@hyperlane-xyz/core": "1.5.1",
"@hyperlane-xyz/utils": "1.5.1",
"@hyperlane-xyz/core": "1.5.4-beta0",
"@hyperlane-xyz/utils": "1.5.4-beta0",
"@solana/web3.js": "^1.78.0",
"@types/coingecko-api": "^1.0.10",
"@types/debug": "^4.1.7",
@ -28,9 +28,9 @@
"fs": "0.0.1-security",
"hardhat": "^2.16.1",
"mocha": "^9.2.2",
"prettier": "^2.4.1",
"prettier": "^2.8.8",
"sinon": "^13.0.2",
"ts-node": "^10.8.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
},
"files": [
@ -57,5 +57,6 @@
"test:unit": "mocha --config .mocharc.json './src/**/*.test.ts'",
"test:hardhat": "hardhat test $(find ./src -name \"*.hardhat-test.ts\")"
},
"types": "dist/index.d.ts"
"types": "dist/index.d.ts",
"stableVersion": "1.5.3"
}

@ -16,8 +16,10 @@ import { MultiGeneric } from '../utils/MultiGeneric';
export class HyperlaneApp<
Factories extends HyperlaneFactories,
> extends MultiGeneric<HyperlaneContracts<Factories>> {
public readonly contractsMap: HyperlaneContractsMap<Factories>;
constructor(
public readonly contractsMap: HyperlaneContractsMap<Factories>,
contractsMap: HyperlaneContractsMap<Factories>,
public readonly multiProvider: MultiProvider,
public readonly logger = debug('hyperlane:App'),
) {
@ -25,6 +27,7 @@ export class HyperlaneApp<
connectContracts(contracts, multiProvider.getSignerOrProvider(chain)),
);
super(connectedContractsMap);
this.contractsMap = connectedContractsMap;
}
getContracts(chain: ChainName): HyperlaneContracts<Factories> {

@ -0,0 +1,26 @@
import { ChainMap } from '../types';
// TODO this was previously in hyp-deploy, but ideally should be integrated
// into the ChainMetadata type
export const agentStartBlocks: ChainMap<number> = {
// --------------- Mainnets ---------------------
celo: 16884144,
ethereum: 16271503,
avalanche: 24145479,
polygon: 37313389,
bsc: 25063295,
arbitrum: 49073182,
optimism: 55698988,
moonbeam: 2595747,
gnosis: 25900000,
// --------------- Testnets ---------------------
alfajores: 14863532,
fuji: 16330615,
mumbai: 29390033,
bsctestnet: 25001629,
goerli: 8039005,
sepolia: 3082913,
moonbasealpha: 3310405,
optimismgoerli: 3055263,
arbitrumgoerli: 1941997,
};

@ -11,7 +11,7 @@ export const hyperlaneEnvironments = { test, testnet, mainnet };
export type HyperlaneEnvironment = keyof typeof hyperlaneEnvironments;
export type HyperlaneEnvironmentChain<E extends HyperlaneEnvironment> = Extract<
keyof typeof hyperlaneEnvironments[E],
keyof (typeof hyperlaneEnvironments)[E],
ChainName
>;
@ -19,4 +19,7 @@ export type HyperlaneEnvironmentChain<E extends HyperlaneEnvironment> = Extract<
export const hyperlaneContractAddresses = objMerge(
hyperlaneEnvironments.testnet,
hyperlaneEnvironments.mainnet,
) as Record<CoreChainName, typeof hyperlaneEnvironments['mainnet']['ethereum']>;
) as Record<
CoreChainName,
(typeof hyperlaneEnvironments)['mainnet']['ethereum']
>;

@ -69,7 +69,7 @@ export class HyperlaneCore extends HyperlaneApp<CoreFactories> {
destination: ChainName,
delayMs?: number,
maxAttempts?: number,
): Promise<void> {
): Promise<true> {
await pollAsync(
async () => {
this.logger(`Checking if message ${messageId} was processed`);
@ -77,7 +77,7 @@ export class HyperlaneCore extends HyperlaneApp<CoreFactories> {
const delivered = await mailbox.delivered(messageId);
if (delivered) {
this.logger(`Message ${messageId} was processed`);
return;
return true;
} else {
throw new Error(`Message ${messageId} not yet processed`);
}
@ -85,7 +85,7 @@ export class HyperlaneCore extends HyperlaneApp<CoreFactories> {
delayMs,
maxAttempts,
);
return;
return true;
}
waitForMessageProcessing(
@ -112,6 +112,7 @@ export class HyperlaneCore extends HyperlaneApp<CoreFactories> {
),
),
);
this.logger(`All messages processed for tx ${sourceTx.transactionHash}`);
}
// Redundant with static method but keeping for backwards compatibility

@ -62,7 +62,7 @@ describe('MultiProtocolCore', () => {
type: ProviderType.EthersV5,
receipt,
});
});
}).timeout(10000);
it('to Sealevel', async () => {
const multiProvider = new MultiProtocolProvider();
@ -88,6 +88,6 @@ describe('MultiProtocolCore', () => {
type: ProviderType.EthersV5,
receipt,
});
});
}).timeout(10000);
});
});

@ -57,15 +57,15 @@ export class MultiProtocolCore extends MultiProtocolApp<
throw new Error(`No adapter for protocol ${protocol}`);
}
waitForMessagesProcessed(
async waitForMessagesProcessed(
origin: ChainName,
destination: ChainName,
sourceTx: TypedTransactionReceipt,
delayMs?: number,
maxAttempts?: number,
): Promise<void[]> {
): Promise<boolean> {
const messages = this.adapter(origin).extractMessageIds(sourceTx);
return Promise.all(
await Promise.all(
messages.map((msg) =>
this.adapter(destination).waitForMessageProcessed(
msg.messageId,
@ -75,5 +75,6 @@ export class MultiProtocolCore extends MultiProtocolApp<
),
),
);
return true;
}
}

@ -60,7 +60,7 @@ export class EvmCoreAdapter extends BaseEvmAdapter implements ICoreAdapter {
destination: ChainName,
delayMs?: number,
maxAttempts?: number,
): Promise<void> {
): Promise<boolean> {
return this.core.waitForMessageIdProcessed(
messageId,
destination,

@ -57,7 +57,7 @@ export class SealevelCoreAdapter
destination: ChainName,
delayMs?: number,
maxAttempts?: number,
): Promise<void> {
): Promise<boolean> {
const pda = SealevelCoreAdapter.deriveMailboxMessageProcessedPda(
this.addresses.mailbox,
messageId,
@ -75,6 +75,8 @@ export class SealevelCoreAdapter
delayMs,
maxAttempts,
);
return true;
}
static parseMessageDispatchLogs(

@ -13,5 +13,5 @@ export interface ICoreAdapter extends BaseAppAdapter {
destination: ChainName,
delayMs?: number,
maxAttempts?: number,
): Promise<void>;
): Promise<boolean>;
}

@ -6,6 +6,7 @@ export {
BaseSealevelAdapter,
MultiProtocolApp,
} from './app/MultiProtocolApp';
export { agentStartBlocks } from './consts/agentStartBlocks';
export {
chainIdToMetadata,
chainMetadata,

@ -33,6 +33,7 @@ import {
RoutingIsmConfig,
} from './types';
// TODO this should handle cached addresses like the other deployers
export class HyperlaneIsmFactory extends HyperlaneApp<IsmFactoryFactories> {
static fromEnvironment<Env extends HyperlaneEnvironment>(
env: Env,

@ -165,6 +165,21 @@ export class ChainMetadataManager<MetaExt = {}> {
return getDomainId(metadata);
}
/**
* Get the protocol type for a given chain name, chain id, or domain id
*/
tryGetProtocol(chainNameOrId: ChainName | number): ProtocolType | null {
return this.tryGetChainMetadata(chainNameOrId)?.protocol ?? null;
}
/**
* Get the protocol type for a given chain name, chain id, or domain id
* @throws if chain's metadata or protocol has not been set
*/
getProtocol(chainNameOrId: ChainName | number): ProtocolType {
return this.getChainMetadata(chainNameOrId).protocol;
}
/**
* Get the domain ids for a list of chain names, chain ids, or domain ids
* @throws if any chain's metadata has not been set

@ -1,11 +1,11 @@
{
"name": "@hyperlane-xyz/hyperlane-token",
"description": "A template for interchain ERC20 and ERC721 tokens using Hyperlane",
"version": "1.5.1",
"version": "1.5.4-beta0",
"dependencies": {
"@hyperlane-xyz/core": "1.5.1",
"@hyperlane-xyz/sdk": "1.5.1",
"@hyperlane-xyz/utils": "1.5.1",
"@hyperlane-xyz/core": "1.5.4-beta0",
"@hyperlane-xyz/sdk": "1.5.4-beta0",
"@hyperlane-xyz/utils": "1.5.4-beta0",
"@openzeppelin/contracts-upgradeable": "^4.8.0",
"@solana/spl-token": "^0.3.8",
"ethers": "^5.7.2"
@ -13,24 +13,24 @@
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.2.1",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"@trivago/prettier-plugin-sort-imports": "^3.2.0",
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
"@typechain/ethers-v5": "10.0.0",
"@typechain/hardhat": "^6.0.0",
"@types/mocha": "^9.1.0",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"chai": "^4.3.0",
"eslint": "^8.16.0",
"eslint-config-prettier": "^8.5.0",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"ethereum-waffle": "^3.4.4",
"hardhat": "^2.16.1",
"hardhat-gas-reporter": "^1.0.9",
"prettier": "^2.4.1",
"prettier": "^2.8.8",
"prettier-plugin-solidity": "^1.0.0-beta.5",
"solhint": "^3.3.2",
"solhint-plugin-prettier": "^0.0.5",
"solidity-coverage": "^0.8.3",
"ts-node": "^10.8.0",
"ts-node": "^10.9.1",
"typechain": "8.0.0",
"typescript": "^5.1.6"
},
@ -62,5 +62,6 @@
"test": "hardhat test ./test/*.test.ts",
"deploy-warp-route": "DEBUG=* ts-node scripts/deploy"
},
"types": "dist/index.d.ts"
"types": "dist/index.d.ts",
"stableVersion": "1.5.3"
}

@ -12,4 +12,4 @@
"./src/types/hardhat.d.ts",
"hardhat.config.ts"
],
}
}

@ -1,7 +1,7 @@
{
"name": "@hyperlane-xyz/utils",
"description": "General utilities and types for the Hyperlane network",
"version": "1.5.1",
"version": "1.5.4-beta0",
"dependencies": {
"@solana/web3.js": "^1.78.0",
"bignumber.js": "^9.1.1",
@ -9,7 +9,7 @@
},
"devDependencies": {
"chai": "^4.3.0",
"prettier": "^2.4.1",
"prettier": "^2.8.8",
"typescript": "^5.1.6"
},
"homepage": "https://www.hyperlane.xyz",
@ -32,5 +32,6 @@
"types": "dist/index.d.ts",
"files": [
"/dist"
]
],
"stableVersion": "1.5.3"
}

@ -17,7 +17,7 @@ export const ProtocolSmallestUnit = {
export type Domain = number;
export type Address = string;
export type AddressBytes32 = string;
export type ChainCaip2Id = `${string}:${string}`; // e.g. ethereum:1 or solana:mainnet-beta
export type ChainCaip2Id = `${string}:${string}`; // e.g. ethereum:1 or sealevel:1399811149
export type TokenCaip19Id = `${string}:${string}/${string}:${string}`; // e.g. ethereum:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f
export type HexString = string;

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save