diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index f31069e091..8abcd8ad40 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -12,14 +12,10 @@ import { WarpCoreConfigSchema, WarpRouteDeployConfig, WarpRouteDeployConfigSchema, + WarpRouteDeployConfigSchemaWithoutMailbox, + WarpRouteDeployConfigWithoutMailbox, } from '@hyperlane-xyz/sdk'; -import { - Address, - assert, - isAddress, - objMap, - promiseObjAll, -} from '@hyperlane-xyz/utils'; +import { Address, assert, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; import { CommandContext } from '../context/types.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; @@ -108,7 +104,7 @@ export async function readWarpRouteDeployConfig( } export function isValidWarpRouteDeployConfig(config: any) { - return WarpRouteDeployConfigSchema.safeParse(config).success; + return WarpRouteDeployConfigSchemaWithoutMailbox.safeParse(config).success; } export async function createWarpRouteDeployConfig({ @@ -131,7 +127,7 @@ export async function createWarpRouteDeployConfig({ requiresConfirmation: !context.skipConfirmation, }); - const result: WarpRouteDeployConfig = {}; + const result: WarpRouteDeployConfigWithoutMailbox = {}; let typeChoices = TYPE_CHOICES; for (const chain of warpChains) { logBlue(`${chain}: Configuring warp route...`); @@ -142,16 +138,6 @@ export async function createWarpRouteDeployConfig({ 'signer', ); - // default to the mailbox from the registry and if not found ask to the user to submit one - const chainAddresses = await context.registry.getChainAddresses(chain); - - const mailbox = - chainAddresses?.mailbox ?? - (await input({ - validate: isAddress, - message: `Could not retrieve mailbox address from the registry for chain "${chain}". Please enter a valid mailbox address:`, - })); - const proxyAdmin: DeployedOwnableConfig = await setProxyAdminConfig( context, chain, @@ -198,7 +184,6 @@ export async function createWarpRouteDeployConfig({ case TokenType.collateralUri: case TokenType.fastCollateral: result[chain] = { - mailbox, type, owner, proxyAdmin, @@ -211,7 +196,6 @@ export async function createWarpRouteDeployConfig({ break; case TokenType.syntheticRebase: result[chain] = { - mailbox, type, owner, isNft, @@ -226,7 +210,6 @@ export async function createWarpRouteDeployConfig({ break; case TokenType.collateralVaultRebase: result[chain] = { - mailbox, type, owner, proxyAdmin, @@ -241,7 +224,6 @@ export async function createWarpRouteDeployConfig({ break; case TokenType.collateralVault: result[chain] = { - mailbox, type, owner, proxyAdmin, @@ -254,7 +236,6 @@ export async function createWarpRouteDeployConfig({ break; default: result[chain] = { - mailbox, type, owner, proxyAdmin, @@ -265,7 +246,8 @@ export async function createWarpRouteDeployConfig({ } try { - const warpRouteDeployConfig = WarpRouteDeployConfigSchema.parse(result); + const warpRouteDeployConfig = + WarpRouteDeployConfigSchemaWithoutMailbox.parse(result); logBlue(`Warp Route config is valid, writing to file ${outPath}:\n`); log(indentYamlOrJson(yamlStringify(warpRouteDeployConfig, null, 2), 4)); writeYamlOrJson(outPath, warpRouteDeployConfig, 'yaml'); diff --git a/typescript/cli/src/tests/warp/warp-init.e2e-test.ts b/typescript/cli/src/tests/warp/warp-init.e2e-test.ts index 4cdc4a6eec..92a9e10546 100644 --- a/typescript/cli/src/tests/warp/warp-init.e2e-test.ts +++ b/typescript/cli/src/tests/warp/warp-init.e2e-test.ts @@ -1,12 +1,10 @@ import { expect } from 'chai'; import { Wallet } from 'ethers'; -import { ChainAddresses } from '@hyperlane-xyz/registry'; import { - ChainMap, ChainName, TokenType, - WarpRouteDeployConfig, + WarpRouteDeployConfigWithoutMailbox, } from '@hyperlane-xyz/sdk'; import { Address } from '@hyperlane-xyz/utils'; @@ -16,7 +14,6 @@ import { CHAIN_NAME_2, CHAIN_NAME_3, CONFIRM_DETECTED_OWNER_STEP, - CORE_CONFIG_PATH, DEFAULT_E2E_TEST_TIMEOUT, KeyBoardKeys, SELECT_ANVIL_2_AND_ANVIL_3_STEPS, @@ -24,7 +21,6 @@ import { SELECT_MAINNET_CHAIN_TYPE_STEP, TestPromptAction, WARP_CONFIG_PATH_2, - deployOrUseExistingCore, deployToken, handlePrompts, } from '../commands/helpers.js'; @@ -33,38 +29,21 @@ import { hyperlaneWarpInit } from '../commands/warp.js'; describe('hyperlane warp init e2e tests', async function () { this.timeout(2 * DEFAULT_E2E_TEST_TIMEOUT); - let chain2Addresses: ChainAddresses = {}; - let chain3Addresses: ChainAddresses = {}; let initialOwnerAddress: Address; - let chainMapAddresses: ChainMap = {}; before(async function () { - [chain2Addresses, chain3Addresses] = await Promise.all([ - deployOrUseExistingCore(CHAIN_NAME_2, CORE_CONFIG_PATH, ANVIL_KEY), - deployOrUseExistingCore(CHAIN_NAME_3, CORE_CONFIG_PATH, ANVIL_KEY), - ]); - - chainMapAddresses = { - [CHAIN_NAME_2]: chain2Addresses, - [CHAIN_NAME_3]: chain3Addresses, - }; - const wallet = new Wallet(ANVIL_KEY); initialOwnerAddress = wallet.address; }); describe('hyperlane warp init --yes', () => { function assertWarpConfig( - warpConfig: WarpRouteDeployConfig, - chainMapAddresses: ChainMap, + warpConfig: WarpRouteDeployConfigWithoutMailbox, chainName: ChainName, ) { expect(warpConfig[chainName]).not.to.be.undefined; const chain2TokenConfig = warpConfig[chainName]; - expect(chain2TokenConfig.mailbox).equal( - chainMapAddresses[chainName].mailbox, - ); expect(chain2TokenConfig.owner).equal(initialOwnerAddress); expect(chain2TokenConfig.type).equal(TokenType.native); } @@ -91,10 +70,10 @@ describe('hyperlane warp init e2e tests', async function () { await handlePrompts(output, steps); - const warpConfig: WarpRouteDeployConfig = + const warpConfig: WarpRouteDeployConfigWithoutMailbox = readYamlOrJson(WARP_CONFIG_PATH_2); - assertWarpConfig(warpConfig, chainMapAddresses, CHAIN_NAME_2); + assertWarpConfig(warpConfig, CHAIN_NAME_2); }); it('it should generate a warp deploy config with a 2 chains warp route (native->native)', async function () { @@ -119,11 +98,11 @@ describe('hyperlane warp init e2e tests', async function () { await handlePrompts(output, steps); - const warpConfig: WarpRouteDeployConfig = + const warpConfig: WarpRouteDeployConfigWithoutMailbox = readYamlOrJson(WARP_CONFIG_PATH_2); [CHAIN_NAME_2, CHAIN_NAME_3].map((chainName) => - assertWarpConfig(warpConfig, chainMapAddresses, chainName), + assertWarpConfig(warpConfig, chainName), ); }); @@ -159,13 +138,12 @@ describe('hyperlane warp init e2e tests', async function () { await handlePrompts(output, steps); - const warpConfig: WarpRouteDeployConfig = + const warpConfig: WarpRouteDeployConfigWithoutMailbox = readYamlOrJson(WARP_CONFIG_PATH_2); expect(warpConfig[CHAIN_NAME_2]).not.to.be.undefined; const chain2TokenConfig = warpConfig[CHAIN_NAME_2]; - expect(chain2TokenConfig.mailbox).equal(chain2Addresses.mailbox); expect(chain2TokenConfig.owner).equal(initialOwnerAddress); expect(chain2TokenConfig.type).equal(TokenType.collateral); expect((chain2TokenConfig as any).token).equal(erc20Token.address); @@ -173,7 +151,6 @@ describe('hyperlane warp init e2e tests', async function () { expect(warpConfig[CHAIN_NAME_3]).not.to.be.undefined; const chain3TokenConfig = warpConfig[CHAIN_NAME_3]; - expect(chain3TokenConfig.mailbox).equal(chain3Addresses.mailbox); expect(chain3TokenConfig.owner).equal(initialOwnerAddress); expect(chain3TokenConfig.type).equal(TokenType.synthetic); }); diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 0ec8e740b3..31bee3c013 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -587,6 +587,8 @@ export { WarpRouteDeployConfig, WarpRouteDeployConfigSchema, WarpRouteDeployConfigSchemaErrors, + WarpRouteDeployConfigWithoutMailbox, + WarpRouteDeployConfigSchemaWithoutMailbox, } from './token/types.js'; export { ChainMap, diff --git a/typescript/sdk/src/token/types.ts b/typescript/sdk/src/token/types.ts index 9b76ea8afb..3a78a763c3 100644 --- a/typescript/sdk/src/token/types.ts +++ b/typescript/sdk/src/token/types.ts @@ -101,58 +101,86 @@ export const HypTokenRouterConfigSchema = HypTokenConfigSchema.and( ); export type HypTokenRouterConfig = z.infer; -export const WarpRouteDeployConfigSchema = z - .record(HypTokenRouterConfigSchema) - .refine((configMap) => { - const entries = Object.entries(configMap); - return ( - entries.some( - ([_, config]) => - isCollateralTokenConfig(config) || - isCollateralRebaseTokenConfig(config) || - isNativeTokenConfig(config), - ) || entries.every(([_, config]) => isTokenMetadata(config)) - ); - }, WarpRouteDeployConfigSchemaErrors.NO_SYNTHETIC_ONLY) - .transform((warpRouteDeployConfig, ctx) => { - const collateralRebaseEntry = Object.entries(warpRouteDeployConfig).find( - ([_, config]) => isCollateralRebaseTokenConfig(config), - ); - - const syntheticRebaseEntry = Object.entries(warpRouteDeployConfig).find( - ([_, config]) => isSyntheticRebaseTokenConfig(config), - ); - - // Require both collateral rebase and synthetic rebase to be present in the config - if (!collateralRebaseEntry && !syntheticRebaseEntry) { - // Pass through for other token types - return warpRouteDeployConfig; - } - - if ( - collateralRebaseEntry && - isCollateralRebasePairedCorrectly(warpRouteDeployConfig) - ) { - const collateralChainName = collateralRebaseEntry[0]; - return objMap(warpRouteDeployConfig, (_, config) => { - if (config.type === TokenType.syntheticRebase) - config.collateralChainName = collateralChainName; - return config; - }) as Record; - } - - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: WarpRouteDeployConfigSchemaErrors.ONLY_SYNTHETIC_REBASE, +export const HypTokenRouterConfigWithoutMailboxSchema = + HypTokenConfigSchema.and(GasRouterConfigSchema.omit({ mailbox: true })); + +export type HypTokenRouterConfigWithoutMailbox = z.infer< + typeof HypTokenRouterConfigWithoutMailboxSchema +>; + +export const WarpRouteDeployConfigSchemaWithoutMailbox = z + .record(HypTokenRouterConfigWithoutMailboxSchema) + .refine(refineTokens, WarpRouteDeployConfigSchemaErrors.NO_SYNTHETIC_ONLY) + .transform((config, ctx) => transformRebaseConfig(config, ctx)); + +export type WarpRouteDeployConfigWithoutMailbox = z.infer< + typeof WarpRouteDeployConfigSchemaWithoutMailbox +>; + +function refineTokens< + T extends HypTokenRouterConfig | HypTokenRouterConfigWithoutMailbox, +>(configMap: Record): boolean { + const entries = Object.entries(configMap); + return ( + entries.some( + ([_, config]) => + isCollateralTokenConfig(config) || + isCollateralRebaseTokenConfig(config) || + isNativeTokenConfig(config), + ) || entries.every(([_, config]) => isTokenMetadata(config)) + ); +} + +function transformRebaseConfig< + T extends HypTokenRouterConfig | HypTokenRouterConfigWithoutMailbox, +>( + warpRouteDeployConfig: Record, + ctx: z.RefinementCtx, +): Record | typeof z.NEVER { + const collateralRebaseEntry = Object.entries(warpRouteDeployConfig).find( + ([_, config]) => isCollateralRebaseTokenConfig(config), + ); + + const syntheticRebaseEntry = Object.entries(warpRouteDeployConfig).find( + ([_, config]) => isSyntheticRebaseTokenConfig(config), + ); + + // Require both collateral rebase and synthetic rebase to be present in the config + if (!collateralRebaseEntry && !syntheticRebaseEntry) { + // Pass through for other token types + return warpRouteDeployConfig; + } + + if ( + collateralRebaseEntry && + isCollateralRebasePairedCorrectly(warpRouteDeployConfig) + ) { + const collateralChainName = collateralRebaseEntry[0]; + return objMap(warpRouteDeployConfig, (_, config) => { + if (config.type === TokenType.syntheticRebase) + config.collateralChainName = collateralChainName; + return config; }); + } - return z.NEVER; // Causes schema validation to throw with above issue + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: WarpRouteDeployConfigSchemaErrors.ONLY_SYNTHETIC_REBASE, }); + + return z.NEVER; // Causes schema validation to throw with above issue +} + +export const WarpRouteDeployConfigSchema = z + .record(HypTokenRouterConfigSchema) + .refine(refineTokens, WarpRouteDeployConfigSchemaErrors.NO_SYNTHETIC_ONLY) + .transform(transformRebaseConfig); + export type WarpRouteDeployConfig = z.infer; -function isCollateralRebasePairedCorrectly( - warpRouteDeployConfig: Record, -): boolean { +function isCollateralRebasePairedCorrectly< + T extends HypTokenRouterConfig | HypTokenRouterConfigWithoutMailbox, +>(warpRouteDeployConfig: Record): boolean { // Filter out all the non-collateral rebase configs to check if they are only synthetic rebase tokens const otherConfigs = Object.entries(warpRouteDeployConfig).filter( ([_, config]) => !isCollateralRebaseTokenConfig(config), @@ -161,8 +189,8 @@ function isCollateralRebasePairedCorrectly( if (otherConfigs.length === 0) return false; // The other configs MUST be synthetic rebase - const allOthersSynthetic: boolean = otherConfigs.every( - ([_, config], _index) => isSyntheticRebaseTokenConfig(config), + const allOthersSynthetic: boolean = otherConfigs.every(([_, config]) => + isSyntheticRebaseTokenConfig(config), ); return allOthersSynthetic; }