Skip to content

Commit

Permalink
fix(cli): Update warp apply to apply changes in single command (#4672)
Browse files Browse the repository at this point in the history
### 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
  • Loading branch information
ltyu authored Oct 12, 2024
1 parent 46044a2 commit a4d5d69
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 119 deletions.
5 changes: 5 additions & 0 deletions .changeset/tender-spiders-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/cli': minor
---

Update `warp apply` such that it updates in place AND extends in a single call
256 changes: 141 additions & 115 deletions typescript/cli/src/deploy/warp.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -53,6 +54,7 @@ import {
Address,
ProtocolType,
assert,
isObjEmpty,
objFilter,
objKeys,
objMap,
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -617,14 +633,15 @@ 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(
deployedContractsMap,
(_, contracts) => getRouter(contracts).address,
);
const allChains = Object.keys(deployedRouters);
const transactions: AnnotatedEV5Transaction[] = [];
await promiseObjAll(
objMap(deployedContractsMap, async (chain, contracts) => {
await retryAsync(async () => {
Expand Down Expand Up @@ -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>) {
Expand Down Expand Up @@ -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),
);
}),
);
}

Expand Down
2 changes: 1 addition & 1 deletion typescript/cli/src/tests/commands/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function updateWarpOwnerConfig(
warpDeployPath,
);
warpDeployConfig[chain].owner = owner;
writeYamlOrJson(warpDeployPath, warpDeployConfig);
await writeYamlOrJson(warpDeployPath, warpDeployConfig);

return warpDeployPath;
}
Expand Down
Loading

0 comments on commit a4d5d69

Please sign in to comment.