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
parent
9b1824aff5
commit
fd48f137c7
@ -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"], |
||||
} |
@ -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, |
||||
}; |
Loading…
Reference in new issue