From a4d5d692f3e8230cfbf2b87ac5e2775fe3da8bb9 Mon Sep 17 00:00:00 2001 From: Lee <6251863+ltyu@users.noreply.github.com> Date: Sat, 12 Oct 2024 09:14:37 -0400 Subject: [PATCH] fix(cli): Update warp apply to apply changes in single command (#4672) ### Description This PR fixes a limitation in `warp apply` such that it can only extend _or_ update an existing warp route. This means that for configs with both changes require `warp apply` to be called multiple times. An example is when Renzo deploys to new chain, and it needs to update the existing ISMs. ### Related issues - Fixes #4671 ### Backward compatibility Yes ### Testing Manual/Unit Tests --- .changeset/tender-spiders-deny.md | 5 + typescript/cli/src/deploy/warp.ts | 256 ++++++++++-------- typescript/cli/src/tests/commands/helpers.ts | 2 +- .../cli/src/tests/warp-apply.e2e-test.ts | 67 ++++- 4 files changed, 211 insertions(+), 119 deletions(-) create mode 100644 .changeset/tender-spiders-deny.md diff --git a/.changeset/tender-spiders-deny.md b/.changeset/tender-spiders-deny.md new file mode 100644 index 0000000000..99a6cede43 --- /dev/null +++ b/.changeset/tender-spiders-deny.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': minor +--- + +Update `warp apply` such that it updates in place AND extends in a single call diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 1fc13c1ec3..872edb4004 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -1,4 +1,5 @@ import { confirm } from '@inquirer/prompts'; +import { groupBy } from 'lodash-es'; import { stringify as yamlStringify } from 'yaml'; import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; @@ -53,6 +54,7 @@ import { Address, ProtocolType, assert, + isObjEmpty, objFilter, objKeys, objMap, @@ -64,14 +66,7 @@ import { readWarpRouteDeployConfig } from '../config/warp.js'; import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js'; import { getOrRequestApiKeys } from '../context/context.js'; import { WriteCommandContext } from '../context/types.js'; -import { - log, - logBlue, - logGray, - logGreen, - logRed, - logTable, -} from '../logger.js'; +import { log, logBlue, logGray, logGreen, logTable } from '../logger.js'; import { getSubmitterBuilder } from '../submit/submit.js'; import { indentYamlOrJson, @@ -438,17 +433,13 @@ export async function runWarpRouteApply( params: WarpApplyParams, ): Promise { const { warpDeployConfig, warpCoreConfig, context } = params; - const { registry, multiProvider, chainMetadata, skipConfirmation } = context; + const { 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 */, + warpCoreConfig.tokens.map((token) => [token.chainName, token]), ); const chains = Object.keys(warpDeployConfig); @@ -457,94 +448,119 @@ export async function runWarpRouteApply( if (!skipConfirmation) apiKeys = await getOrRequestApiKeys(chains, chainMetadata); - const contractVerifier = new ContractVerifier( - multiProvider, - apiKeys, - coreBuildArtifact, - ExplorerLicenseType.MIT, - ); + const transactions: AnnotatedEV5Transaction[] = [ + ...(await extendWarpRoute( + params, + apiKeys, + warpDeployConfig, + warpCoreConfigByChain, + )), + ...(await updateExistingWarpRoute( + params, + apiKeys, + warpDeployConfig, + warpCoreConfigByChain, + )), + ]; + if (transactions.length == 0) + return logGreen(`Warp config is the same as target. No updates needed.`); - const warpDeployChains = Object.keys(warpDeployConfig); + await submitWarpApplyTransactions(params, groupBy(transactions, 'chainId')); +} + +async function extendWarpRoute( + params: WarpApplyParams, + apiKeys: ChainMap, + warpDeployConfig: WarpRouteDeployConfig, + warpCoreConfigByChain: ChainMap, +) { + logBlue('Extending Warp Route'); + const { multiProvider } = params.context; 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 - ] as ProxyFactoryFactoriesAddresses; - 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.`, - ); - await submitWarpApplyTransactions(chain, params, transactions); - } 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: WarpRouteDeployConfig = objFilter( - warpDeployConfig, - (chain, _config): _config is any => warpCoreChains.includes(chain), - ); + // Split between the existing and additional config + const existingConfigs: WarpRouteDeployConfig = objFilter( + warpDeployConfig, + (chain, _config): _config is any => warpCoreChains.includes(chain), + ); - let extendedConfigs: WarpRouteDeployConfig = objFilter( - warpDeployConfig, - (chain, _config): _config is any => !warpCoreChains.includes(chain), - ); + let extendedConfigs: WarpRouteDeployConfig = objFilter( + warpDeployConfig, + (chain, _config): _config is any => !warpCoreChains.includes(chain), + ); - extendedConfigs = await deriveMetadataFromExisting( - multiProvider, - existingConfigs, - extendedConfigs, - ); + if (isObjEmpty(extendedConfigs)) return []; - const newDeployedContracts = await executeDeploy( - { - // TODO: use EvmERC20WarpModule when it's ready - context, - warpDeployConfig: extendedConfigs, - }, - apiKeys, - ); + extendedConfigs = await deriveMetadataFromExisting( + multiProvider, + existingConfigs, + extendedConfigs, + ); - const mergedRouters = mergeAllRouters( - multiProvider, - existingConfigs, - newDeployedContracts, - warpCoreConfigByChain, - ); + const newDeployedContracts = await executeDeploy( + { + // TODO: use EvmERC20WarpModule when it's ready + context: params.context, + warpDeployConfig: extendedConfigs, + }, + apiKeys, + ); - await enrollRemoteRouters(params, mergedRouters); + const mergedRouters = mergeAllRouters( + multiProvider, + existingConfigs, + newDeployedContracts, + warpCoreConfigByChain, + ); - const updatedWarpCoreConfig = await getWarpCoreConfig( - params, - mergedRouters, - ); - WarpCoreConfigSchema.parse(updatedWarpCoreConfig); - await writeDeploymentArtifacts(updatedWarpCoreConfig, context); - } else { - throw new Error('Unenrolling warp routes is currently not supported'); - } + const updatedWarpCoreConfig = await getWarpCoreConfig(params, mergedRouters); + WarpCoreConfigSchema.parse(updatedWarpCoreConfig); + await writeDeploymentArtifacts(updatedWarpCoreConfig, params.context); + + return enrollRemoteRouters(params, mergedRouters); +} + +async function updateExistingWarpRoute( + params: WarpApplyParams, + apiKeys: ChainMap, + warpDeployConfig: WarpRouteDeployConfig, + warpCoreConfigByChain: ChainMap, +) { + logBlue('Updating deployed Warp Routes'); + const { multiProvider, registry } = params.context; + const addresses = await registry.getAddresses(); + const contractVerifier = new ContractVerifier( + multiProvider, + apiKeys, + coreBuildArtifact, + ExplorerLicenseType.MIT, + ); + const transactions: AnnotatedEV5Transaction[] = []; + await promiseObjAll( + objMap(warpDeployConfig, async (chain, config) => { + const deployedConfig = warpCoreConfigByChain[chain]; + if (!deployedConfig) + return logGray( + `Missing artifacts for ${chain}. Probably new deployment. Skipping update...`, + ); + config.ismFactoryAddresses = addresses[ + chain + ] as ProxyFactoryFactoriesAddresses; + const evmERC20WarpModule = new EvmERC20WarpModule( + multiProvider, + { + config, + chain, + addresses: { + deployedTokenRoute: deployedConfig.addressOrDenom!, + }, + }, + contractVerifier, + ); + transactions.push(...(await evmERC20WarpModule.update(config))); + }), + ); + return transactions; } /** @@ -617,7 +633,7 @@ function mergeAllRouters( async function enrollRemoteRouters( params: WarpApplyParams, deployedContractsMap: HyperlaneContractsMap, -): Promise { +): Promise { logBlue(`Enrolling deployed routers with each other...`); const { multiProvider } = params.context; const deployedRouters: ChainMap
= objMap( @@ -625,6 +641,7 @@ async function enrollRemoteRouters( (_, contracts) => getRouter(contracts).address, ); const allChains = Object.keys(deployedRouters); + const transactions: AnnotatedEV5Transaction[] = []; await promiseObjAll( objMap(deployedContractsMap, async (chain, contracts) => { await retryAsync(async () => { @@ -660,10 +677,12 @@ async function enrollRemoteRouters( return logGreen( `Warp config on ${chain} is the same as target. No updates needed.`, ); - await submitWarpApplyTransactions(chain, params, mutatedConfigTxs); + transactions.push(...mutatedConfigTxs); }); }), ); + + return transactions; } function getRouter(contracts: HyperlaneContracts) { @@ -805,29 +824,36 @@ function transformIsmConfigForDisplay(ismConfig: IsmConfig): any[] { * Submits a set of transactions to the specified chain and outputs transaction receipts */ async function submitWarpApplyTransactions( - chain: string, params: WarpApplyParams, - transactions: AnnotatedEV5Transaction[], -) { - const submitter: TxSubmitterBuilder = - await getWarpApplySubmitter({ - chain, - context: params.context, - strategyUrl: params.strategyUrl, - }); + chainTransactions: Record, +): Promise { + const { multiProvider } = params.context; + await promiseObjAll( + objMap(chainTransactions, async (chainId, transactions) => { + const chain = multiProvider.getChainName(chainId); + const submitter: TxSubmitterBuilder = + await getWarpApplySubmitter({ + chain, + context: params.context, + strategyUrl: params.strategyUrl, + }); - const transactionReceipts = await submitter.submit(...transactions); - if (transactionReceipts) { - const receiptPath = `${params.receiptsDir}/${chain}-${ - submitter.txSubmitterType - }-${Date.now()}-receipts.json`; - writeYamlOrJson(receiptPath, transactionReceipts); - logGreen(`Transactions receipts successfully written to ${receiptPath}`); - } + const transactionReceipts = await submitter.submit(...transactions); + if (transactionReceipts) { + const receiptPath = `${params.receiptsDir}/${chain}-${ + submitter.txSubmitterType + }-${Date.now()}-receipts.json`; + writeYamlOrJson(receiptPath, transactionReceipts); + logGreen( + `Transactions receipts successfully written to ${receiptPath}`, + ); + } - return logGreen( - `✅ Warp route update success with ${submitter.txSubmitterType} on ${chain}:\n\n`, - indentYamlOrJson(yamlStringify(transactionReceipts, null, 2), 0), + logGreen( + `✅ Warp route update success with ${submitter.txSubmitterType} on ${chain}:\n\n`, + indentYamlOrJson(yamlStringify(transactionReceipts, null, 2), 0), + ); + }), ); } diff --git a/typescript/cli/src/tests/commands/helpers.ts b/typescript/cli/src/tests/commands/helpers.ts index e4f43e894d..2d2be7a6e9 100644 --- a/typescript/cli/src/tests/commands/helpers.ts +++ b/typescript/cli/src/tests/commands/helpers.ts @@ -43,7 +43,7 @@ export async function updateWarpOwnerConfig( warpDeployPath, ); warpDeployConfig[chain].owner = owner; - writeYamlOrJson(warpDeployPath, warpDeployConfig); + await writeYamlOrJson(warpDeployPath, warpDeployConfig); return warpDeployPath; } diff --git a/typescript/cli/src/tests/warp-apply.e2e-test.ts b/typescript/cli/src/tests/warp-apply.e2e-test.ts index 7917268615..7b0c20d91a 100644 --- a/typescript/cli/src/tests/warp-apply.e2e-test.ts +++ b/typescript/cli/src/tests/warp-apply.e2e-test.ts @@ -18,7 +18,11 @@ import { getChainId, updateOwner, } from './commands/helpers.js'; -import { hyperlaneWarpDeploy, readWarpConfig } from './commands/warp.js'; +import { + hyperlaneWarpApply, + hyperlaneWarpDeploy, + readWarpConfig, +} from './commands/warp.js'; const CHAIN_NAME_2 = 'anvil2'; const CHAIN_NAME_3 = 'anvil3'; @@ -86,9 +90,8 @@ describe('WarpApply e2e tests', async function () { warpConfigPath, WARP_CORE_CONFIG_PATH_2, ); - expect(stdout).to.include( - 'Warp config on anvil2 is the same as target. No updates needed.', + 'Warp config is the same as target. No updates needed.', ); }); @@ -198,4 +201,62 @@ describe('WarpApply e2e tests', async function () { ); expect(remoteRouterKeys2).to.include(chain1Id); }); + + it('should extend an existing warp route and update the owner', async () => { + const warpDeployPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`; + // Burn anvil2 owner in config + const warpDeployConfig = await readWarpConfig( + CHAIN_NAME_2, + WARP_CORE_CONFIG_PATH_2, + warpDeployPath, + ); + warpDeployConfig[CHAIN_NAME_2].owner = BURN_ADDRESS; + + // Extend with new config + const randomOwner = new Wallet(ANVIL_KEY).address; + const extendedConfig: TokenRouterConfig = { + decimals: 18, + mailbox: chain2Addresses!.mailbox, + name: 'Ether', + owner: randomOwner, + symbol: 'ETH', + totalSupply: 0, + type: TokenType.native, + }; + + warpDeployConfig[CHAIN_NAME_3] = extendedConfig; + writeYamlOrJson(warpDeployPath, warpDeployConfig); + await hyperlaneWarpApply(warpDeployPath, WARP_CORE_CONFIG_PATH_2); + + const COMBINED_WARP_CORE_CONFIG_PATH = `${REGISTRY_PATH}/deployments/warp_routes/ETH/anvil2-anvil3-config.yaml`; + + const updatedWarpDeployConfig_2 = await readWarpConfig( + CHAIN_NAME_2, + COMBINED_WARP_CORE_CONFIG_PATH, + warpDeployPath, + ); + const updatedWarpDeployConfig_3 = await readWarpConfig( + CHAIN_NAME_3, + COMBINED_WARP_CORE_CONFIG_PATH, + warpDeployPath, + ); + // Check that anvil2 owner is burned + expect(updatedWarpDeployConfig_2.anvil2.owner).to.equal(BURN_ADDRESS); + + // Also, anvil3 owner is not burned + expect(updatedWarpDeployConfig_3.anvil3.owner).to.equal(randomOwner); + + // Check that both chains enrolled + const chain2Id = await getChainId(CHAIN_NAME_2, ANVIL_KEY); + const chain3Id = await getChainId(CHAIN_NAME_3, ANVIL_KEY); + + const remoteRouterKeys2 = Object.keys( + updatedWarpDeployConfig_2[CHAIN_NAME_2].remoteRouters!, + ); + const remoteRouterKeys3 = Object.keys( + updatedWarpDeployConfig_3[CHAIN_NAME_3].remoteRouters!, + ); + expect(remoteRouterKeys2).to.include(chain3Id); + expect(remoteRouterKeys3).to.include(chain2Id); + }); });