UNPKG

@hyperlane-xyz/cli

Version:

A command-line utility for common Hyperlane operations

227 lines 9.74 kB
import { confirm, input, select } from '@inquirer/prompts'; import { stringify as yamlStringify } from 'yaml'; import { IsmType, TokenType, WarpCoreConfigSchema, WarpRouteDeployConfigMailboxRequiredSchema, WarpRouteDeployConfigSchema, } from '@hyperlane-xyz/sdk'; import { assert, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; import { runMultiChainSelectionStep } from '../utils/chains.js'; import { indentYamlOrJson, readYamlOrJson, writeYamlOrJson, } from '../utils/files.js'; import { detectAndConfirmOrPrompt, setProxyAdminConfig, } from '../utils/input.js'; import { createAdvancedIsmConfig } from './ism.js'; const TYPE_DESCRIPTIONS = { [TokenType.synthetic]: 'A new ERC20 with remote transfer functionality', [TokenType.syntheticRebase]: `A rebasing ERC20 with remote transfer functionality. Must be paired with ${TokenType.collateralVaultRebase}`, [TokenType.collateral]: 'Extends an existing ERC20 with remote transfer functionality', [TokenType.native]: 'Extends the native token with remote transfer functionality', [TokenType.collateralVault]: 'Extends an existing ERC4626 with remote transfer functionality. Yields are manually claimed by owner.', [TokenType.collateralVaultRebase]: 'Extends an existing ERC4626 with remote transfer functionality. Rebases yields to token holders.', [TokenType.collateralFiat]: 'Extends an existing FiatToken with remote transfer functionality', [TokenType.XERC20]: 'Extends an existing xERC20 with Warp Route functionality', [TokenType.XERC20Lockbox]: 'Extends an existing xERC20 Lockbox with Warp Route functionality', // TODO: describe [TokenType.syntheticUri]: '', [TokenType.collateralUri]: '', [TokenType.nativeScaled]: '', }; const TYPE_CHOICES = Object.values(TokenType).map((type) => ({ name: type, value: type, description: TYPE_DESCRIPTIONS[type], })); async function fillDefaults(context, config) { return promiseObjAll(objMap(config, async (chain, config) => { let mailbox = config.mailbox; if (!mailbox) { const addresses = await context.registry.getChainAddresses(chain); assert(addresses, `No addresses found for chain ${chain}`); mailbox = addresses.mailbox; } let owner = config.owner; if (!owner) { owner = context.signerAddress ?? (await context.multiProvider.getSignerAddress(chain)); } return { owner, mailbox, ...config, }; })); } export async function readWarpRouteDeployConfig(filePath, context) { let config = readYamlOrJson(filePath); if (!config) throw new Error(`No warp route deploy config found at ${filePath}`); config = await fillDefaults(context, config); //fillDefaults would have added a mailbox to the config if it was missing return WarpRouteDeployConfigMailboxRequiredSchema.parse(config); } export function isValidWarpRouteDeployConfig(config) { return WarpRouteDeployConfigSchema.safeParse(config).success; } export async function createWarpRouteDeployConfig({ context, outPath, advanced = false, }) { logBlue('Creating a new warp route deployment config...'); const warpChains = await runMultiChainSelectionStep({ chainMetadata: context.chainMetadata, message: 'Select chains to connect', requireNumber: 1, // If the user supplied the --yes flag we skip asking selection // confirmation requiresConfirmation: !context.skipConfirmation, }); const result = {}; let typeChoices = TYPE_CHOICES; for (const chain of warpChains) { logBlue(`${chain}: Configuring warp route...`); const owner = await detectAndConfirmOrPrompt(async () => context.signerAddress, 'Enter the desired', 'owner address', 'signer'); const proxyAdmin = await setProxyAdminConfig(context, chain); /** * The logic from the cli is as follows: * --yes flag is provided: set ism to undefined (default ISM config) * --advanced flag is provided: the user will have to build their own configuration using the available ISM types * -- no flag is provided: the user must choose if the default ISM config should be used: * - yes: the default ISM config will be used (Trusted ISM + Default fallback ISM) * - no: keep ism as undefined (default ISM config) */ let interchainSecurityModule; if (context.skipConfirmation) { interchainSecurityModule = undefined; } else if (advanced) { interchainSecurityModule = await createAdvancedIsmConfig(context); } else if (await confirm({ message: 'Do you want to use a trusted ISM for warp route?', })) { interchainSecurityModule = createDefaultWarpIsmConfig(owner); } const type = await select({ message: `Select ${chain}'s token type`, choices: typeChoices, }); // TODO: restore NFT prompting const isNft = type === TokenType.syntheticUri || type === TokenType.collateralUri; switch (type) { case TokenType.collateral: case TokenType.XERC20: case TokenType.XERC20Lockbox: case TokenType.collateralFiat: case TokenType.collateralUri: result[chain] = { type, owner, proxyAdmin, isNft, interchainSecurityModule, token: await input({ message: `Enter the existing token address on chain ${chain}`, }), }; break; case TokenType.syntheticRebase: result[chain] = { type, owner, isNft, proxyAdmin, collateralChainName: '', // This will be derived correctly by zod.parse() below interchainSecurityModule, }; typeChoices = restrictChoices([ TokenType.syntheticRebase, TokenType.collateralVaultRebase, ]); break; case TokenType.collateralVaultRebase: result[chain] = { type, owner, proxyAdmin, isNft, interchainSecurityModule, token: await input({ message: `Enter the ERC-4626 vault address on chain ${chain}`, }), }; typeChoices = restrictChoices([TokenType.syntheticRebase]); break; case TokenType.collateralVault: result[chain] = { type, owner, proxyAdmin, isNft, interchainSecurityModule, token: await input({ message: `Enter the ERC-4626 vault address on chain ${chain}`, }), }; break; default: result[chain] = { type, owner, proxyAdmin, isNft, interchainSecurityModule, }; } } try { const warpRouteDeployConfig = WarpRouteDeployConfigSchema.parse(result); logBlue(`Warp Route config is valid, writing to file ${outPath}:\n`); log(indentYamlOrJson(yamlStringify(warpRouteDeployConfig, null, 2), 4)); writeYamlOrJson(outPath, warpRouteDeployConfig, 'yaml'); logGreen('✅ Successfully created new warp route deployment config.'); } catch (e) { errorRed(`Warp route deployment config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/warp-route-deployment.yaml for an example.`); throw e; } } function restrictChoices(typeChoices) { return TYPE_CHOICES.filter((choice) => typeChoices.includes(choice.name)); } // Note, this is different than the function above which reads a config // for a DEPLOYMENT. This gets a config for using a warp route (aka WarpCoreConfig) export function readWarpCoreConfig(filePath) { const config = readYamlOrJson(filePath); if (!config) throw new Error(`No warp route config found at ${filePath}`); return WarpCoreConfigSchema.parse(config); } /** * Creates a default configuration for an ISM with a TRUSTED_RELAYER and FALLBACK_ROUTING. * * Properties relayer and owner are both set as input owner. * * @param owner - The address of the owner of the ISM. * @returns The default Aggregation ISM configuration. */ function createDefaultWarpIsmConfig(owner) { return { type: IsmType.AGGREGATION, modules: [ { type: IsmType.TRUSTED_RELAYER, relayer: owner, }, createFallbackRoutingConfig(owner), ], threshold: 1, }; } /** * Creates a fallback configuration for an ISM with a FALLBACK_ROUTING and the provided `owner`. * * @param owner - The address of the owner of the ISM. * @returns The Fallback Routing ISM configuration. */ function createFallbackRoutingConfig(owner) { return { type: IsmType.FALLBACK_ROUTING, domains: {}, owner, }; } //# sourceMappingURL=warp.js.map