Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: cli core checker #4687

Open
wants to merge 5 commits into
base: xeno/cli-warp-route-checker
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/flat-swans-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@hyperlane-xyz/utils': minor
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---

Adds new `core check` command to compare local configuration and on chain deployments. Adds memoization to the EvmHookReader to avoid repeating configuration derivation
105 changes: 71 additions & 34 deletions typescript/cli/src/commands/core.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { stringify as yamlStringify } from 'yaml';
import { CommandModule } from 'yargs';

import {
CoreConfig,
DeployedCoreAddresses,
DeployedCoreAddressesSchema,
EvmCoreReader,
normalizeConfig,
} from '@hyperlane-xyz/sdk';
import { diffObjMerge } from '@hyperlane-xyz/utils';

import {
createCoreDeployConfig,
Expand All @@ -16,17 +19,21 @@ import {
} from '../context/types.js';
import { runCoreApply, runCoreDeploy } from '../deploy/core.js';
import { evaluateIfDryRunFailure } from '../deploy/dry-run.js';
import { errorRed, log, logGray, logGreen } from '../logger.js';
import { log, logCommandHeader, logGreen } from '../logger.js';
import { executeCoreRead } from '../read/core.js';
import {
logYamlIfUnderMaxLines,
readYamlOrJson,
writeYamlOrJson,
} from '../utils/files.js';
import { formatYamlViolationsOutput } from '../utils/output.js';

import {
DEFAULT_CORE_DEPLOYMENT_CONFIG_PATH,
chainCommandOption,
dryRunCommandOption,
fromAddressCommandOption,
inputFileCommandOption,
outputFileCommandOption,
skipConfirmationOption,
} from './options.js';
Expand All @@ -40,13 +47,15 @@ export const coreCommand: CommandModule = {
builder: (yargs) =>
yargs
.command(apply)
.command(check)
.command(deploy)
.command(init)
.command(read)
.version(false)
.demandCommand(),
handler: () => log('Command required'),
};

export const apply: CommandModuleWithWriteContext<{
chain: string;
config: string;
Expand All @@ -60,14 +69,13 @@ export const apply: CommandModuleWithWriteContext<{
demandOption: true,
},
config: outputFileCommandOption(
'./configs/core-config.yaml',
DEFAULT_CORE_DEPLOYMENT_CONFIG_PATH,
true,
'The path to output a Core Config JSON or YAML file.',
),
},
handler: async ({ context, chain, config: configFilePath }) => {
logGray(`Hyperlane Core Apply`);
logGray('--------------------');
logCommandHeader(`Hyperlane Core Apply`);

const addresses = (await context.registry.getChainAddresses(
chain,
Expand Down Expand Up @@ -103,7 +111,7 @@ export const deploy: CommandModuleWithWriteContext<{
builder: {
chain: chainCommandOption,
config: outputFileCommandOption(
'./configs/core-config.yaml',
DEFAULT_CORE_DEPLOYMENT_CONFIG_PATH,
false,
'The path to a JSON or YAML file with a core deployment config.',
),
Expand All @@ -112,8 +120,7 @@ export const deploy: CommandModuleWithWriteContext<{
'skip-confirmation': skipConfirmationOption,
},
handler: async ({ context, chain, config: configFilePath, dryRun }) => {
logGray(`Hyperlane Core deployment${dryRun ? ' dry-run' : ''}`);
logGray(`------------------------------------------------`);
logCommandHeader(`Hyperlane Core deployment${dryRun ? ' dry-run' : ''}`);

try {
await runCoreDeploy({
Expand Down Expand Up @@ -142,14 +149,13 @@ export const init: CommandModuleWithContext<{
default: false,
},
config: outputFileCommandOption(
'./configs/core-config.yaml',
DEFAULT_CORE_DEPLOYMENT_CONFIG_PATH,
false,
'The path to output a Core Config JSON or YAML file.',
),
},
handler: async ({ context, advanced, config: configFilePath }) => {
logGray('Hyperlane Core Configure');
logGray('------------------------');
logCommandHeader('Hyperlane Core Configure');

await createCoreDeployConfig({
context,
Expand Down Expand Up @@ -178,39 +184,70 @@ export const read: CommandModuleWithContext<{
description: 'Mailbox address used to derive the core config',
},
config: outputFileCommandOption(
'./configs/core-config.yaml',
DEFAULT_CORE_DEPLOYMENT_CONFIG_PATH,
false,
'The path to output a Core Config JSON or YAML file.',
),
},
handler: async ({ context, chain, mailbox, config: configFilePath }) => {
if (!mailbox) {
const addresses = await context.registry.getChainAddresses(chain);
mailbox = addresses?.mailbox;
if (!mailbox) {
throw new Error(
`${chain} mailbox not provided and none found in registry.`,
);
}
}
logCommandHeader('Hyperlane Core Read');

logGray('Hyperlane Core Read');
logGray('-------------------');
const coreConfig = await executeCoreRead({ context, chain, mailbox });

const evmCoreReader = new EvmCoreReader(context.multiProvider, chain);
try {
const coreConfig = await evmCoreReader.deriveCoreConfig(mailbox);
writeYamlOrJson(configFilePath, coreConfig, 'yaml');
logGreen(`✅ Core config written successfully to ${configFilePath}:\n`);
logYamlIfUnderMaxLines(coreConfig);
} catch (e: any) {
errorRed(
`❌ Failed to read core config for mailbox ${mailbox} on ${chain}:`,
e,
);
writeYamlOrJson(configFilePath, coreConfig, 'yaml');
logGreen(`✅ Core config written successfully to ${configFilePath}:\n`);
logYamlIfUnderMaxLines(coreConfig);

process.exit(0);
},
};

export const check: CommandModuleWithContext<{
chain: string;
config: string;
mailbox?: string;
}> = {
command: 'check',
describe:
'Reads onchain Core configuration for a given mailbox address and compares it with a provided file',
builder: {
chain: {
...chainCommandOption,
demandOption: true,
},
mailbox: {
type: 'string',
description:
'Mailbox address used to derive the core config. If not provided it will be inferred from the registry',
},
config: inputFileCommandOption({
defaultPath: DEFAULT_CORE_DEPLOYMENT_CONFIG_PATH,
description: 'The path to a a Core Config JSON or YAML file.',
demandOption: false,
}),
},
handler: async ({ context, chain, mailbox, config: configFilePath }) => {
logCommandHeader('Hyperlane Core Check');

const expectedCoreConfig: CoreConfig = await readYamlOrJson(configFilePath);
const onChainCoreConfig = await executeCoreRead({
context,
chain,
mailbox,
});

const { mergedObject, isInvalid } = diffObjMerge(
normalizeConfig(onChainCoreConfig),
normalizeConfig(expectedCoreConfig),
);

if (isInvalid) {
log(formatYamlViolationsOutput(yamlStringify(mergedObject, null, 2)));
process.exit(1);
}

logGreen(`No violations found`);

process.exit(0);
},
};
2 changes: 2 additions & 0 deletions typescript/cli/src/commands/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ export const hookCommandOption: Options = {
export const DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH =
'./configs/warp-route-deployment.yaml';

export const DEFAULT_CORE_DEPLOYMENT_CONFIG_PATH = './configs/core-config.yaml';

export const warpDeploymentConfigCommandOption: Options = {
type: 'string',
description:
Expand Down
35 changes: 35 additions & 0 deletions typescript/cli/src/read/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ChainName, CoreConfig, EvmCoreReader } from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';

import { CommandContext } from '../context/types.js';
import { errorRed } from '../logger.js';

export async function executeCoreRead({
context,
chain,
mailbox,
}: {
context: CommandContext;
chain: ChainName;
mailbox?: Address;
}): Promise<CoreConfig> {
if (!mailbox) {
const addresses = await context.registry.getChainAddresses(chain);
mailbox = addresses?.mailbox;
if (!mailbox) {
errorRed(`${chain} mailbox not provided and none found in registry.`);
process.exit(1);
}
}

const evmCoreReader = new EvmCoreReader(context.multiProvider, chain);
try {
return evmCoreReader.deriveCoreConfig(mailbox);
} catch (e: any) {
errorRed(
`❌ Failed to read core config for mailbox ${mailbox} on ${chain}:`,
e,
);
process.exit(1);
}
}
Loading