cosmic-interchain-cli
Version:
A command-line utility for Cosmic Wire's interchain messaging protocol
465 lines • 22 kB
JavaScript
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