diff --git a/typescript/cli/examples/multisig-ism.yaml b/typescript/cli/examples/multisig-ism.yaml index 726617e797..2b33eed1a2 100644 --- a/typescript/cli/examples/multisig-ism.yaml +++ b/typescript/cli/examples/multisig-ism.yaml @@ -1,20 +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 -# Module types: -# UNUSED : 0 -# ROUTING : 1 -# AGGREGATION : 2 -# LEGACY_MULTISIG : 3 -# MERKLE_ROOT_MULTISIG : 4 -# MESSAGE_ID_MULTISIG : 5 +# +# Valid module types: +# routing +# aggregation +# legacy_multisig +# merkle_root_multisig +# message_id_multisig --- anvil1: - type: 3 # ModuleType.LEGACY_MULTISIG + type: 'legacy_multisig' threshold: 1 # Number: Signatures required to approve a message validators: # Array: List of validator configs - '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' anvil2: - type: 3 + type: 'legacy_multisig' threshold: 1 validators: - '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' diff --git a/typescript/cli/examples/warp-tokens.yaml b/typescript/cli/examples/warp-tokens.yaml index 984e703955..0d78580f59 100644 --- a/typescript/cli/examples/warp-tokens.yaml +++ b/typescript/cli/examples/warp-tokens.yaml @@ -1,5 +1,6 @@ # A config for a Warp Route deployment # Typically used with the 'hyperlane deploy warp' command +# # Token Types: # synthetic # syntheticUri diff --git a/typescript/cli/src/commands/config.ts b/typescript/cli/src/commands/config.ts index 628790305e..566589bd6e 100644 --- a/typescript/cli/src/commands/config.ts +++ b/typescript/cli/src/commands/config.ts @@ -1,4 +1,4 @@ -import { input } from '@inquirer/prompts'; +import { confirm, input } from '@inquirer/prompts'; import select from '@inquirer/select'; import { CommandModule } from 'yargs'; @@ -6,7 +6,7 @@ import { ChainMetadata, isValidChainMetadata } from '@hyperlane-xyz/sdk'; import { ProtocolType } from '@hyperlane-xyz/utils'; import { readChainConfig } from '../configs.js'; -import { errorRed, logBlue, logGreen } from '../logger.js'; +import { errorRed, log, logBlue, logGreen } from '../logger.js'; import { FileFormat, mergeYamlOrJson } from '../utils/files.js'; /** @@ -21,7 +21,7 @@ export const configCommand: CommandModule = { .command(validateCommand) .version(false) .demandCommand(), - handler: () => console.log('Command required'), + handler: () => log('Command required'), }; /** @@ -52,9 +52,17 @@ const createCommand: CommandModule = { message: 'Enter chain name (one word, lower case)', }); const chainId = await input({ message: 'Enter chain id (number)' }); - const domainId = await input({ - message: 'Enter domain id (number, often matches chainId)', + 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) => ({ diff --git a/typescript/cli/src/commands/deploy.ts b/typescript/cli/src/commands/deploy.ts index 609cd664d3..5e6dee1403 100644 --- a/typescript/cli/src/commands/deploy.ts +++ b/typescript/cli/src/commands/deploy.ts @@ -2,7 +2,7 @@ import { CommandModule } from 'yargs'; import { runCoreDeploy } from '../deploy/core.js'; import { runWarpDeploy } from '../deploy/warp.js'; -import { logGray } from '../logger.js'; +import { log, logGray } from '../logger.js'; import { chainsCommandOption, @@ -23,7 +23,7 @@ export const deployCommand: CommandModule = { .command(warpCommand) .version(false) .demandCommand(), - handler: () => console.log('Command required'), + handler: () => log('Command required'), }; /** diff --git a/typescript/cli/src/configs.ts b/typescript/cli/src/configs.ts index 2cb9e3bb57..76f21f89c5 100644 --- a/typescript/cli/src/configs.ts +++ b/typescript/cli/src/configs.ts @@ -6,16 +6,18 @@ import { ChainMap, ChainMetadata, HyperlaneContractsMap, + ModuleType, MultisigIsmConfig, isValidChainMetadata, } from '@hyperlane-xyz/sdk'; +import { objMap } from '@hyperlane-xyz/utils'; import { getMultiProvider } from './context.js'; import { errorRed, log, logGreen } from './logger.js'; import { readYamlOrJson } from './utils/files.js'; export function readChainConfig(filepath: string) { - console.log(`Reading file configs in ${filepath}`); + log(`Reading file configs in ${filepath}`); const chainToMetadata = readYamlOrJson>(filepath); if ( @@ -76,7 +78,7 @@ export function readDeploymentArtifacts(filePath: string) { const MultisigConfigSchema = z.object({}).catchall( z.object({ - type: z.number(), + type: z.string(), threshold: z.number(), validators: z.array(z.string()), }), @@ -92,7 +94,20 @@ export function readMultisigConfig(filePath: string) { `Invalid multisig config: ${firstIssue.path} => ${firstIssue.message}`, ); } - return result.data as ChainMap; + const parsedConfig = result.data; + const formattedConfig = objMap(parsedConfig, (_, config) => ({ + ...config, + type: humanReadableIsmTypeToEnum(config.type), + })); + + return formattedConfig as ChainMap; +} + +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}`); } const ConnectionConfigSchema = { diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index 4e6ed1a2ec..8997fd8f32 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -1,11 +1,8 @@ -import { Separator, checkbox, confirm, input } from '@inquirer/prompts'; -import select from '@inquirer/select'; -import chalk from 'chalk'; +import { confirm, input } from '@inquirer/prompts'; import { ethers } from 'ethers'; import { ChainMap, - ChainMetadata, ChainName, CoreConfig, DeployedIsm, @@ -26,10 +23,8 @@ import { agentStartBlocks, buildAgentConfig, defaultMultisigIsmConfigs, - mainnetChainsMetadata, multisigIsmVerificationCost, serializeContractsMap, - testnetChainsMetadata, } from '@hyperlane-xyz/sdk'; import { Address, objFilter, objMerge } from '@hyperlane-xyz/utils'; @@ -41,6 +36,7 @@ import { sdkContractAddressesMap, } from '../context.js'; import { log, logBlue, logGray, logGreen } from '../logger.js'; +import { runLocalAndRemotesSelectionStep } from '../utils/chains.js'; import { prepNewArtifactsFiles, writeJson } from '../utils/files.js'; import { @@ -63,11 +59,10 @@ export async function runCoreDeploy({ chainConfigPath, ); - const { local, remotes, allChains } = await runChainSelectionStep( - customChains, - ); - const artifacts = await runArtifactStep(allChains); - const multisigConfig = await runIsmStep(allChains); + const { local, remotes, selectedChains } = + await runLocalAndRemotesSelectionStep(customChains); + const artifacts = await runArtifactStep(selectedChains); + const multisigConfig = await runIsmStep(selectedChains); const deploymentParams = { local, @@ -87,41 +82,10 @@ export async function runCoreDeploy({ await executeDeploy(deploymentParams); } -async function runChainSelectionStep(customChains: ChainMap) { - const chainsToChoices = (chains: ChainMetadata[]) => - chains.map((c) => ({ name: c.name, value: c.name })); - const choices: Parameters['0']['choices'] = [ - new Separator('--Custom Chains--'), - ...chainsToChoices(Object.values(customChains)), - { name: '(New custom chain)', value: '__new__' }, - new Separator('--Mainnet Chains--'), - ...chainsToChoices(mainnetChainsMetadata), - new Separator('--Testnet Chains--'), - ...chainsToChoices(testnetChainsMetadata), - ]; - - const local = (await select({ - message: 'Select local chain (the chain to which you will deploy now)', - choices, - pageSize: 20, - })) as string; - handleNewChain([local]); - - const remotes = (await checkbox({ - message: 'Select remote chains the local will send messages to', - choices, - pageSize: 20, - })) as string[]; - handleNewChain(remotes); - if (!remotes?.length) throw new Error('No remote chains selected'); - - const allChains = [local, ...remotes]; - return { local, remotes, allChains }; -} - -async function runArtifactStep(allChains: ChainName[]) { +async function runArtifactStep(selectedChains: ChainName[]) { logBlue( - '\nDeployments can be totally new or can use some existing contract addresses.', + '\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?', @@ -133,15 +97,16 @@ async function runArtifactStep(allChains: ChainName[]) { }); const artifacts = readDeploymentArtifacts(artifactsPath); const artifactChains = Object.keys(artifacts).filter((c) => - allChains.includes(c), + selectedChains.includes(c), ); log(`Found existing artifacts for chains: ${artifactChains.join(', ')}`); return artifacts; } -async function runIsmStep(allChains: ChainName[]) { +async function runIsmStep(selectedChains: ChainName[]) { logBlue( - '\nHyperlane instances requires an Interchain Security Module (ISM).', + '\n', + 'Hyperlane instances requires an Interchain Security Module (ISM).', ); const isMultisig = await confirm({ message: 'Do you want use a Multisig ISM?', @@ -152,7 +117,7 @@ async function runIsmStep(allChains: ChainName[]) { ); const defaultConfigChains = Object.keys(defaultMultisigIsmConfigs); - const configRequired = !!allChains.find( + const configRequired = !!selectedChains.find( (c) => !defaultConfigChains.includes(c), ); if (!configRequired) return; @@ -165,26 +130,12 @@ async function runIsmStep(allChains: ChainName[]) { }); const configs = readMultisigConfig(multisigConfigPath); const multisigConfigChains = Object.keys(configs).filter((c) => - allChains.includes(c), + selectedChains.includes(c), ); log(`Found configs for chains: ${multisigConfigChains.join(', ')}`); return configs; } -function handleNewChain(chainNames: string[]) { - if (chainNames.includes('__new__')) { - 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); - } -} - interface DeployParams { local: string; remotes: string[]; @@ -202,7 +153,7 @@ async function runDeployPlanStep({ artifacts, }: DeployParams) { const address = await signer.getAddress(); - logBlue('\nDeployment plan:'); + logBlue('\n', 'Deployment plan:'); logGray('===============:'); log(`Transaction signer and owner of new contracts will be ${address}`); log(`Deploying to ${local} and connecting it to ${remotes.join(', ')}`); @@ -210,14 +161,12 @@ async function runDeployPlanStep({ Object.values(sdkContractAddressesMap)[0], ).length; log(`There are ${numContracts} contracts for each chain`); - if (artifacts) { + if (artifacts) log('But contracts with an address in the artifacts file will be skipped'); - for (const chain of [local, ...remotes]) { - const chainArtifacts = artifacts[chain]; - if (!chainArtifacts) continue; - const numRequired = numContracts - Object.keys(chainArtifacts).length; - log(`${chain} will require ${numRequired} of ${numContracts}`); - } + for (const chain of [local, ...remotes]) { + const chainArtifacts = artifacts?.[chain] || {}; + const numRequired = numContracts - Object.keys(chainArtifacts).length; + log(`${chain} will require ${numRequired} of ${numContracts}`); } log('The interchain security module will be a Multisig.'); const isConfirmed = await confirm({ @@ -243,14 +192,14 @@ async function executeDeploy({ ]); const owner = await signer.getAddress(); - const allChains = [local, ...remotes]; + const selectedChains = [local, ...remotes]; const mergedContractAddrs = getMergedContractAddresses(artifacts); // 1. Deploy ISM factories to all deployable chains that don't have them. log('Deploying ISM factory contracts'); const ismDeployer = new HyperlaneIsmFactoryDeployer(multiProvider); ismDeployer.cacheAddressesMap(mergedContractAddrs); - const ismFactoryContracts = await ismDeployer.deploy(allChains); + const ismFactoryContracts = await ismDeployer.deploy(selectedChains); artifacts = writeMergedAddresses( contractsFilePath, artifacts, @@ -262,8 +211,8 @@ async function executeDeploy({ log(`Deploying IGP contracts`); const igpConfig = buildIgpConfigMap( owner, - allChains, - allChains, + selectedChains, + selectedChains, multiSigConfig, ); const igpDeployer = new HyperlaneIgpDeployer(multiProvider); @@ -293,7 +242,7 @@ async function executeDeploy({ const ismConfigs = buildIsmConfigMap( owner, remotes, - allChains, + selectedChains, multiSigConfig, ); const ismContracts: ChainMap<{ multisigIsm: DeployedIsm }> = {}; @@ -312,7 +261,10 @@ async function executeDeploy({ // 5. Deploy TestRecipients to all deployable chains log(`Deploying test recipient contracts`); - const testRecipientConfig = buildTestRecipientConfigMap(allChains, artifacts); + const testRecipientConfig = buildTestRecipientConfigMap( + selectedChains, + artifacts, + ); const testRecipientDeployer = new TestRecipientDeployer(multiProvider); testRecipientDeployer.cacheAddressesMap(artifacts); const testRecipients = await testRecipientDeployer.deploy( @@ -407,7 +359,7 @@ function buildTestRecipientConfigMap( function buildIgpConfigMap( owner: Address, deployChains: ChainName[], - allChains: ChainName[], + selectedChains: ChainName[], multisigIsmConfigs: ChainMap, ): ChainMap { const mergedMultisigIsmConfig: ChainMap = objMerge( @@ -418,7 +370,7 @@ function buildIgpConfigMap( for (const local of deployChains) { const overhead: ChainMap = {}; const gasOracleType: ChainMap = {}; - for (const remote of allChains) { + for (const remote of selectedChains) { if (local === remote) continue; overhead[remote] = multisigIsmVerificationCost( mergedMultisigIsmConfig[remote].threshold, @@ -455,7 +407,7 @@ async function writeAgentConfig( remotes: ChainName[], multiProvider: MultiProvider, ) { - const allChains = [local, ...remotes]; + const selectedChains = [local, ...remotes]; const startBlocks: ChainMap = { ...agentStartBlocks }; startBlocks[local] = await multiProvider.getProvider(local).getBlockNumber(); @@ -466,14 +418,14 @@ async function writeAgentConfig( const filteredAddressesMap = objFilter( mergedAddressesMap, (chain, v): v is HyperlaneAddresses => - allChains.includes(chain) && + selectedChains.includes(chain) && !!v.mailbox && !!v.interchainGasPaymaster && !!v.validatorAnnounce, ) as ChainMap; const agentConfig = buildAgentConfig( - allChains, + selectedChains, multiProvider, filteredAddressesMap, startBlocks, diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index b9dd1f03b0..5fa206c2ea 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -200,7 +200,7 @@ async function runDeployPlanStep({ const address = await signer.getAddress(); const baseToken = configMap[local]; const baseName = getTokenName(baseToken); - logBlue('\nDeployment plan:'); + logBlue('\n', 'Deployment 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 ${local}`); diff --git a/typescript/cli/src/send/message.ts b/typescript/cli/src/send/message.ts index 30a38b5b2f..eca03ddb00 100644 --- a/typescript/cli/src/send/message.ts +++ b/typescript/cli/src/send/message.ts @@ -2,13 +2,12 @@ import { BigNumber, ethers } from 'ethers'; import { ChainName, - DispatchedMessage, HyperlaneContractsMap, HyperlaneCore, HyperlaneIgp, MultiProvider, } from '@hyperlane-xyz/sdk'; -import { addressToBytes32, sleep, timeout } from '@hyperlane-xyz/utils'; +import { addressToBytes32, timeout } from '@hyperlane-xyz/utils'; import { readDeploymentArtifacts } from '../configs.js'; import { MINIMUM_TEST_SEND_BALANCE } from '../consts.js'; @@ -80,7 +79,7 @@ async function executeDelivery({ const destinationDomain = multiProvider.getDomainId(destination); const signerAddress = await signer.getAddress(); - let message: DispatchedMessage; + let messageReceipt: ethers.ContractReceipt; try { const recipient = mergedContractAddrs[destination].testRecipient; if (!recipient) { @@ -93,8 +92,8 @@ async function executeDelivery({ addressToBytes32(recipient), '0x48656c6c6f21', // Hello! ); - const messageReceipt = await multiProvider.handleTx(origin, messageTx); - message = core.getDispatchedMessages(messageReceipt)[0]; + messageReceipt = await multiProvider.handleTx(origin, messageTx); + const message = core.getDispatchedMessages(messageReceipt)[0]; logBlue(`Sent message from ${origin} to ${recipient} on ${destination}.`); logBlue(`Message ID: ${message.id}`); @@ -118,15 +117,7 @@ async function executeDelivery({ ); throw e; } - while (true) { - const destination = multiProvider.getChainName(message.parsed.destination); - const mailbox = core.getContracts(destination).mailbox; - const delivered = await mailbox.delivered(message.id); - if (delivered) break; - - log('Waiting for message delivery on destination chain...'); - await sleep(5000); - } - + log('Waiting for message delivery on destination chain...'); + await core.waitForMessageProcessed(messageReceipt, 5000); logGreen('Message was delivered!'); } diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index cda07256ed..af11986fd2 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -1,4 +1,3 @@ -import assert from 'assert'; import { ethers } from 'ethers'; import { @@ -12,13 +11,13 @@ import { HyperlaneCore, MultiProvider, } from '@hyperlane-xyz/sdk'; -import { Address, sleep, timeout } from '@hyperlane-xyz/utils'; +import { Address, timeout } from '@hyperlane-xyz/utils'; import { readDeploymentArtifacts } from '../configs.js'; import { MINIMUM_TEST_SEND_BALANCE } from '../consts.js'; import { getDeployerContext, getMergedContractAddresses } from '../context.js'; import { runPreflightChecks } from '../deploy/utils.js'; -import { log, logBlue, logGreen } from '../logger.js'; +import { logBlue, logGreen } from '../logger.js'; import { assertTokenBalance } from '../utils/balances.js'; // TODO improve the UX here by making params optional and @@ -144,18 +143,7 @@ async function executeDelivery({ const message = core.getDispatchedMessages(receipt)[0]; logBlue(`Sent message from ${origin} to ${recipient} on ${destination}.`); logBlue(`Message ID: ${message.id}`); - - const msgDestination = multiProvider.getChainName(message.parsed.destination); - assert(destination === msgDestination); - - while (true) { - const mailbox = core.getContracts(destination).mailbox; - const delivered = await mailbox.delivered(message.id); - if (delivered) break; - log('Waiting for message delivery on destination chain...'); - await sleep(5000); - } - + await core.waitForMessageProcessed(receipt, 5000); logGreen(`Transfer sent to destination chain!`); } diff --git a/typescript/cli/src/utils/chains.ts b/typescript/cli/src/utils/chains.ts new file mode 100644 index 0000000000..7da05cdb5c --- /dev/null +++ b/typescript/cli/src/utils/chains.ts @@ -0,0 +1,89 @@ +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 runLocalAndRemotesSelectionStep( + customChains: ChainMap, +) { + const local = await runSingleChainSelectionStep( + customChains, + 'Select local chain (the chain to which you will deploy now)', + ); + const remotes = await runMultiChainSelectionStep( + customChains, + 'Select remote chains the local will send messages to', + ); + const selectedChains = [local, ...remotes]; + return { local, remotes, selectedChains }; +} + +export async function runSingleChainSelectionStep( + customChains: ChainMap, + message = 'Select chain', +) { + const choices = getChainChoices(customChains); + const local = (await select({ + message, + choices, + pageSize: 20, + })) as string; + handleNewChain([local]); + return local; +} + +export async function runMultiChainSelectionStep( + customChains: ChainMap, + 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) { + const chainsToChoices = (chains: ChainMetadata[]) => + chains.map((c) => ({ name: c.name, value: c.name })); + const choices: Parameters['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); + } +} diff --git a/typescript/sdk/src/core/HyperlaneCore.ts b/typescript/sdk/src/core/HyperlaneCore.ts index 1f9fd6e280..b3116c0c7b 100644 --- a/typescript/sdk/src/core/HyperlaneCore.ts +++ b/typescript/sdk/src/core/HyperlaneCore.ts @@ -82,15 +82,21 @@ export class HyperlaneCore extends HyperlaneApp { protected async waitForMessageWasProcessed( message: DispatchedMessage, + delay?: number, + maxAttempts?: number, ): Promise { const id = messageId(message.message); const { mailbox } = this.getDestination(message); - await pollAsync(async () => { - const delivered = await mailbox.delivered(id); - if (!delivered) { - throw new Error(`Message ${id} not yet processed`); - } - }); + await pollAsync( + async () => { + const delivered = await mailbox.delivered(id); + if (!delivered) { + throw new Error(`Message ${id} not yet processed`); + } + }, + delay, + maxAttempts, + ); return; } @@ -103,10 +109,14 @@ export class HyperlaneCore extends HyperlaneApp { async waitForMessageProcessed( sourceTx: ethers.ContractReceipt, + delay?: number, + maxAttempts?: number, ): Promise { const messages = HyperlaneCore.getDispatchedMessages(sourceTx); await Promise.all( - messages.map((msg) => this.waitForMessageWasProcessed(msg)), + messages.map((msg) => + this.waitForMessageWasProcessed(msg, delay, maxAttempts), + ), ); }