feat: add `hyperlane hook read` + `hyperlane ism read` to cli (#3648)

pull/3673/head
Paul Balaji 6 months ago committed by GitHub
parent d6f25ed035
commit af26342079
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/hip-toys-warn.md
  2. 5
      .changeset/odd-books-think.md
  3. 6
      .changeset/thirty-games-shake.md
  4. 4
      typescript/cli/cli.ts
  5. 53
      typescript/cli/src/commands/hook.ts
  6. 58
      typescript/cli/src/commands/ism.ts
  7. 16
      typescript/cli/src/commands/options.ts
  8. 51
      typescript/cli/src/hook/read.ts
  9. 51
      typescript/cli/src/ism/read.ts
  10. 45
      typescript/cli/src/utils/files.ts
  11. 70
      typescript/infra/scripts/read-config.ts
  12. 5
      typescript/sdk/src/hook/read.ts
  13. 5
      typescript/sdk/src/ism/read.ts
  14. 3
      typescript/utils/package.json
  15. 1
      typescript/utils/src/index.ts
  16. 18
      typescript/utils/src/objects.ts
  17. 1
      yarn.lock

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/infra': minor
---
Moved Hook/ISM reading into CLI.

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/cli': minor
---
Introduces `hyperlane hook read` and `hyperlane ism read` commands for deriving onchain Hook/ISM configs from an address on a given chain.

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/utils': minor
'@hyperlane-xyz/sdk': minor
---
Moved Hook/ISM config stringify into a general object stringify utility.

@ -8,6 +8,8 @@ import './env.js';
import { chainsCommand } from './src/commands/chains.js';
import { configCommand } from './src/commands/config.js';
import { deployCommand } from './src/commands/deploy.js';
import { hookCommand } from './src/commands/hook.js';
import { ismCommand } from './src/commands/ism.js';
import {
logFormatCommandOption,
logLevelCommandOption,
@ -37,6 +39,8 @@ try {
.command(chainsCommand)
.command(configCommand)
.command(deployCommand)
.command(hookCommand)
.command(ismCommand)
.command(sendCommand)
.command(statusCommand)
.version(VERSION)

@ -0,0 +1,53 @@
import { CommandModule } from 'yargs';
import { readHookConfig } from '../hook/read.js';
import { log } from '../logger.js';
import {
addressCommandOption,
chainCommandOption,
chainsCommandOption,
fileFormatOption,
outputFileOption,
} from './options.js';
/**
* Parent command
*/
export const hookCommand: CommandModule = {
command: 'hook',
describe: 'Operations relating to Hooks',
builder: (yargs) => yargs.command(read).version(false).demandCommand(),
handler: () => log('Command required'),
};
// Examples for testing:
// Fallback routing hook on polygon (may take 5s):
// hyperlane hook read --chain polygon --address 0xca4cCe24E7e06241846F5EA0cda9947F0507C40C
// IGP hook on inevm (may take 5s):
// hyperlane hook read --chain inevm --address 0x19dc38aeae620380430C200a6E990D5Af5480117
export const read: CommandModule = {
command: 'read',
describe: 'Reads onchain Hook configuration for a given address',
builder: (yargs) =>
yargs.options({
chains: chainsCommandOption,
chain: {
...chainCommandOption,
demandOption: true,
},
address: addressCommandOption('Address of the Hook to read.', true),
format: fileFormatOption,
output: outputFileOption(),
}),
handler: async (argv: any) => {
await readHookConfig({
chain: argv.chain,
address: argv.address,
chainConfigPath: argv.chains,
format: argv.format,
output: argv.output,
});
process.exit(0);
},
};

@ -0,0 +1,58 @@
import { CommandModule } from 'yargs';
import { readIsmConfig } from '../ism/read.js';
import { log } from '../logger.js';
import {
addressCommandOption,
chainCommandOption,
chainsCommandOption,
fileFormatOption,
outputFileOption,
} from './options.js';
/**
* Parent command
*/
export const ismCommand: CommandModule = {
command: 'ism',
describe: 'Operations relating to ISMs',
builder: (yargs) => yargs.command(read).version(false).demandCommand(),
handler: () => log('Command required'),
};
// Examples for testing:
// Top-level aggregation ISM on celo (may take 10s)
// hyperlane ism read --chain celo --address 0x99e8E56Dce3402D6E09A82718937fc1cA2A9491E
// Aggregation ISM for bsc domain on inevm (may take 5s)
// hyperlane ism read --chain inevm --address 0x79A7c7Fe443971CBc6baD623Fdf8019C379a7178
// Test ISM on alfajores testnet
// hyperlane ism read --chain alfajores --address 0xdB52E4853b6A40D2972E6797E0BDBDb3eB761966
export const read: CommandModule = {
command: 'read',
describe: 'Reads onchain ISM configuration for a given address',
builder: (yargs) =>
yargs.options({
chains: chainsCommandOption,
chain: {
...chainCommandOption,
demandOption: true,
},
address: addressCommandOption(
'Address of the Interchain Security Module to read.',
true,
),
format: fileFormatOption,
output: outputFileOption(),
}),
handler: async (argv: any) => {
await readIsmConfig({
chain: argv.chain,
address: argv.address,
chainConfigPath: argv.chains,
format: argv.format,
output: argv.output,
});
process.exit(0);
},
};

@ -126,7 +126,7 @@ export const fileFormatOption: Options = {
alias: 'f',
};
export const outputFileOption = (defaultPath: string): Options => ({
export const outputFileOption = (defaultPath?: string): Options => ({
type: 'string',
description: 'Output file path',
default: defaultPath,
@ -146,3 +146,17 @@ export const dryRunOption: Options = {
'Chain name to fork and simulate deployment. Please ensure an anvil node instance is running during execution via `anvil`.',
alias: ['d', 'dr'],
};
export const chainCommandOption: Options = {
type: 'string',
description: 'The specific chain to perform operations with.',
};
export const addressCommandOption = (
description: string,
demandOption = false,
): Options => ({
type: 'string',
description,
demandOption,
});

@ -0,0 +1,51 @@
import { ChainName, EvmHookReader } from '@hyperlane-xyz/sdk';
import { Address, ProtocolType, stringifyObject } from '@hyperlane-xyz/utils';
import { readChainConfigsIfExists } from '../config/chain.js';
import { getMultiProvider } from '../context.js';
import { log, logBlue, logRed } from '../logger.js';
import {
FileFormat,
resolveFileFormat,
writeFileAtPath,
} from '../utils/files.js';
/**
* Read Hook config for a specified chain and address, logging or writing result to file.
*/
export async function readHookConfig({
chain,
address,
chainConfigPath,
format,
output,
}: {
chain: ChainName;
address: Address;
chainConfigPath: string;
format: FileFormat;
output?: string;
}): Promise<void> {
const customChains = readChainConfigsIfExists(chainConfigPath);
const multiProvider = getMultiProvider(customChains);
if (multiProvider.getProtocol(chain) === ProtocolType.Ethereum) {
const hookReader = new EvmHookReader(multiProvider, chain);
const config = await hookReader.deriveHookConfig(address);
const stringConfig = stringifyObject(
config,
resolveFileFormat(output, format),
2,
);
if (!output) {
logBlue(`Hook Config at ${address} on ${chain}:`);
log(stringConfig);
} else {
writeFileAtPath(output, stringConfig + '\n');
logBlue(`Hook Config written to ${output}.`);
}
return;
}
logRed('Unsupported chain. Currently this command supports EVM chains only.');
}

@ -0,0 +1,51 @@
import { ChainName, EvmIsmReader } from '@hyperlane-xyz/sdk';
import { Address, ProtocolType, stringifyObject } from '@hyperlane-xyz/utils';
import { readChainConfigsIfExists } from '../config/chain.js';
import { getMultiProvider } from '../context.js';
import { log, logBlue, logRed } from '../logger.js';
import {
FileFormat,
resolveFileFormat,
writeFileAtPath,
} from '../utils/files.js';
/**
* Read ISM config for a specified chain and address, logging or writing result to file.
*/
export async function readIsmConfig({
chain,
address,
chainConfigPath,
format,
output,
}: {
chain: ChainName;
address: Address;
chainConfigPath: string;
format: FileFormat;
output?: string;
}): Promise<void> {
const customChains = readChainConfigsIfExists(chainConfigPath);
const multiProvider = getMultiProvider(customChains);
if (multiProvider.getProtocol(chain) === ProtocolType.Ethereum) {
const ismReader = new EvmIsmReader(multiProvider, chain);
const config = await ismReader.deriveIsmConfig(address);
const stringConfig = stringifyObject(
config,
resolveFileFormat(output, format),
2,
);
if (!output) {
logBlue(`ISM Config at ${address} on ${chain}:`);
log(stringConfig);
} else {
writeFileAtPath(output, stringConfig + '\n');
logBlue(`ISM Config written to ${output}.`);
}
return;
}
logRed('Unsupported chain. Currently this command supports EVM chains only.');
}

@ -99,7 +99,7 @@ export function mergeYaml<T extends Record<string, any>>(
}
export function readYamlOrJson<T>(filepath: string, format?: FileFormat): T {
return resolveYamlOrJson(filepath, readJson, readYaml, format);
return resolveYamlOrJsonFn(filepath, readJson, readYaml, format);
}
export function writeYamlOrJson(
@ -107,7 +107,7 @@ export function writeYamlOrJson(
obj: Record<string, any>,
format?: FileFormat,
) {
return resolveYamlOrJson(
return resolveYamlOrJsonFn(
filepath,
(f: string) => writeJson(f, obj),
(f: string) => writeYaml(f, obj),
@ -120,7 +120,7 @@ export function mergeYamlOrJson(
obj: Record<string, any>,
format?: FileFormat,
) {
return resolveYamlOrJson(
return resolveYamlOrJsonFn(
filepath,
(f: string) => mergeJson(f, obj),
(f: string) => mergeYaml(f, obj),
@ -128,23 +128,46 @@ export function mergeYamlOrJson(
);
}
function resolveYamlOrJson(
function resolveYamlOrJsonFn(
filepath: string,
jsonFn: any,
yamlFn: any,
format?: FileFormat,
) {
if (format === 'json' || filepath.endsWith('.json')) {
const fileFormat = resolveFileFormat(filepath, format);
if (!fileFormat) {
throw new Error(`Invalid file format for ${filepath}`);
}
if (fileFormat === 'json') {
return jsonFn(filepath);
} else if (
}
return yamlFn(filepath);
}
export function resolveFileFormat(
filepath?: string,
format?: FileFormat,
): FileFormat | undefined {
// early out if filepath is undefined
if (!filepath) {
return format;
}
if (format === 'json' || filepath?.endsWith('.json')) {
return 'json';
}
if (
format === 'yaml' ||
filepath.endsWith('.yaml') ||
filepath.endsWith('.yml')
filepath?.endsWith('.yaml') ||
filepath?.endsWith('.yml')
) {
return yamlFn(filepath);
} else {
throw new Error(`Invalid file format for ${filepath}`);
return 'yaml';
}
return undefined;
}
export function prepNewArtifactsFiles(

@ -1,70 +0,0 @@
import { EvmHookReader, EvmIsmReader, chainMetadata } from '@hyperlane-xyz/sdk';
import { mainnetConfigs } from '../config/environments/mainnet3/chains.js';
import { testnetConfigs } from '../config/environments/testnet4/chains.js';
import { Role } from '../src/roles.js';
import {
getArgs,
getMultiProviderForRole,
withContext,
withNetwork,
} from './agent-utils.js';
// Examples from <monorepo>/typescript/infra:
// Fallback routing hook on polygon (may take 5s):
// yarn tsx scripts/read-config.ts -e mainnet3 --type hook --network polygon --address 0xca4cCe24E7e06241846F5EA0cda9947F0507C40C
// IGP hook on inevm (may take 5s):
// yarn tsx scripts/read-config.ts -e mainnet3 --type hook --network inevm --address 0x19dc38aeae620380430C200a6E990D5Af5480117
// Top-level aggregation ISM on celo (may take 10s)
// yarn tsx scripts/read-config.ts -e mainnet3 --type ism --network celo --address 0x99e8E56Dce3402D6E09A82718937fc1cA2A9491E
// Aggregation ISM for bsc domain on inevm (may take 5s)
// yarn tsx scripts/read-config.ts -e mainnet3 --type ism --network inevm --address 0x79A7c7Fe443971CBc6baD623Fdf8019C379a7178
// Test ISM on alfajores testnet
// yarn tsx scripts/read-config.ts -e testnet4 --type ism --network alfajores --address 0xdB52E4853b6A40D2972E6797E0BDBDb3eB761966
async function readConfig() {
const { environment, network, context, type, address, concurrency } =
await withContext(withNetwork(getArgs()))
.option('type', {
describe: 'Specify the type of config to read',
choices: ['ism', 'hook'],
demandOption: true,
})
.number('concurrency')
.describe(
'concurrency',
'option to override the default concurrency level',
)
.string('address')
.describe('address', 'config address')
.demandOption('address')
.demandOption('network').argv;
const multiProvider = await getMultiProviderForRole(
chainMetadata[network].isTestnet ? testnetConfigs : mainnetConfigs,
environment,
context,
Role.Deployer,
);
if (type === 'ism') {
const ismReader = new EvmIsmReader(multiProvider, network, concurrency);
const config = await ismReader.deriveIsmConfig(address);
console.log(EvmIsmReader.stringifyConfig(config, 2));
} else if (type === 'hook') {
const hookReader = new EvmHookReader(multiProvider, network, concurrency);
const config = await hookReader.deriveHookConfig(address);
console.log(EvmHookReader.stringifyConfig(config, 2));
} else {
console.error('Invalid type specified. Please use "ism" or "hook".');
process.exit(1);
}
process.exit(0);
}
readConfig().catch((e) => {
console.error(e);
process.exit(1);
});

@ -20,7 +20,6 @@ import {
WithAddress,
concurrentMap,
eqAddress,
ethersBigNumberSerializer,
rootLogger,
} from '@hyperlane-xyz/utils';
@ -81,10 +80,6 @@ export class EvmHookReader implements HookReader {
this.provider = this.multiProvider.getProvider(chain);
}
public static stringifyConfig(config: HookConfig, space?: number): string {
return JSON.stringify(config, ethersBigNumberSerializer, space);
}
async deriveHookConfig(address: Address): Promise<WithAddress<HookConfig>> {
const hook = IPostDispatchHook__factory.connect(address, this.provider);
const onchainHookType: OnchainHookType = await hook.hookType();

@ -15,7 +15,6 @@ import {
WithAddress,
assert,
concurrentMap,
ethersBigNumberSerializer,
rootLogger,
} from '@hyperlane-xyz/utils';
@ -70,10 +69,6 @@ export class EvmIsmReader implements IsmReader {
this.provider = this.multiProvider.getProvider(chain);
}
public static stringifyConfig(config: IsmConfig, space?: number): string {
return JSON.stringify(config, ethersBigNumberSerializer, space);
}
async deriveIsmConfig(
address: Address,
): Promise<DerivedIsmConfigWithAddress> {

@ -7,7 +7,8 @@
"@solana/web3.js": "^1.78.0",
"bignumber.js": "^9.1.1",
"ethers": "^5.7.2",
"pino": "^8.19.0"
"pino": "^8.19.0",
"yaml": "^2.3.1"
},
"devDependencies": {
"@types/mocha": "^10.0.1",

@ -110,6 +110,7 @@ export {
objMerge,
pick,
promiseObjAll,
stringifyObject,
} from './objects.js';
export { difference, setEquality, symmetricDifference } from './sets.js';
export {

@ -1,3 +1,7 @@
import { stringify as yamlStringify } from 'yaml';
import { ethersBigNumberSerializer } from './logging.js';
export function isObject(item: any) {
return item && typeof item === 'object' && !Array.isArray(item);
}
@ -118,3 +122,17 @@ export function arrayToObject(keys: Array<string>, val = true) {
return result;
}, {});
}
export function stringifyObject(
object: object,
format: 'json' | 'yaml' = 'yaml',
space?: number,
): string {
// run through JSON first because ethersBigNumberSerializer does not play nice with yamlStringify
// so we fix up in JSON, then parse and if required return yaml on processed JSON after
const json = JSON.stringify(object, ethersBigNumberSerializer, space);
if (format === 'json') {
return json;
}
return yamlStringify(JSON.parse(json), null, space);
}

@ -5224,6 +5224,7 @@ __metadata:
pino: "npm:^8.19.0"
prettier: "npm:^2.8.8"
typescript: "npm:5.3.3"
yaml: "npm:^2.3.1"
languageName: unknown
linkType: soft

Loading…
Cancel
Save