UNPKG

cosmic-interchain-cli

Version:

A command-line utility for Cosmic Wire's interchain messaging protocol

465 lines 22 kB
import { confirm } from '@inquirer/prompts'; import { stringify as yamlStringify } from 'yaml'; import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; import { ChainSubmissionStrategySchema, ContractVerifier, EvmERC20WarpModule, EvmERC20WarpRouteReader, EvmIsmModule, ExplorerLicenseType, HypERC20Deployer, HypERC721Deployer, HyperlaneProxyFactoryDeployer, IsmType, TOKEN_TYPE_TO_STANDARD, TxSubmitterType, WarpCoreConfigSchema, WarpRouteDeployConfigSchema, attachContractsMap, connectContractsMap, getTokenConnectionId, hypERC20factories, isCollateralConfig, isTokenMetadata, serializeContracts, } from '@hyperlane-xyz/sdk'; import { ProtocolType, assert, objFilter, objKeys, objMap, promiseObjAll, } from '@hyperlane-xyz/utils'; import { readWarpRouteDeployConfig } from '../config/warp.js'; import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js'; import { getOrRequestApiKeys } from '../context/context.js'; import { log, logBlue, logGray, logGreen, logRed, logTable, } from '../logger.js'; import { getSubmitterBuilder } from '../submit/submit.js'; import { indentYamlOrJson, isFile, readYamlOrJson, runFileSelectionStep, } from '../utils/files.js'; import { completeDeploy, prepareDeploy, runPreflightChecksForChains, } from './utils.js'; export async function runWarpRouteDeploy({ context, warpRouteDeploymentConfigPath, }) { const { signer, skipConfirmation, chainMetadata } = context; if (!warpRouteDeploymentConfigPath || !isFile(warpRouteDeploymentConfigPath)) { if (skipConfirmation) throw new Error('Warp route deployment config required'); warpRouteDeploymentConfigPath = await runFileSelectionStep('./configs', 'Warp route deployment config', 'warp'); } else { log(`Using warp route deployment config at ${warpRouteDeploymentConfigPath}`); } const warpRouteConfig = await readWarpRouteDeployConfig(warpRouteDeploymentConfigPath, context); const chains = Object.keys(warpRouteConfig); let apiKeys = {}; if (!skipConfirmation) apiKeys = await getOrRequestApiKeys(chains, chainMetadata); const deploymentParams = { context, warpDeployConfig: warpRouteConfig, }; await runDeployPlanStep(deploymentParams); await runPreflightChecksForChains({ context, chains, minGas: MINIMUM_WARP_DEPLOY_GAS, }); const userAddress = await signer.getAddress(); const initialBalances = await prepareDeploy(context, userAddress, chains); const deployedContracts = await executeDeploy(deploymentParams, apiKeys); const warpCoreConfig = await getWarpCoreConfig(deploymentParams, deployedContracts); await writeDeploymentArtifacts(warpCoreConfig, context); await completeDeploy(context, 'warp', initialBalances, userAddress, chains); } async function runDeployPlanStep({ context, warpDeployConfig }) { const { skipConfirmation } = context; displayWarpDeployPlan(warpDeployConfig); if (skipConfirmation || context.isDryRun) return; const isConfirmed = await confirm({ message: 'Is this deployment plan correct?', }); if (!isConfirmed) throw new Error('Deployment cancelled'); } async function executeDeploy(params, apiKeys) { logBlue('🚀 All systems ready, captain! Beginning deployment...'); const { warpDeployConfig, context: { registry, multiProvider, isDryRun, dryRunChain }, } = params; const deployer = warpDeployConfig.isNft ? new HypERC721Deployer(multiProvider) : new HypERC20Deployer(multiProvider); // TODO: replace with EvmERC20WarpModule const config = isDryRun && dryRunChain ? { [dryRunChain]: warpDeployConfig[dryRunChain] } : warpDeployConfig; const contractVerifier = new ContractVerifier(multiProvider, apiKeys, coreBuildArtifact, ExplorerLicenseType.MIT); const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider, contractVerifier); // For each chain in WarpRouteConfig, deploy each Ism Factory, if it's not in the registry // Then return a modified config with the ism address as a string const modifiedConfig = await deployAndResolveWarpIsm(config, multiProvider, registry, ismFactoryDeployer, contractVerifier); const deployedContracts = await deployer.deploy(modifiedConfig); logGreen('✅ Warp contract deployments complete'); return deployedContracts; } async function writeDeploymentArtifacts(warpCoreConfig, context) { if (!context.isDryRun) { log('Writing deployment artifacts...'); await context.registry.addWarpRoute(warpCoreConfig); } log(indentYamlOrJson(yamlStringify(warpCoreConfig, null, 2), 4)); } async function deployAndResolveWarpIsm(warpConfig, multiProvider, registry, ismFactoryDeployer, contractVerifier) { return promiseObjAll(objMap(warpConfig, async (chain, config) => { if (!config.interchainSecurityModule || typeof config.interchainSecurityModule === 'string') { logGray(`Config Ism is ${!config.interchainSecurityModule ? 'empty' : config.interchainSecurityModule}, skipping deployment.`); return config; } logBlue(`Loading registry factory addresses for ${chain}...`); let chainAddresses = await registry.getChainAddresses(chain); if (!chainAddresses) { logGray(`Registry factory addresses not found for ${chain}. Deploying...`); chainAddresses = serializeContracts(await ismFactoryDeployer.deployContracts(chain)); } logGray(`Creating ${config.interchainSecurityModule.type} ISM for ${config.type} token on ${chain} chain...`); const deployedIsm = await createWarpIsm(chain, warpConfig, multiProvider, { domainRoutingIsmFactory: chainAddresses.domainRoutingIsmFactory, staticAggregationHookFactory: chainAddresses.staticAggregationHookFactory, staticAggregationIsmFactory: chainAddresses.staticAggregationIsmFactory, staticMerkleRootMultisigIsmFactory: chainAddresses.staticMerkleRootMultisigIsmFactory, staticMessageIdMultisigIsmFactory: chainAddresses.staticMessageIdMultisigIsmFactory, staticMerkleRootWeightedMultisigIsmFactory: chainAddresses.staticMerkleRootWeightedMultisigIsmFactory, staticMessageIdWeightedMultisigIsmFactory: chainAddresses.staticMessageIdWeightedMultisigIsmFactory, }, contractVerifier); logGreen(`Finished creating ${config.interchainSecurityModule.type} ISM for ${config.type} token on ${chain} chain.`); return { ...warpConfig[chain], interchainSecurityModule: deployedIsm }; })); } /** * Deploys the Warp ISM for a given config * * @returns The deployed ism address */ async function createWarpIsm(chain, warpConfig, multiProvider, factoryAddresses, contractVerifier) { const { domainRoutingIsmFactory, staticAggregationHookFactory, staticAggregationIsmFactory, staticMerkleRootMultisigIsmFactory, staticMessageIdMultisigIsmFactory, staticMerkleRootWeightedMultisigIsmFactory, staticMessageIdWeightedMultisigIsmFactory, } = factoryAddresses; const evmIsmModule = await EvmIsmModule.create({ chain, multiProvider, mailbox: warpConfig[chain].mailbox, proxyFactoryFactories: { domainRoutingIsmFactory, staticAggregationHookFactory, staticAggregationIsmFactory, staticMerkleRootMultisigIsmFactory, staticMessageIdMultisigIsmFactory, staticMerkleRootWeightedMultisigIsmFactory, staticMessageIdWeightedMultisigIsmFactory, }, config: warpConfig[chain].interchainSecurityModule, contractVerifier, }); const { deployedIsm } = evmIsmModule.serialize(); return deployedIsm; } async function getWarpCoreConfig({ warpDeployConfig, context }, contracts) { const warpCoreConfig = { tokens: [] }; // TODO: replace with warp read const tokenMetadata = await HypERC20Deployer.deriveTokenMetadata(context.multiProvider, warpDeployConfig); assert(tokenMetadata && isTokenMetadata(tokenMetadata), 'Missing required token metadata'); const { decimals, symbol, name } = tokenMetadata; assert(decimals, 'Missing decimals on token metadata'); generateTokenConfigs(warpCoreConfig, warpDeployConfig, contracts, symbol, name, decimals); fullyConnectTokens(warpCoreConfig); return warpCoreConfig; } /** * Creates token configs. */ function generateTokenConfigs(warpCoreConfig, warpDeployConfig, contracts, symbol, name, decimals) { for (const [chainName, contract] of Object.entries(contracts)) { const config = warpDeployConfig[chainName]; const collateralAddressOrDenom = isCollateralConfig(config) ? config.token // gets set in the above deriveTokenMetadata() : undefined; warpCoreConfig.tokens.push({ chainName, standard: TOKEN_TYPE_TO_STANDARD[config.type], decimals, symbol, name, addressOrDenom: contract[warpDeployConfig[chainName].type] .address, collateralAddressOrDenom, }); } } /** * Adds connections between tokens. * * Assumes full interconnectivity between all tokens for now b.c. that's * what the deployers do by default. */ function fullyConnectTokens(warpCoreConfig) { for (const token1 of warpCoreConfig.tokens) { for (const token2 of warpCoreConfig.tokens) { if (token1.chainName === token2.chainName && token1.addressOrDenom === token2.addressOrDenom) continue; token1.connections ||= []; token1.connections.push({ token: getTokenConnectionId(ProtocolType.Ethereum, token2.chainName, token2.addressOrDenom), }); } } } export async function runWarpRouteApply(params) { const { warpDeployConfig, warpCoreConfig, context, strategyUrl } = params; const { registry, multiProvider, chainMetadata, skipConfirmation } = context; WarpRouteDeployConfigSchema.parse(warpDeployConfig); WarpCoreConfigSchema.parse(warpCoreConfig); const addresses = await registry.getAddresses(); const warpCoreConfigByChain = Object.fromEntries(warpCoreConfig.tokens.map((token) => [ token.chainName, token, ]) /* Necessary for O(1) reads below */); const chains = Object.keys(warpDeployConfig); let apiKeys = {}; if (!skipConfirmation) apiKeys = await getOrRequestApiKeys(chains, chainMetadata); const contractVerifier = new ContractVerifier(multiProvider, apiKeys, coreBuildArtifact, ExplorerLicenseType.MIT); const warpDeployChains = Object.keys(warpDeployConfig); const warpCoreChains = Object.keys(warpCoreConfigByChain); if (warpDeployChains.length === warpCoreChains.length) { logGray('Updating deployed Warp Routes'); await promiseObjAll(objMap(warpDeployConfig, async (chain, config) => { try { config.ismFactoryAddresses = addresses[chain]; const evmERC20WarpModule = new EvmERC20WarpModule(multiProvider, { config, chain, addresses: { deployedTokenRoute: warpCoreConfigByChain[chain].addressOrDenom, }, }, contractVerifier); const transactions = await evmERC20WarpModule.update(config); if (transactions.length == 0) return logGreen(`Warp config on ${chain} is the same as target. No updates needed.`); const submitter = await getWarpApplySubmitter({ chain, context, strategyUrl, }); const transactionReceipts = await submitter.submit(...transactions); return logGreen(`✅ Warp config update successfully submitted with ${submitter.txSubmitterType} on ${chain}:\n\n`, indentYamlOrJson(yamlStringify(transactionReceipts, null, 2), 4)); } catch (e) { logRed(`Warp config on ${chain} failed to update.`, e); } })); } else if (warpDeployChains.length > warpCoreChains.length) { logGray('Extending deployed Warp configs'); // Split between the existing and additional config const existingConfigs = objFilter(warpDeployConfig, (chain, _config) => warpCoreChains.includes(chain)); let extendedConfigs = objFilter(warpDeployConfig, (chain, _config) => !warpCoreChains.includes(chain)); extendedConfigs = await deriveMetadataFromExisting(multiProvider, existingConfigs, extendedConfigs); const newDeployedContracts = await executeDeploy({ // TODO: use EvmERC20WarpModule when it's ready context, warpDeployConfig: extendedConfigs, }, apiKeys); const mergedRouters = mergeAllRouters(multiProvider, existingConfigs, newDeployedContracts, warpCoreConfigByChain); await enrollRemoteRouters(context, mergedRouters, strategyUrl); const updatedWarpCoreConfig = await getWarpCoreConfig(params, mergedRouters); WarpCoreConfigSchema.parse(updatedWarpCoreConfig); await writeDeploymentArtifacts(updatedWarpCoreConfig, context); } else { throw new Error('Unenrolling warp routes is currently not supported'); } } /** * Retrieves a chain submission strategy from the provided filepath. * @param submissionStrategyFilepath a filepath to the submission strategy file * @returns a formatted submission strategy */ export function readChainSubmissionStrategy(submissionStrategyFilepath) { const submissionStrategyFileContent = readYamlOrJson(submissionStrategyFilepath.trim()); return ChainSubmissionStrategySchema.parse(submissionStrategyFileContent); } /** * Derives token metadata from existing config and merges it with extended config. * @returns The merged Warp route deployment config with token metadata. */ async function deriveMetadataFromExisting(multiProvider, existingConfigs, extendedConfigs) { const existingTokenMetadata = await HypERC20Deployer.deriveTokenMetadata(multiProvider, existingConfigs); return objMap(extendedConfigs, (_chain, extendedConfig) => { return { ...existingTokenMetadata, ...extendedConfig, }; }); } /** * Merges existing router configs with newly deployed router contracts. */ function mergeAllRouters(multiProvider, existingConfigs, deployedContractsMap, warpCoreConfigByChain) { const existingContractAddresses = objMap(existingConfigs, (chain, config) => ({ [config.type]: warpCoreConfigByChain[chain].addressOrDenom, })); return { ...connectContractsMap(attachContractsMap(existingContractAddresses, hypERC20factories), multiProvider), ...deployedContractsMap, }; } /** * Enroll all deployed routers with each other. * * @param deployedContractsMap - A map of deployed Hyperlane contracts by chain. * @param multiProvider - A MultiProvider instance to interact with multiple chains. */ async function enrollRemoteRouters(context, deployedContractsMap, strategyUrl) { logBlue(`Enrolling deployed routers with each other (if not already)...`); const { multiProvider } = context; const deployedRouters = objMap(deployedContractsMap, (_, contracts) => getRouter(contracts).address); const allChains = Object.keys(deployedRouters); await promiseObjAll(objMap(deployedContractsMap, async (chain, contracts) => { const router = getRouter(contracts); // Assume deployedContract always has 1 value // Mutate the config.remoteRouters by setting it to all other routers to update const warpRouteReader = new EvmERC20WarpRouteReader(multiProvider, chain); const mutatedWarpRouteConfig = await warpRouteReader.deriveWarpRouteConfig(router.address); const evmERC20WarpModule = new EvmERC20WarpModule(multiProvider, { config: mutatedWarpRouteConfig, chain, addresses: { deployedTokenRoute: router.address }, }); const otherChains = multiProvider .getRemoteChains(chain) .filter((c) => allChains.includes(c)); mutatedWarpRouteConfig.remoteRouters = otherChains.reduce((remoteRouters, chain) => { remoteRouters[multiProvider.getDomainId(chain)] = deployedRouters[chain]; return remoteRouters; }, {}); const mutatedConfigTxs = await evmERC20WarpModule.update(mutatedWarpRouteConfig); if (mutatedConfigTxs.length == 0) return logGreen(`Mutated warp config on ${chain} is the same as target. No updates needed.`); const submitter = await getWarpApplySubmitter({ chain, context, strategyUrl, }); const transactionReceipts = await submitter.submit(...mutatedConfigTxs); return logGreen(`✅ Router enrollment update successfully submitted with ${submitter.txSubmitterType} on ${chain}:\n\n`, indentYamlOrJson(yamlStringify(transactionReceipts, null, 2), 4)); })); } function getRouter(contracts) { for (const key of objKeys(hypERC20factories)) { if (contracts[key]) return contracts[key]; } throw new Error('No matching contract found.'); } function displayWarpDeployPlan(deployConfig) { logBlue('\nWarp Route Deployment Plan'); logGray('=========================='); log(`📋 Token Standard: ${deployConfig.isNft ? 'ERC721' : 'ERC20'}`); const { transformedDeployConfig, transformedIsmConfigs } = transformDeployConfigForDisplay(deployConfig); log('📋 Warp Route Config:'); logTable(transformedDeployConfig); objMap(transformedIsmConfigs, (chain, ismConfigs) => { log(`📋 ${chain} ISM Config(s):`); ismConfigs.forEach((ismConfig) => { logTable(ismConfig); }); }); } function transformDeployConfigForDisplay(deployConfig) { const transformedIsmConfigs = {}; const transformedDeployConfig = objMap(deployConfig, (chain, config) => { if (config.interchainSecurityModule) transformedIsmConfigs[chain] = transformIsmConfigForDisplay(config.interchainSecurityModule); return { 'NFT?': config.isNft ?? false, Type: config.type, Owner: config.owner, Mailbox: config.mailbox, 'ISM Config(s)': config.interchainSecurityModule ? 'See table(s) below.' : 'No ISM config(s) specified.', }; }); return { transformedDeployConfig, transformedIsmConfigs, }; } function transformIsmConfigForDisplay(ismConfig) { const ismConfigs = []; switch (ismConfig.type) { case IsmType.AGGREGATION: ismConfigs.push({ Type: ismConfig.type, Threshold: ismConfig.threshold, Modules: 'See table(s) below.', }); ismConfig.modules.forEach((module) => { ismConfigs.push(...transformIsmConfigForDisplay(module)); }); return ismConfigs; case IsmType.ROUTING: return [ { Type: ismConfig.type, Owner: ismConfig.owner, 'Owner Overrides': ismConfig.ownerOverrides ?? 'Undefined', Domains: 'See warp config for domain specification.', }, ]; case IsmType.FALLBACK_ROUTING: return [ { Type: ismConfig.type, Owner: ismConfig.owner, 'Owner Overrides': ismConfig.ownerOverrides ?? 'Undefined', Domains: 'See warp config for domain specification.', }, ]; case IsmType.MERKLE_ROOT_MULTISIG: return [ { Type: ismConfig.type, Validators: ismConfig.validators, Threshold: ismConfig.threshold, }, ]; case IsmType.MESSAGE_ID_MULTISIG: return [ { Type: ismConfig.type, Validators: ismConfig.validators, Threshold: ismConfig.threshold, }, ]; case IsmType.OP_STACK: return [ { Type: ismConfig.type, Origin: ismConfig.origin, 'Native Bridge': ismConfig.nativeBridge, }, ]; case IsmType.PAUSABLE: return [ { Type: ismConfig.type, Owner: ismConfig.owner, 'Paused ?': ismConfig.paused, 'Owner Overrides': ismConfig.ownerOverrides ?? 'Undefined', }, ]; case IsmType.TRUSTED_RELAYER: return [ { Type: ismConfig.type, Relayer: ismConfig.relayer, }, ]; default: return [ismConfig]; } } /** * Helper function to get warp apply specific submitter. * * @returns the warp apply submitter */ async function getWarpApplySubmitter({ chain, context, strategyUrl, }) { const { chainMetadata, multiProvider } = context; const submissionStrategy = strategyUrl ? readChainSubmissionStrategy(strategyUrl)[chain] : { submitter: { type: TxSubmitterType.JSON_RPC, }, }; const protocol = chainMetadata[chain].protocol; return getSubmitterBuilder({ submissionStrategy, multiProvider, }); } //# sourceMappingURL=warp.js.map