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
pull/4676/head
Lee 1 month ago committed by GitHub
parent 46044a2e98
commit a4d5d692f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/tender-spiders-deny.md
  2. 256
      typescript/cli/src/deploy/warp.ts
  3. 2
      typescript/cli/src/tests/commands/helpers.ts
  4. 67
      typescript/cli/src/tests/warp-apply.e2e-test.ts

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/cli': minor
---
Update `warp apply` such that it updates in place AND extends in a single call

@ -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<void> {
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<string>,
warpDeployConfig: WarpRouteDeployConfig,
warpCoreConfigByChain: ChainMap<WarpCoreConfig['tokens'][number]>,
) {
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<string>,
warpDeployConfig: WarpRouteDeployConfig,
warpCoreConfigByChain: ChainMap<WarpCoreConfig['tokens'][number]>,
) {
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<HypERC20Factories>,
): Promise<void> {
): Promise<AnnotatedEV5Transaction[]> {
logBlue(`Enrolling deployed routers with each other...`);
const { multiProvider } = params.context;
const deployedRouters: ChainMap<Address> = 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<HypERC20Factories>) {
@ -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<ProtocolType> =
await getWarpApplySubmitter({
chain,
context: params.context,
strategyUrl: params.strategyUrl,
});
chainTransactions: Record<string, AnnotatedEV5Transaction[]>,
): Promise<void> {
const { multiProvider } = params.context;
await promiseObjAll(
objMap(chainTransactions, async (chainId, transactions) => {
const chain = multiProvider.getChainName(chainId);
const submitter: TxSubmitterBuilder<ProtocolType> =
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),
);
}),
);
}

@ -43,7 +43,7 @@ export async function updateWarpOwnerConfig(
warpDeployPath,
);
warpDeployConfig[chain].owner = owner;
writeYamlOrJson(warpDeployPath, warpDeployConfig);
await writeYamlOrJson(warpDeployPath, warpDeployConfig);
return warpDeployPath;
}

@ -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);
});
});

Loading…
Cancel
Save