diff --git a/.changeset/nice-rivers-own.md b/.changeset/nice-rivers-own.md new file mode 100644 index 0000000000..8000edf814 --- /dev/null +++ b/.changeset/nice-rivers-own.md @@ -0,0 +1,7 @@ +--- +'@hyperlane-xyz/infra': minor +'@hyperlane-xyz/cli': minor +'@hyperlane-xyz/sdk': minor +--- + +Implement multi collateral warp routes diff --git a/typescript/cli/src/commands/config.ts b/typescript/cli/src/commands/config.ts index b498db4b96..bf30e78ee8 100644 --- a/typescript/cli/src/commands/config.ts +++ b/typescript/cli/src/commands/config.ts @@ -170,7 +170,7 @@ const validateWarpCommand: CommandModuleWithContext<{ path: string }> = { path: inputFileCommandOption, }, handler: async ({ path }) => { - readWarpRouteDeployConfig(path); + await readWarpRouteDeployConfig(path); logGreen('Config is valid'); process.exit(0); }, diff --git a/typescript/cli/src/config/chain.ts b/typescript/cli/src/config/chain.ts index 232205dc04..c2655cab31 100644 --- a/typescript/cli/src/config/chain.ts +++ b/typescript/cli/src/config/chain.ts @@ -46,8 +46,8 @@ export async function createChainConfig({ await new ethers.providers.JsonRpcProvider().getNetwork(); return ethers.providers.JsonRpcProvider.defaultUrl(); }, - 'rpc url', 'Enter http or https', + 'rpc url', ); const provider = new ethers.providers.JsonRpcProvider(rpcUrl); @@ -58,8 +58,8 @@ export async function createChainConfig({ const client = clientName.split('/')[0]; return `${client}${port}`; }, - 'chain name', 'Enter (one word, lower case)', + 'chain name', ); const chainId = parseInt( @@ -68,8 +68,8 @@ export async function createChainConfig({ const network = await provider.getNetwork(); return network.chainId.toString(); }, - 'chain id', 'Enter a (number)', + 'chain id', ), 10, ); diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 19f1a1fbde..189d5198cf 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -1,30 +1,87 @@ -import { confirm, input } from '@inquirer/prompts'; -import { ethers } from 'ethers'; +import { input, select } from '@inquirer/prompts'; import { - ChainMetadata, + ChainMap, + MailboxClientConfig, TokenType, WarpCoreConfig, WarpCoreConfigSchema, WarpRouteDeployConfig, WarpRouteDeployConfigSchema, } from '@hyperlane-xyz/sdk'; -import { objFilter } from '@hyperlane-xyz/utils'; +import { assert, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; import { CommandContext } from '../context/types.js'; import { errorRed, logBlue, logGreen } from '../logger.js'; import { + detectAndConfirmOrPrompt, runMultiChainSelectionStep, - runSingleChainSelectionStep, } from '../utils/chains.js'; import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js'; -export function readWarpRouteDeployConfig( +const TYPE_DESCRIPTIONS: Record = { + [TokenType.synthetic]: 'A new ERC20 with remote transfer functionality', + [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', + [TokenType.collateralFiat]: + 'Extends an existing FiatToken with remote transfer functionality', + [TokenType.collateralXERC20]: + 'Extends an existing xERC20 with Warp Route functionality', + // TODO: describe + [TokenType.fastSynthetic]: '', + [TokenType.syntheticUri]: '', + [TokenType.fastCollateral]: '', + [TokenType.collateralUri]: '', + [TokenType.nativeScaled]: '', +}; + +const TYPE_CHOICES = Object.values(TokenType).map((type) => ({ + name: type, + value: type, + description: TYPE_DESCRIPTIONS[type], +})); + +async function fillDefaults( + context: CommandContext, + config: ChainMap>, +): Promise> { + return promiseObjAll( + objMap(config, async (chain, config): Promise => { + 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 = + (await context.signer?.getAddress()) ?? + (await context.multiProvider.getSignerAddress(chain)); + } + return { + owner, + mailbox, + ...config, + }; + }), + ); +} + +export async function readWarpRouteDeployConfig( filePath: string, -): WarpRouteDeployConfig { - const config = readYamlOrJson(filePath); + context?: CommandContext, +): Promise { + let config = readYamlOrJson(filePath); if (!config) throw new Error(`No warp route deploy config found at ${filePath}`); + if (context) { + config = await fillDefaults(context, config as any); + } return WarpRouteDeployConfigSchema.parse(config); } @@ -40,75 +97,70 @@ export async function createWarpRouteDeployConfig({ outPath: string; }) { logBlue('Creating a new warp route deployment config'); - const baseChain = await runSingleChainSelectionStep( - context.chainMetadata, - 'Select base chain with the original token to warp', + + const owner = await detectAndConfirmOrPrompt( + async () => context.signer?.getAddress(), + 'Enter the desired', + 'owner address', ); - const isNative = await confirm({ - message: - 'Are you creating a route for the native token of the base chain (e.g. Ether on Ethereum)?', - }); - - const isNft = isNative - ? false - : await confirm({ message: 'Is this an NFT (i.e. ERC-721)?' }); - const isYieldBearing = - isNative || isNft - ? false - : await confirm({ - message: - 'Do you want this warp route to be yield-bearing (i.e. deposits into ERC-4626 vault)?', - }); - - const addressMessage = `Enter the ${ - isYieldBearing ? 'ERC-4626 vault' : 'collateral token' - } address`; - const baseAddress = isNative - ? ethers.constants.AddressZero - : await input({ message: addressMessage }); - - const metadataWithoutBase = objFilter( + const warpChains = await runMultiChainSelectionStep( context.chainMetadata, - (chain, _): _ is ChainMetadata => chain !== baseChain, - ); - const syntheticChains = await runMultiChainSelectionStep( - metadataWithoutBase, - 'Select chains to which the base token will be connected', + 'Select chains to connect', ); - // TODO add more prompts here to support customizing the token metadata - let result: WarpRouteDeployConfig; - if (isNative) { - result = { - [baseChain]: { - type: TokenType.native, - }, - }; - } else { - result = { - [baseChain]: { - type: isYieldBearing ? TokenType.collateralVault : TokenType.collateral, - token: baseAddress, - isNft, + const result: WarpRouteDeployConfig = {}; + for (const chain of warpChains) { + logBlue(`Configuring warp route for chain ${chain}`); + const type = await select({ + message: `Select ${chain}'s token type`, + choices: TYPE_CHOICES, + }); + + // TODO: restore NFT prompting + const isNft = + type === TokenType.syntheticUri || type === TokenType.collateralUri; + + const mailbox = await detectAndConfirmOrPrompt( + async () => { + const addresses = await context.registry.getChainAddresses(chain); + return addresses?.mailbox; }, - }; - } + `For ${chain}, enter the`, + 'mailbox address', + ); - syntheticChains.map((chain) => { - result[chain] = { - type: TokenType.synthetic, - }; - }); + switch (type) { + case TokenType.collateral: + case TokenType.collateralXERC20: + case TokenType.collateralFiat: + case TokenType.collateralUri: + case TokenType.fastCollateral: + case TokenType.collateralVault: + result[chain] = { + mailbox, + type, + owner, + isNft, + token: await input({ + message: `Enter the existing token address on chain ${chain}`, + }), + }; + break; + default: + result[chain] = { mailbox, type, owner, isNft }; + } + } - if (isValidWarpRouteDeployConfig(result)) { + try { + const parsed = WarpRouteDeployConfigSchema.parse(result); logGreen(`Warp Route config is valid, writing to file ${outPath}`); - writeYamlOrJson(outPath, result); - } else { + writeYamlOrJson(outPath, parsed); + } 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 new Error('Invalid multisig config'); + throw e; } } diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index a2f411d2ec..f1082b3013 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -18,36 +18,6 @@ import { assertSigner } from '../utils/keys.js'; import { completeDryRun } from './dry-run.js'; -export async function runPreflightChecks({ - context, - origin, - remotes, - minGas, - chainsToGasCheck, -}: { - context: WriteCommandContext; - origin: ChainName; - remotes: ChainName[]; - minGas: string; - chainsToGasCheck?: ChainName[]; -}) { - log('Running pre-flight checks...'); - - if (!origin || !remotes?.length) throw new Error('Invalid chain selection'); - logGreen('✅ Chain selections are valid'); - - if (remotes.includes(origin)) - throw new Error('Origin and remotes must be distinct'); - logGreen('✅ Origin and remote are distinct'); - - return runPreflightChecksForChains({ - context, - chains: [origin, ...remotes], - minGas, - chainsToGasCheck, - }); -} - export async function runPreflightChecksForChains({ context, chains, diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index a4a92c465a..eb07f189af 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -1,38 +1,35 @@ -import { confirm, input } from '@inquirer/prompts'; +import { confirm } from '@inquirer/prompts'; import { - ChainMap, - ChainName, - ConnectionClientConfig, - EvmTokenAdapter, HypERC20Deployer, HypERC721Deployer, HyperlaneContractsMap, - MinimalTokenMetadata, - MultiProtocolProvider, - MultiProvider, - RouterConfig, TOKEN_TYPE_TO_STANDARD, - TokenConfig, TokenFactories, - TokenRouterConfig, TokenType, WarpCoreConfig, WarpRouteDeployConfig, getTokenConnectionId, - isCollateralConfig, - isNativeConfig, - isSyntheticConfig, + isTokenMetadata, } from '@hyperlane-xyz/sdk'; import { ProtocolType } from '@hyperlane-xyz/utils'; import { readWarpRouteDeployConfig } from '../config/warp.js'; import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js'; import { WriteCommandContext } from '../context/types.js'; -import { log, logBlue, logGray, logGreen } from '../logger.js'; +import { log, logBlue, logGray, logGreen, logTable } from '../logger.js'; import { isFile, runFileSelectionStep } from '../utils/files.js'; -import { completeDeploy, prepareDeploy, runPreflightChecks } from './utils.js'; +import { + completeDeploy, + prepareDeploy, + runPreflightChecksForChains, +} from './utils.js'; + +interface DeployParams { + context: WriteCommandContext; + configMap: WarpRouteDeployConfig; +} export async function runWarpRouteDeploy({ context, @@ -59,30 +56,28 @@ export async function runWarpRouteDeploy({ `Using warp route deployment config at ${warpRouteDeploymentConfigPath}`, ); } - const warpRouteConfig = readWarpRouteDeployConfig( + const warpRouteConfig = await readWarpRouteDeployConfig( warpRouteDeploymentConfigPath, - ); - - const configs = await runBuildConfigStep({ context, - warpRouteConfig, - }); + ); const deploymentParams = { context, - ...configs, + configMap: warpRouteConfig, }; logBlue('Warp route deployment plan'); await runDeployPlanStep(deploymentParams); - await runPreflightChecks({ - ...deploymentParams, + const chains = Object.keys(warpRouteConfig); + + await runPreflightChecksForChains({ + context, + chains, minGas: MINIMUM_WARP_DEPLOY_GAS, }); const userAddress = await signer.getAddress(); - const chains = [deploymentParams.origin, ...configs.remotes]; const initialBalances = await prepareDeploy(context, userAddress, chains); @@ -91,111 +86,13 @@ export async function runWarpRouteDeploy({ await completeDeploy(context, 'warp', initialBalances, userAddress, chains); } -async function runBuildConfigStep({ - context, - warpRouteConfig, -}: { - context: WriteCommandContext; - warpRouteConfig: WarpRouteDeployConfig; -}) { - const { registry, signer, multiProvider, skipConfirmation } = context; - log('Assembling token configs'); - const chainAddresses = await registry.getAddresses(); - const owner = await signer.getAddress(); - const requiredRouterFields: Array = ['mailbox']; - const remotes: string[] = []; - - /// @dev This will keep track of the base collateral metadata which can get overwritten if there are multiple collaterals. - /// These 'base' variables are used to derive synthetic fields - /// @todo Remove this artifact when multi-collateral is enabled - let baseChainName = ''; - let baseMetadata = {} as MinimalTokenMetadata; - // Define configs that coalesce together values from the config file - for (const [chain, config] of Object.entries(warpRouteConfig)) { - // the artifacts, and the SDK as a fallback - config.owner = owner; - config.mailbox = config.mailbox || chainAddresses[chain]?.mailbox; - config.interchainSecurityModule = - config.interchainSecurityModule || - chainAddresses[chain]?.interchainSecurityModule || - chainAddresses[chain]?.multisigIsm; - // config.ismFactory: chainAddresses[baseChainName].domainRoutingIsmFactory, // TODO fix when updating from routingIsm - - if (isCollateralConfig(config) || isNativeConfig(config)) { - // Store the base metadata - baseChainName = chain; - baseMetadata = await fetchBaseTokenMetadata(chain, config, multiProvider); - log( - `Using token metadata: Name: ${baseMetadata.name}, Symbol: ${baseMetadata.symbol}, Decimals: ${baseMetadata.decimals}`, - ); - if (isCollateralConfig(config)) { - config.name = baseMetadata.name; - config.symbol = baseMetadata.symbol; - config.decimals = baseMetadata.decimals; - } - } else if (isSyntheticConfig(config)) { - // Use the config, or baseMetadata - config.name = config.name || baseMetadata.name; - config.symbol = config.symbol || baseMetadata.symbol; - config.totalSupply = config.totalSupply || 0; - remotes.push(chain); - } - - let hasShownInfo = false; - // Request input for any address fields that are missing - for (const field of requiredRouterFields) { - if (config[field]) continue; - if (skipConfirmation) - throw new Error(`Field ${field} for token on ${chain} required`); - if (!hasShownInfo) { - logBlue( - 'Some router fields are missing. Please enter them now, add them to your warp config, or use the --core flag to use deployment artifacts.', - ); - hasShownInfo = true; - } - const value = await input({ - message: `Enter ${field} for ${getTokenName(config)} token on ${chain}`, - }); - if (!value) throw new Error(`Field ${field} required`); - config[field] = value.trim(); - } - } - - log('Token configs ready'); - return { - configMap: warpRouteConfig, - origin: baseChainName, - metadata: baseMetadata, - remotes, - }; -} +async function runDeployPlanStep({ context, configMap }: DeployParams) { + const { skipConfirmation } = context; -interface DeployParams { - context: WriteCommandContext; - configMap: WarpRouteDeployConfig; - metadata: MinimalTokenMetadata; - origin: ChainName; - remotes: ChainName[]; -} - -async function runDeployPlanStep({ - context, - configMap, - origin, - remotes, -}: DeployParams) { - const { signer, skipConfirmation } = context; - const address = await signer.getAddress(); - const baseToken = configMap[origin]; - - const baseName = getTokenName(baseToken); logBlue('\nDeployment plan'); logGray('==============='); - log(`Collateral type will be ${baseToken.type}`); - log(`Transaction signer and owner of new contracts will be ${address}`); - log(`Deploying a warp route with a base of ${baseName} token on ${origin}`); - log(`Connecting it to new synthetic tokens on ${remotes.join(', ')}`); log(`Using token standard ${configMap.isNft ? 'ERC721' : 'ERC20'}`); + logTable(configMap); if (skipConfirmation) return; @@ -210,80 +107,65 @@ async function executeDeploy(params: DeployParams) { const { configMap, - context: { registry, multiProvider, isDryRun }, + context: { registry, multiProvider, isDryRun, dryRunChain }, } = params; const deployer = configMap.isNft ? new HypERC721Deployer(multiProvider) : new HypERC20Deployer(multiProvider); - const config = isDryRun - ? { [params.origin]: configMap[params.origin] } - : configMap; + const config: WarpRouteDeployConfig = + isDryRun && dryRunChain + ? { [dryRunChain]: configMap[dryRunChain] } + : configMap; - const deployedContracts = await deployer.deploy( - config as ChainMap, - ); /// @todo remove ChainMap once Hyperlane deployers are refactored + const deployedContracts = await deployer.deploy(config); logGreen('✅ Hyp token deployments complete'); if (!isDryRun) log('Writing deployment artifacts'); - const warpCoreConfig = getWarpCoreConfig(params, deployedContracts); + const warpCoreConfig = await getWarpCoreConfig(params, deployedContracts); await registry.addWarpRoute(warpCoreConfig); log(JSON.stringify(warpCoreConfig, null, 2)); logBlue('Deployment is complete!'); } -async function fetchBaseTokenMetadata( - chain: string, - config: TokenRouterConfig, - multiProvider: MultiProvider, -): Promise { - if (config.type === TokenType.native) { - // If it's a native token, use the chain's native token metadata - const chainNativeToken = multiProvider.getChainMetadata(chain).nativeToken; - if (chainNativeToken) return chainNativeToken; - else throw new Error(`No native token metadata for ${chain}`); - } else if ( - config.type === TokenType.collateralVault || - config.type === TokenType.collateral - ) { - // If it's a collateral type, use a TokenAdapter to query for its metadata - log(`Fetching token metadata for ${config.token} on ${chain}`); - const adapter = new EvmTokenAdapter( - chain, - MultiProtocolProvider.fromMultiProvider(multiProvider), - { token: config.token }, - ); - return adapter.getMetadata(); - } else { - throw new Error( - `Unsupported token: ${config.type}. Consider setting token metadata in your deployment config.`, - ); - } -} - -function getTokenName(token: TokenConfig) { - return token.type === TokenType.native ? 'native' : token.name; -} - -function getWarpCoreConfig( - { configMap, metadata }: DeployParams, +async function getWarpCoreConfig( + { configMap, context }: DeployParams, contracts: HyperlaneContractsMap, -): WarpCoreConfig { +): Promise { const warpCoreConfig: WarpCoreConfig = { tokens: [] }; + // TODO: replace with warp read + const tokenMetadata = await HypERC20Deployer.deriveTokenMetadata( + context.multiProvider, + configMap, + ); + // First pass, create token configs for (const [chainName, contract] of Object.entries(contracts)) { const config = configMap[chainName]; + const metadata = { + ...tokenMetadata, + ...config, + }; + + if (!isTokenMetadata(metadata)) { + throw new Error('Missing required token metadata'); + } + + const { decimals } = metadata; + if (!decimals) { + throw new Error('Missing decimals on token metadata'); + } + const collateralAddressOrDenom = config.type === TokenType.collateral ? config.token : undefined; warpCoreConfig.tokens.push({ chainName, standard: TOKEN_TYPE_TO_STANDARD[config.type], - name: metadata.name, - symbol: metadata.symbol, - decimals: metadata.decimals, + ...metadata, + decimals, addressOrDenom: contract[configMap[chainName].type as keyof TokenFactories].address, collateralAddressOrDenom, diff --git a/typescript/cli/src/send/message.ts b/typescript/cli/src/send/message.ts index e48e712376..102592bbc8 100644 --- a/typescript/cli/src/send/message.ts +++ b/typescript/cli/src/send/message.ts @@ -5,7 +5,7 @@ import { addressToBytes32, timeout } from '@hyperlane-xyz/utils'; import { MINIMUM_TEST_SEND_GAS } from '../consts.js'; import { CommandContext, WriteCommandContext } from '../context/types.js'; -import { runPreflightChecks } from '../deploy/utils.js'; +import { runPreflightChecksForChains } from '../deploy/utils.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; import { runSingleChainSelectionStep } from '../utils/chains.js'; @@ -42,12 +42,11 @@ export async function sendTestMessage({ ); } - await runPreflightChecks({ + await runPreflightChecksForChains({ context, - origin, - remotes: [destination], - minGas: MINIMUM_TEST_SEND_GAS, + chains: [origin, destination], chainsToGasCheck: [origin], + minGas: MINIMUM_TEST_SEND_GAS, }); await timeout( diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index dcd3aa6fd7..23cd5ba521 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -13,7 +13,7 @@ import { timeout } from '@hyperlane-xyz/utils'; import { readWarpRouteConfig } from '../config/warp.js'; import { MINIMUM_TEST_SEND_GAS } from '../consts.js'; import { WriteCommandContext } from '../context/types.js'; -import { runPreflightChecks } from '../deploy/utils.js'; +import { runPreflightChecksForChains } from '../deploy/utils.js'; import { logBlue, logGreen, logRed } from '../logger.js'; import { runSingleChainSelectionStep } from '../utils/chains.js'; import { runTokenSelectionStep } from '../utils/tokens.js'; @@ -57,12 +57,11 @@ export async function sendTestTransfer({ ); } - await runPreflightChecks({ + await runPreflightChecksForChains({ context, - origin, - remotes: [destination], - minGas: MINIMUM_TEST_SEND_GAS, + chains: [origin, destination], chainsToGasCheck: [origin], + minGas: MINIMUM_TEST_SEND_GAS, }); await timeout( diff --git a/typescript/cli/src/tests/deployTestErc20.ts b/typescript/cli/src/tests/deployTestErc20.ts index 517668a599..be49a13daf 100644 --- a/typescript/cli/src/tests/deployTestErc20.ts +++ b/typescript/cli/src/tests/deployTestErc20.ts @@ -2,7 +2,7 @@ import { Wallet, providers } from 'ethers'; import fs from 'fs'; import { ERC20Test__factory } from '@hyperlane-xyz/core'; -import { TokenType, WarpRouteDeployConfig } from '@hyperlane-xyz/sdk'; +import { TokenType } from '@hyperlane-xyz/sdk'; async function deployERC20() { const [rpcUrl, chain1, chain2, privateKey, outPath] = process.argv.slice(2); @@ -19,13 +19,14 @@ async function deployERC20() { await contract.deployed(); console.log('Test ERC20 contract deployed', contract.address); - const warpDeploymentConfig: WarpRouteDeployConfig = { + const warpDeploymentConfig = { [chain1]: { type: TokenType.collateral, token: contract.address, - isNft: false, }, - [chain2]: { type: TokenType.synthetic }, + [chain2]: { + type: TokenType.synthetic, + }, }; console.log('Writing deployment config to', outPath); diff --git a/typescript/cli/src/utils/chains.ts b/typescript/cli/src/utils/chains.ts index 470f0a20da..3c0e7477d6 100644 --- a/typescript/cli/src/utils/chains.ts +++ b/typescript/cli/src/utils/chains.ts @@ -75,18 +75,20 @@ function handleNewChain(chainNames: string[]) { } export async function detectAndConfirmOrPrompt( - detect: () => Promise, - label: string, + detect: () => Promise, prompt: string, + label: string, ): Promise { let detectedValue: string | undefined; try { detectedValue = await detect(); - const confirmed = await confirm({ - message: `Detected ${label} as ${detectedValue}, is this correct?`, - }); - if (confirmed) { - return detectedValue; + if (detectedValue) { + const confirmed = await confirm({ + message: `Detected ${label} as ${detectedValue}, is this correct?`, + }); + if (confirmed) { + return detectedValue; + } } // eslint-disable-next-line no-empty } catch (e) {} diff --git a/typescript/infra/config/warp.ts b/typescript/infra/config/warp.ts index a878cfc247..43fe0beda1 100644 --- a/typescript/infra/config/warp.ts +++ b/typescript/infra/config/warp.ts @@ -4,8 +4,7 @@ import { ChainMap, HyperlaneIsmFactory, MultiProvider, - RouterConfig, - TokenConfig, + TokenRouterConfig, TokenType, buildAggregationIsmConfigs, defaultMultisigConfigs, @@ -21,7 +20,7 @@ import { DEPLOYER } from './environments/mainnet3/owners.js'; export async function getWarpConfig( multiProvider: MultiProvider, envConfig: EnvironmentConfig, -): Promise> { +): Promise> { const { core } = await getHyperlaneCore(envConfig.environment, multiProvider); const ismFactory = HyperlaneIsmFactory.fromAddressesMap( getAddresses(envConfig.environment, Modules.PROXY_FACTORY), @@ -43,7 +42,7 @@ export async function getWarpConfig( const routerConfig = core.getRouterConfig(envConfig.owners); - const ethereum: TokenConfig & RouterConfig = { + const ethereum: TokenRouterConfig = { ...routerConfig.ethereum, type: TokenType.collateral, token: tokens.ethereum.USDC, @@ -59,7 +58,7 @@ export async function getWarpConfig( // TokenMetadata, but in practice these are actually inferred from a // collateral config. To avoid needing to specify the TokenMetadata, just // ts-ignore for synthetic tokens. - const ancient8: TokenConfig & RouterConfig = { + const ancient8: TokenRouterConfig = { ...routerConfig.ancient8, type: TokenType.synthetic, // Uses the default ISM diff --git a/typescript/infra/src/govern/HyperlaneAppGovernor.ts b/typescript/infra/src/govern/HyperlaneAppGovernor.ts index a2449ec434..cfe2243a87 100644 --- a/typescript/infra/src/govern/HyperlaneAppGovernor.ts +++ b/typescript/infra/src/govern/HyperlaneAppGovernor.ts @@ -3,7 +3,6 @@ import prompts from 'prompts'; import { Ownable__factory } from '@hyperlane-xyz/core'; import { - AccountConfig, ChainMap, ChainName, HyperlaneApp, @@ -137,12 +136,7 @@ export abstract class HyperlaneAppGovernor< SubmissionType.SIGNER, new SignerMultiSend(this.checker.multiProvider, chain), ); - let safeOwner: Address; - if (typeof this.checker.configMap[chain].owner === 'string') { - safeOwner = this.checker.configMap[chain].owner as Address; - } else { - safeOwner = (this.checker.configMap[chain].owner as AccountConfig).owner; - } + const safeOwner = this.checker.configMap[chain].owner; await sendCallsForType( SubmissionType.SAFE, new SafeMultiSend(this.checker.multiProvider, chain, safeOwner), diff --git a/typescript/infra/test/govern.hardhat-test.ts b/typescript/infra/test/govern.hardhat-test.ts index 07f94c0cef..e5852d1f3b 100644 --- a/typescript/infra/test/govern.hardhat-test.ts +++ b/typescript/infra/test/govern.hardhat-test.ts @@ -134,6 +134,12 @@ describe('ICA governance', async () => { localRouter: remote.address, }; + accountOwner = await resolveOrDeployAccountOwner( + multiProvider, + remoteChain, + accountConfig, + ); + const recipientF = new TestRecipient__factory(signer); recipient = await recipientF.deploy(); @@ -145,23 +151,15 @@ describe('ICA governance', async () => { recipient, }, }; - // missing ica const configMap = { [localChain]: { owner: signer.address }, - [remoteChain]: { - owner: { origin: TestChainName.test1, owner: signer.address }, - }, + [remoteChain]: { owner: accountOwner }, }; const app = new TestApp(contractsMap, multiProvider); const checker = new TestChecker(multiProvider, app, configMap); governor = new HyperlaneTestGovernor(checker, icaApp); - accountOwner = await resolveOrDeployAccountOwner( - multiProvider, - remoteChain, - accountConfig, - ); await recipient.transferOwnership(accountOwner); }); diff --git a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts index 9edcfe5f8b..6a06fa88f6 100644 --- a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts +++ b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts @@ -71,11 +71,6 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< proxyAdmin, [domain], ); - // resolve the owner account so that the subsequent calls terminate early - config.owner = await this.resolveInterchainAccountAsOwner( - chain, - config.owner, - ); let defaultIsm = await mailbox.defaultIsm(); const matches = await moduleMatchesConfig( @@ -113,15 +108,11 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< // configure mailbox try { - const owner = await this.resolveInterchainAccountAsOwner( - chain, - config.owner, - ); this.logger.debug('Initializing mailbox'); await this.multiProvider.handleTx( chain, mailbox.initialize( - owner, + config.owner, defaultIsm, defaultHook.address, requiredHook.address, diff --git a/typescript/sdk/src/deploy/HyperlaneDeployer.ts b/typescript/sdk/src/deploy/HyperlaneDeployer.ts index 0598d48072..d9fefa0e1e 100644 --- a/typescript/sdk/src/deploy/HyperlaneDeployer.ts +++ b/typescript/sdk/src/deploy/HyperlaneDeployer.ts @@ -43,7 +43,7 @@ import { proxyConstructorArgs, proxyImplementation, } from './proxy.js'; -import { OwnableConfig, Owner } from './types.js'; +import { OwnableConfig } from './types.js'; import { ContractVerifier } from './verify/ContractVerifier.js'; import { ContractVerificationInput, @@ -732,10 +732,7 @@ export abstract class HyperlaneDeployer< continue; } const current = await ownable.owner(); - const owner = await this.resolveInterchainAccountAsOwner( - chain, - config.ownerOverrides?.[contractName as K] ?? config.owner, - ); + const owner = config.ownerOverrides?.[contractName as K] ?? config.owner; if (!eqAddress(current, owner)) { this.logger.debug( { contractName, current, desiredOwner: owner }, @@ -759,24 +756,4 @@ export abstract class HyperlaneDeployer< return receipts.filter((x) => !!x) as ethers.ContractReceipt[]; } - - protected async resolveInterchainAccountAsOwner( - chain: ChainName, - owner: Owner, - ): Promise
{ - if (typeof owner === 'string') { - return owner; - } else { - const routerAddress = this.options.icaApp?.routerAddress(chain); - if (!routerAddress) { - throw new Error('InterchainAccountRouter not deployed'); - } - const router = InterchainAccount.fromAddressesMap( - { chain: { router: routerAddress } }, - this.multiProvider, - ); - // submits network transaction to deploy the account iff it doesn't exist - return router.deployAccount(chain, owner); - } - } } diff --git a/typescript/sdk/src/deploy/schemas.ts b/typescript/sdk/src/deploy/schemas.ts index 7d965f0bc1..99b1066525 100644 --- a/typescript/sdk/src/deploy/schemas.ts +++ b/typescript/sdk/src/deploy/schemas.ts @@ -1,8 +1,6 @@ import { z } from 'zod'; -import { AccountConfigSchema } from '../middleware/account/schemas.js'; - -export const OwnerSchema = z.union([z.string(), AccountConfigSchema]); +export const OwnerSchema = z.string(); export const OwnableConfigSchema = z.object({ owner: OwnerSchema, diff --git a/typescript/sdk/src/deploy/types.ts b/typescript/sdk/src/deploy/types.ts index 084979d0e5..5c74845a4c 100644 --- a/typescript/sdk/src/deploy/types.ts +++ b/typescript/sdk/src/deploy/types.ts @@ -39,7 +39,7 @@ export async function resolveOrDeployAccountOwner( throw new Error('localRouter is required for AccountConfig'); } // submits a transaction to deploy an interchain account if the owner is an AccountConfig and the ICA isn't not deployed yet - return await deployInterchainAccount(multiProvider, chain, owner); + return deployInterchainAccount(multiProvider, chain, owner); } } diff --git a/typescript/sdk/src/hook/HyperlaneHookDeployer.ts b/typescript/sdk/src/hook/HyperlaneHookDeployer.ts index a426e079bb..d1877c9309 100644 --- a/typescript/sdk/src/hook/HyperlaneHookDeployer.ts +++ b/typescript/sdk/src/hook/HyperlaneHookDeployer.ts @@ -115,7 +115,7 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer< config.maxProtocolFee, config.protocolFee, config.beneficiary, - await this.resolveInterchainAccountAsOwner(chain, config.owner), + config.owner, ]); } diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index fd338351e7..aceb6509ed 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -340,8 +340,6 @@ export { MailboxClientConfig as ConnectionClientConfig, ClientViolation as ConnectionClientViolation, ClientViolationType as ConnectionClientViolationType, - ForeignDeploymentConfig, - GasConfig, GasRouterConfig, MailboxClientConfig, ProxiedFactories, @@ -419,27 +417,7 @@ export { } from './token/adapters/serialization.js'; export { HypERC20App } from './token/app.js'; export { HypERC20Checker } from './token/checker.js'; -export { - CollateralConfig, - ERC20Metadata, - ERC20RouterConfig, - ERC721RouterConfig, - HypERC20CollateralConfig, - HypERC20Config, - HypERC721CollateralConfig, - HypERC721Config, - HypNativeConfig, - MinimalTokenMetadata, - NativeConfig, - SyntheticConfig, - TokenConfig, - TokenMetadata, - TokenType, - isCollateralConfig, - isNativeConfig, - isSyntheticConfig, - isUriConfig, -} from './token/config.js'; +export { TokenType } from './token/config.js'; export { HypERC20Factories, HypERC721Factories, @@ -480,8 +458,15 @@ export { AggregationIsmConfigSchema } from './ism/schemas.js'; export { MailboxClientConfigSchema as mailboxClientConfigSchema } from './router/schemas.js'; export { WarpRouteDeployConfigSchema, - TokenRouterConfigSchema as tokenRouterConfigSchema, + TokenRouterConfigSchema, + CollateralConfig, + NativeConfig, + isCollateralConfig, + isNativeConfig, + isSyntheticConfig, + isTokenMetadata, } from './token/schemas.js'; +export { isCompliant } from './utils/schemas.js'; export { TokenRouterConfig, WarpRouteDeployConfig } from './token/types.js'; // prettier-ignore diff --git a/typescript/sdk/src/ism/schemas.test.ts b/typescript/sdk/src/ism/schemas.test.ts index db44109034..7605382c24 100644 --- a/typescript/sdk/src/ism/schemas.test.ts +++ b/typescript/sdk/src/ism/schemas.test.ts @@ -1,8 +1,7 @@ import { expect } from 'chai'; import { ethers } from 'ethers'; -import { AggregationIsmConfigSchema } from '@hyperlane-xyz/sdk'; - +import { AggregationIsmConfigSchema } from './schemas.js'; import { IsmType } from './types.js'; const SOME_ADDRESS = ethers.Wallet.createRandom().address; diff --git a/typescript/sdk/src/ism/schemas.ts b/typescript/sdk/src/ism/schemas.ts index f07df3f256..c449e0e42e 100644 --- a/typescript/sdk/src/ism/schemas.ts +++ b/typescript/sdk/src/ism/schemas.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { OwnableConfigSchema } from '../deploy/schemas.js'; -import { ZHash } from '../index.js'; +import { ZHash } from '../metadata/customZodTypes.js'; import { AggregationIsmConfig, IsmConfig, IsmType } from './types.js'; diff --git a/typescript/sdk/src/middleware/account/schemas.ts b/typescript/sdk/src/middleware/account/schemas.ts index fd0e91d2ce..5b95b13d15 100644 --- a/typescript/sdk/src/middleware/account/schemas.ts +++ b/typescript/sdk/src/middleware/account/schemas.ts @@ -1,7 +1,6 @@ import { z } from 'zod'; -import { ZHash } from '../../index.js'; -import { ZChainName } from '../../metadata/customZodTypes.js'; +import { ZChainName, ZHash } from '../../metadata/customZodTypes.js'; export const AccountConfigSchema = z.object({ origin: ZChainName, diff --git a/typescript/sdk/src/router/GasRouterDeployer.ts b/typescript/sdk/src/router/GasRouterDeployer.ts index 8c7d0a52e6..2c929123a4 100644 --- a/typescript/sdk/src/router/GasRouterDeployer.ts +++ b/typescript/sdk/src/router/GasRouterDeployer.ts @@ -4,15 +4,18 @@ import { Address } from '@hyperlane-xyz/utils'; import { HyperlaneContracts, HyperlaneContractsMap, + HyperlaneFactories, } from '../contracts/types.js'; import { ChainMap } from '../types.js'; import { ProxiedRouterDeployer } from './ProxiedRouterDeployer.js'; -import { GasRouterConfig, ProxiedFactories } from './types.js'; +import { GasRouterConfig } from './types.js'; + +const DEFAULT_GAS_OVERHEAD = 100_000; export abstract class GasRouterDeployer< Config extends GasRouterConfig, - Factories extends ProxiedFactories, + Factories extends HyperlaneFactories, > extends ProxiedRouterDeployer { abstract router(contracts: HyperlaneContracts): GasRouter; @@ -37,7 +40,7 @@ export abstract class GasRouterDeployer< const remoteConfigs = remoteDomains .map((domain, i) => ({ domain, - gas: configMap[remoteChains[i]].gas, + gas: configMap[remoteChains[i]].gas ?? DEFAULT_GAS_OVERHEAD, })) .filter(({ gas }, index) => !currentConfigs[index].eq(gas)); if (remoteConfigs.length == 0) { diff --git a/typescript/sdk/src/router/ProxiedRouterDeployer.ts b/typescript/sdk/src/router/ProxiedRouterDeployer.ts index f8a5608045..335a421e27 100644 --- a/typescript/sdk/src/router/ProxiedRouterDeployer.ts +++ b/typescript/sdk/src/router/ProxiedRouterDeployer.ts @@ -7,18 +7,41 @@ import { } from '@hyperlane-xyz/core'; import { eqAddress } from '@hyperlane-xyz/utils'; -import { HyperlaneContracts } from '../contracts/types.js'; +import { HyperlaneContracts, HyperlaneFactories } from '../contracts/types.js'; +import { DeployerOptions } from '../deploy/HyperlaneDeployer.js'; import { resolveOrDeployAccountOwner } from '../deploy/types.js'; +import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainName } from '../types.js'; import { HyperlaneRouterDeployer } from './HyperlaneRouterDeployer.js'; -import { ProxiedFactories, ProxiedRouterConfig } from './types.js'; +import { + ProxiedFactories, + ProxiedRouterConfig, + proxiedFactories, +} from './types.js'; export abstract class ProxiedRouterDeployer< Config extends ProxiedRouterConfig, - Factories extends ProxiedFactories, -> extends HyperlaneRouterDeployer { - abstract router(contracts: HyperlaneContracts): Router; + Factories extends HyperlaneFactories, +> extends HyperlaneRouterDeployer { + constructor( + multiProvider: MultiProvider, + factories: Factories, + options?: DeployerOptions, + ) { + super( + multiProvider, + { + ...factories, + ...proxiedFactories, + }, + options, + ); + } + + abstract router( + contracts: HyperlaneContracts, + ): Router; /** * Returns the contract name @@ -59,7 +82,7 @@ export abstract class ProxiedRouterDeployer< async deployContracts( chain: ChainName, config: Config, - ): Promise> { + ): Promise> { const proxyAdmin = await this.deployContractFromFactory( chain, this.factories.proxyAdmin, @@ -112,6 +135,6 @@ export abstract class ProxiedRouterDeployer< [this.routerContractKey(config)]: proxiedRouter, proxyAdmin, timelockController, - } as HyperlaneContracts; + } as HyperlaneContracts; } } diff --git a/typescript/sdk/src/router/schemas.ts b/typescript/sdk/src/router/schemas.ts index e186c4e1cf..6c6ffa0311 100644 --- a/typescript/sdk/src/router/schemas.ts +++ b/typescript/sdk/src/router/schemas.ts @@ -1,21 +1,23 @@ import { z } from 'zod'; import { OwnableConfigSchema } from '../deploy/schemas.js'; -import { ZHash } from '../index.js'; import { IsmConfigSchema } from '../ism/schemas.js'; +import { ZHash } from '../metadata/customZodTypes.js'; -export const ForeignDeploymentConfigSchema = z.object({ - foreignDeployment: z.string().optional(), -}); - -export const MailboxClientConfigSchema = z.object({ +export const MailboxClientConfigSchema = OwnableConfigSchema.extend({ mailbox: ZHash, hook: ZHash.optional(), interchainSecurityModule: IsmConfigSchema.optional(), }); -export const routerConfigSchema = MailboxClientConfigSchema.merge( - OwnableConfigSchema, -) - .merge(ForeignDeploymentConfigSchema) - .deepPartial(); +export const ForeignDeploymentConfigSchema = z.object({ + foreignDeployment: z.string().optional(), +}); + +export const RouterConfigSchema = MailboxClientConfigSchema.merge( + ForeignDeploymentConfigSchema, +); + +export const GasRouterConfigSchema = RouterConfigSchema.extend({ + gas: z.number().optional(), +}); diff --git a/typescript/sdk/src/router/types.ts b/typescript/sdk/src/router/types.ts index f1c7ab4480..1d4202e05b 100644 --- a/typescript/sdk/src/router/types.ts +++ b/typescript/sdk/src/router/types.ts @@ -6,37 +6,27 @@ import { Router, TimelockController__factory, } from '@hyperlane-xyz/core'; +import { Address } from '@hyperlane-xyz/utils'; -import type { Address } from '../../../utils/dist/index.js'; import { HyperlaneFactories } from '../contracts/types.js'; import { UpgradeConfig } from '../deploy/proxy.js'; -import { CheckerViolation, OwnableConfig } from '../deploy/types.js'; +import { CheckerViolation } from '../deploy/types.js'; import { - ForeignDeploymentConfigSchema, + GasRouterConfigSchema, MailboxClientConfigSchema, + RouterConfigSchema, } from './schemas.js'; export type RouterAddress = { router: Address; }; -export type ForeignDeploymentConfig = z.infer< - typeof ForeignDeploymentConfigSchema ->; - -export type RouterConfig = MailboxClientConfig & - OwnableConfig & - ForeignDeploymentConfig; +export type MailboxClientConfig = z.infer; +export type RouterConfig = z.infer; +export type GasRouterConfig = z.infer; export type ProxiedRouterConfig = RouterConfig & Partial; - -export type GasConfig = { - gas: number; -}; - -export type GasRouterConfig = RouterConfig & GasConfig; - export type ProxiedFactories = HyperlaneFactories & { proxyAdmin: ProxyAdmin__factory; timelockController: TimelockController__factory; @@ -47,10 +37,6 @@ export const proxiedFactories: ProxiedFactories = { timelockController: new TimelockController__factory(), }; -// TODO: merge with kunal's hook deployer - -export type MailboxClientConfig = z.infer; - export enum ClientViolationType { InterchainSecurityModule = 'ClientIsm', Mailbox = 'ClientMailbox', diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts index 0699fe5c45..d62285705a 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts @@ -3,24 +3,24 @@ import { ethers, providers } from 'ethers'; import { ERC20__factory, HypERC20Collateral__factory, + MailboxClient__factory, } from '@hyperlane-xyz/core'; -import { ERC20Metadata, ERC20RouterConfig } from '@hyperlane-xyz/sdk'; -import { Address } from '@hyperlane-xyz/utils'; +import { Address, eqAddress } from '@hyperlane-xyz/utils'; import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js'; import { EvmHookReader } from '../hook/EvmHookReader.js'; import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { MultiProvider } from '../providers/MultiProvider.js'; +import { MailboxClientConfig } from '../router/types.js'; import { ChainName } from '../types.js'; -type WarpRouteBaseMetadata = Record< - 'mailbox' | 'owner' | 'token' | 'hook' | 'interchainSecurityModule', - string ->; +import { TokenType } from './config.js'; +import { TokenRouterConfig } from './schemas.js'; +import { TokenMetadata } from './types.js'; -type DerivedERC20WarpRouteConfig = Omit; +const { AddressZero } = ethers.constants; -export class EvmERC20WarpRouteReader { +export class EvmWarpRouteReader { provider: providers.Provider; evmHookReader: EvmHookReader; evmIsmReader: EvmIsmReader; @@ -44,34 +44,32 @@ export class EvmERC20WarpRouteReader { */ async deriveWarpRouteConfig( address: Address, - ): Promise { - const fetchedBaseMetadata = await this.fetchBaseMetadata(address); - const fetchedTokenMetadata = await this.fetchTokenMetadata( - fetchedBaseMetadata.token, - ); - - const results: DerivedERC20WarpRouteConfig = { - ...fetchedBaseMetadata, - ...fetchedTokenMetadata, - }; + type = TokenType.collateral, + ): Promise { + const mailboxClientConfig = await this.fetchMailboxClientConfig(address); - if ( - fetchedBaseMetadata.interchainSecurityModule !== - ethers.constants.AddressZero - ) { - results.interchainSecurityModule = - await this.evmIsmReader.deriveIsmConfig( - fetchedBaseMetadata.interchainSecurityModule, - ); + let token: Address; + switch (type) { + case TokenType.collateral: + token = await HypERC20Collateral__factory.connect( + address, + this.provider, + ).wrappedToken(); + break; + case TokenType.synthetic: + token = address; + break; + default: + throw new Error(`Invalid token type: ${type}`); } - // @todo add after https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3667 is fixed - // if (fetchedBaseMetadata.hook !== ethers.constants.AddressZero) { - // results.hook = await this.evmHookReader.deriveHookConfig( - // fetchedBaseMetadata.hook, - // ); - // } + const fetchedTokenMetadata = await this.fetchTokenMetadata(token); - return results; + return { + type, + token: TokenType.collateral === type ? token : undefined, + ...mailboxClientConfig, + ...fetchedTokenMetadata, + } as TokenRouterConfig; } /** @@ -80,28 +78,31 @@ export class EvmERC20WarpRouteReader { * @param routerAddress - The address of the Warp Route contract. * @returns The base metadata for the Warp Route contract, including the mailbox, owner, wrapped token address, hook, and interchain security module. */ - async fetchBaseMetadata( + async fetchMailboxClientConfig( routerAddress: Address, - ): Promise { - const warpRoute = HypERC20Collateral__factory.connect( + ): Promise { + const warpRoute = MailboxClient__factory.connect( routerAddress, this.provider, ); - const [mailbox, owner, token, hook, interchainSecurityModule] = - await Promise.all([ - warpRoute.mailbox(), - warpRoute.owner(), - warpRoute.wrappedToken(), - warpRoute.hook(), - warpRoute.interchainSecurityModule(), - ]); + const [mailbox, owner, hook, ism] = await Promise.all([ + warpRoute.mailbox(), + warpRoute.owner(), + warpRoute.hook(), + warpRoute.interchainSecurityModule(), + ]); + + const derivedIsm = eqAddress(ism, AddressZero) + ? undefined + : await this.evmIsmReader.deriveIsmConfig(ism); + // TODO: add after https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3667 is fixed + const derivedHook = eqAddress(hook, AddressZero) ? undefined : hook; return { mailbox, owner, - token, - hook, - interchainSecurityModule, + hook: derivedHook, + interchainSecurityModule: derivedIsm, }; } @@ -111,7 +112,7 @@ export class EvmERC20WarpRouteReader { * @param tokenAddress - The address of the token. * @returns A partial ERC20 metadata object containing the token name, symbol, total supply, and decimals. */ - async fetchTokenMetadata(tokenAddress: Address): Promise { + async fetchTokenMetadata(tokenAddress: Address): Promise { const erc20 = ERC20__factory.connect(tokenAddress, this.provider); const [name, symbol, totalSupply, decimals] = await Promise.all([ erc20.name(), @@ -120,6 +121,6 @@ export class EvmERC20WarpRouteReader { erc20.decimals(), ]); - return { name, symbol, totalSupply, decimals }; + return { name, symbol, totalSupply: totalSupply.toString(), decimals }; } } diff --git a/typescript/sdk/src/token/adapters/CosmWasmTokenAdapter.ts b/typescript/sdk/src/token/adapters/CosmWasmTokenAdapter.ts index 9ee5fba389..e1b476520d 100644 --- a/typescript/sdk/src/token/adapters/CosmWasmTokenAdapter.ts +++ b/typescript/sdk/src/token/adapters/CosmWasmTokenAdapter.ts @@ -30,7 +30,7 @@ import { } from '../../cw-types/WarpCw20.types.js'; import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider.js'; import { ChainName } from '../../types.js'; -import { ERC20Metadata } from '../config.js'; +import { TokenMetadata } from '../types.js'; import { IHypTokenAdapter, @@ -92,7 +92,7 @@ export class CwNativeTokenAdapter } } -export type CW20Metadata = ERC20Metadata; +export type CW20Metadata = TokenMetadata; type CW20Response = TokenInfoResponse | BalanceResponse; // Interacts with CW20/721 contracts diff --git a/typescript/sdk/src/token/adapters/CosmosTokenAdapter.ts b/typescript/sdk/src/token/adapters/CosmosTokenAdapter.ts index de0bfd2ee7..ea85d05aa9 100644 --- a/typescript/sdk/src/token/adapters/CosmosTokenAdapter.ts +++ b/typescript/sdk/src/token/adapters/CosmosTokenAdapter.ts @@ -6,7 +6,7 @@ import { Address, Domain, assert } from '@hyperlane-xyz/utils'; import { BaseCosmosAdapter } from '../../app/MultiProtocolApp.js'; import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider.js'; import { ChainName } from '../../types.js'; -import { MinimalTokenMetadata } from '../config.js'; +import { TokenMetadata } from '../types.js'; import { CwHypCollateralAdapter } from './CosmWasmTokenAdapter.js'; import { @@ -43,7 +43,7 @@ export class CosmNativeTokenAdapter return BigInt(coin.amount); } - getMetadata(): Promise { + getMetadata(): Promise { throw new Error('Metadata not available to native tokens'); } diff --git a/typescript/sdk/src/token/adapters/EvmTokenAdapter.ts b/typescript/sdk/src/token/adapters/EvmTokenAdapter.ts index febbd57a90..3fc25ade31 100644 --- a/typescript/sdk/src/token/adapters/EvmTokenAdapter.ts +++ b/typescript/sdk/src/token/adapters/EvmTokenAdapter.ts @@ -21,7 +21,7 @@ import { import { BaseEvmAdapter } from '../../app/MultiProtocolApp.js'; import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider.js'; import { ChainName } from '../../types.js'; -import { MinimalTokenMetadata } from '../config.js'; +import { TokenMetadata } from '../types.js'; import { IHypTokenAdapter, @@ -45,7 +45,7 @@ export class EvmNativeTokenAdapter return BigInt(balance.toString()); } - async getMetadata(): Promise { + async getMetadata(): Promise { // TODO get metadata from chainMetadata config throw new Error('Metadata not available to native tokens'); } @@ -98,13 +98,14 @@ export class EvmTokenAdapter return BigInt(balance.toString()); } - override async getMetadata(isNft?: boolean): Promise { - const [decimals, symbol, name] = await Promise.all([ + override async getMetadata(isNft?: boolean): Promise { + const [decimals, symbol, name, totalSupply] = await Promise.all([ isNft ? 0 : this.contract.decimals(), this.contract.symbol(), this.contract.name(), + this.contract.totalSupply(), ]); - return { decimals, symbol, name }; + return { decimals, symbol, name, totalSupply: totalSupply.toString() }; } override async isApproveRequired( @@ -247,7 +248,7 @@ export class EvmHypCollateralAdapter }); } - override getMetadata(isNft?: boolean): Promise { + override getMetadata(isNft?: boolean): Promise { return this.getWrappedTokenAdapter().then((t) => t.getMetadata(isNft)); } diff --git a/typescript/sdk/src/token/adapters/ITokenAdapter.ts b/typescript/sdk/src/token/adapters/ITokenAdapter.ts index f5dfd906a4..67bd1a0f4f 100644 --- a/typescript/sdk/src/token/adapters/ITokenAdapter.ts +++ b/typescript/sdk/src/token/adapters/ITokenAdapter.ts @@ -1,6 +1,6 @@ import { Address, Domain, Numberish } from '@hyperlane-xyz/utils'; -import { MinimalTokenMetadata } from '../config.js'; +import { TokenMetadata } from '../types.js'; export interface TransferParams { weiAmountOrId: Numberish; @@ -23,7 +23,7 @@ export interface InterchainGasQuote { export interface ITokenAdapter { getBalance(address: Address): Promise; - getMetadata(isNft?: boolean): Promise; + getMetadata(isNft?: boolean): Promise; isApproveRequired( owner: Address, spender: Address, diff --git a/typescript/sdk/src/token/adapters/SealevelTokenAdapter.ts b/typescript/sdk/src/token/adapters/SealevelTokenAdapter.ts index a924bc92c1..5cae1811f2 100644 --- a/typescript/sdk/src/token/adapters/SealevelTokenAdapter.ts +++ b/typescript/sdk/src/token/adapters/SealevelTokenAdapter.ts @@ -33,7 +33,7 @@ import { SealevelAccountDataWrapper, SealevelInstructionWrapper, } from '../../utils/sealevelSerialization.js'; -import { MinimalTokenMetadata } from '../config.js'; +import { TokenMetadata } from '../types.js'; import { IHypTokenAdapter, @@ -61,7 +61,7 @@ export class SealevelNativeTokenAdapter return BigInt(balance.toString()); } - async getMetadata(): Promise { + async getMetadata(): Promise { throw new Error('Metadata not available to native tokens'); } @@ -115,9 +115,9 @@ export class SealevelTokenAdapter return BigInt(response.value.amount); } - async getMetadata(_isNft?: boolean): Promise { + async getMetadata(_isNft?: boolean): Promise { // TODO solana support - return { decimals: 9, symbol: 'SPL', name: 'SPL Token' }; + return { decimals: 9, symbol: 'SPL', name: 'SPL Token', totalSupply: '' }; } async isApproveRequired(): Promise { @@ -212,11 +212,12 @@ export abstract class SealevelHypTokenAdapter return this.cachedTokenAccountData; } - override async getMetadata(): Promise { + override async getMetadata(): Promise { const tokenData = await this.getTokenAccountData(); // TODO full token metadata support return { decimals: tokenData.decimals, + totalSupply: '0', symbol: 'HYP', name: 'Unknown Hyp Token', }; @@ -506,7 +507,7 @@ export class SealevelHypNativeAdapter extends SealevelHypTokenAdapter { return this.wrappedNative.getBalance(owner); } - override async getMetadata(): Promise { + override async getMetadata(): Promise { return this.wrappedNative.getMetadata(); } diff --git a/typescript/sdk/src/token/app.ts b/typescript/sdk/src/token/app.ts index 9ea8141fa5..0eeb41dbca 100644 --- a/typescript/sdk/src/token/app.ts +++ b/typescript/sdk/src/token/app.ts @@ -10,11 +10,7 @@ import { import { MultiProvider } from '../providers/MultiProvider.js'; import { GasRouterApp } from '../router/RouterApps.js'; -import { - HypERC20Factories, - hypERC20Tokenfactories, - hypERC20factories, -} from './contracts.js'; +import { HypERC20Factories, hypERC20factories } from './contracts.js'; export class HypERC20App extends GasRouterApp { constructor( @@ -25,7 +21,7 @@ export class HypERC20App extends GasRouterApp { } router(contracts: HyperlaneContracts): TokenRouter { - for (const key of objKeys(hypERC20Tokenfactories)) { + for (const key of objKeys(hypERC20factories)) { if (contracts[key]) { return contracts[key] as unknown as TokenRouter; } diff --git a/typescript/sdk/src/token/checker.ts b/typescript/sdk/src/token/checker.ts index 9b0e366f8e..48b38841fb 100644 --- a/typescript/sdk/src/token/checker.ts +++ b/typescript/sdk/src/token/checker.ts @@ -8,20 +8,19 @@ import { HyperlaneRouterChecker } from '../router/HyperlaneRouterChecker.js'; import { ChainName } from '../types.js'; import { HypERC20App } from './app.js'; +import { HypERC20Factories } from './contracts.js'; import { - ERC20RouterConfig, - HypERC20Config, - TokenMetadata, + TokenRouterConfig, isCollateralConfig, isNativeConfig, isSyntheticConfig, -} from './config.js'; -import { HypERC20Factories } from './contracts.js'; +} from './schemas.js'; +import { TokenMetadata } from './types.js'; export class HypERC20Checker extends HyperlaneRouterChecker< HypERC20Factories, HypERC20App, - ERC20RouterConfig + TokenRouterConfig > { async checkChain(chain: ChainName): Promise { await super.checkChain(chain); @@ -31,10 +30,10 @@ export class HypERC20Checker extends HyperlaneRouterChecker< async checkToken(chain: ChainName): Promise { const checkERC20 = async ( token: ERC20, - config: HypERC20Config, + config: TokenRouterConfig, ): Promise => { const checks: { - method: keyof TokenMetadata | 'decimals'; + method: keyof ERC20 & keyof TokenMetadata; violationType: string; }[] = [ { method: 'symbol', violationType: 'TokenSymbolMismatch' }, diff --git a/typescript/sdk/src/token/config.ts b/typescript/sdk/src/token/config.ts index e8d57a4a7d..59a2e597dc 100644 --- a/typescript/sdk/src/token/config.ts +++ b/typescript/sdk/src/token/config.ts @@ -1,10 +1,3 @@ -import { ethers } from 'ethers'; -import z from 'zod'; - -import { GasRouterConfig } from '../router/types.js'; - -import { SyntheticConfigSchema } from './schemas.js'; - export enum TokenType { synthetic = 'synthetic', fastSynthetic = 'fastSynthetic', @@ -19,86 +12,14 @@ export enum TokenType { nativeScaled = 'nativeScaled', } -export type TokenMetadata = { - name: string; - symbol: string; - totalSupply: ethers.BigNumberish; -}; - -export type TokenDecimals = { - decimals: number; - scale?: number; +export const gasOverhead = (tokenType: TokenType) => { + switch (tokenType) { + case TokenType.fastSynthetic: + case TokenType.synthetic: + return 64_000; + case TokenType.native: + return 44_000; + default: + return 68_000; + } }; - -export type ERC20Metadata = TokenMetadata & TokenDecimals; -export type MinimalTokenMetadata = Omit; - -export const isTokenMetadata = (metadata: any): metadata is TokenMetadata => - metadata.name && metadata.symbol && metadata.totalSupply !== undefined; // totalSupply can be 0 - -export const isErc20Metadata = (metadata: any): metadata is ERC20Metadata => - metadata.decimals && isTokenMetadata(metadata); - -export type SyntheticConfig = z.infer; -export type CollateralConfig = { - type: - | TokenType.collateral - | TokenType.collateralXERC20 - | TokenType.collateralFiat - | TokenType.collateralUri - | TokenType.fastCollateral - | TokenType.fastSynthetic - | TokenType.collateralVault; - token: string; -} & Partial; -export type NativeConfig = { - type: TokenType.native; -} & Partial; - -export type TokenConfig = SyntheticConfig | CollateralConfig | NativeConfig; - -export const isCollateralConfig = ( - config: TokenConfig, -): config is CollateralConfig => - config.type === TokenType.collateral || - config.type === TokenType.collateralXERC20 || - config.type === TokenType.collateralFiat || - config.type === TokenType.collateralUri || - config.type === TokenType.fastCollateral || - config.type == TokenType.collateralVault; - -export const isCollateralVaultConfig = ( - config: TokenConfig, -): config is CollateralConfig => config.type === TokenType.collateralVault; - -export const isSyntheticConfig = ( - config: TokenConfig, -): config is SyntheticConfig => - config.type === TokenType.synthetic || - config.type === TokenType.syntheticUri || - config.type === TokenType.fastSynthetic; - -export const isNativeConfig = (config: TokenConfig): config is NativeConfig => - config.type === TokenType.native; - -export const isUriConfig = (config: TokenConfig): boolean => - config.type === TokenType.syntheticUri || - config.type === TokenType.collateralUri; - -export const isFastConfig = (config: TokenConfig): boolean => - config.type === TokenType.fastSynthetic || - config.type === TokenType.fastCollateral; - -export type HypERC20Config = GasRouterConfig & SyntheticConfig & ERC20Metadata; -export type HypERC20CollateralConfig = GasRouterConfig & - CollateralConfig & - Partial; -export type HypNativeConfig = GasRouterConfig & NativeConfig; -export type ERC20RouterConfig = - | HypERC20Config - | HypERC20CollateralConfig - | HypNativeConfig; - -export type HypERC721Config = GasRouterConfig & SyntheticConfig; -export type HypERC721CollateralConfig = GasRouterConfig & CollateralConfig; -export type ERC721RouterConfig = HypERC721Config | HypERC721CollateralConfig; diff --git a/typescript/sdk/src/token/contracts.ts b/typescript/sdk/src/token/contracts.ts index 50ef36dbb0..57e45a7bd5 100644 --- a/typescript/sdk/src/token/contracts.ts +++ b/typescript/sdk/src/token/contracts.ts @@ -14,8 +14,6 @@ import { HypXERC20Collateral__factory, } from '@hyperlane-xyz/core'; -import { proxiedFactories } from '../router/types.js'; - import { TokenType } from './config.js'; export const hypERC20contracts = { @@ -31,7 +29,7 @@ export const hypERC20contracts = { }; export type HypERC20contracts = typeof hypERC20contracts; -export const hypERC20Tokenfactories = { +export const hypERC20factories = { [TokenType.fastCollateral]: new FastHypERC20Collateral__factory(), [TokenType.fastSynthetic]: new FastHypERC20__factory(), [TokenType.synthetic]: new HypERC20__factory(), @@ -42,11 +40,6 @@ export const hypERC20Tokenfactories = { [TokenType.native]: new HypNative__factory(), [TokenType.nativeScaled]: new HypNativeScaled__factory(), }; - -export const hypERC20factories = { - ...hypERC20Tokenfactories, - ...proxiedFactories, -}; export type HypERC20Factories = typeof hypERC20factories; export const hypERC721contracts = { @@ -63,7 +56,6 @@ export const hypERC721factories = { [TokenType.collateral]: new HypERC721Collateral__factory(), [TokenType.syntheticUri]: new HypERC721URIStorage__factory(), [TokenType.synthetic]: new HypERC721__factory(), - ...proxiedFactories, }; export type HypERC721Factories = typeof hypERC721factories; diff --git a/typescript/sdk/src/token/deploy.hardhat-test.ts b/typescript/sdk/src/token/deploy.hardhat-test.ts index 25ed9e50bf..49323191d1 100644 --- a/typescript/sdk/src/token/deploy.hardhat-test.ts +++ b/typescript/sdk/src/token/deploy.hardhat-test.ts @@ -2,38 +2,31 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js'; import { expect } from 'chai'; import hre from 'hardhat'; -import { - ERC20Test, - ERC20Test__factory, - Mailbox__factory, -} from '@hyperlane-xyz/core'; -import { RouterConfig, TestChainName } from '@hyperlane-xyz/sdk'; -import { objMap } from '@hyperlane-xyz/utils'; +import { ERC20Test__factory } from '@hyperlane-xyz/core'; +import { Address, objMap } from '@hyperlane-xyz/utils'; +import { TestChainName } from '../consts/testChains.js'; import { TestCoreApp } from '../core/TestCoreApp.js'; import { TestCoreDeployer } from '../core/TestCoreDeployer.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { MultiProvider } from '../providers/MultiProvider.js'; -import { ChainMap } from '../types.js'; -import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js'; -import { - HypERC20CollateralConfig, - HypERC20Config, - TokenConfig, - TokenType, -} from './config.js'; +import { EvmWarpRouteReader } from './EvmERC20WarpRouteReader.js'; +import { TokenType } from './config.js'; import { HypERC20Deployer } from './deploy.js'; +import { TokenRouterConfig } from './schemas.js'; import { WarpRouteDeployConfig } from './types.js'; +const chain = TestChainName.test1; + describe('TokenDeployer', async () => { let signer: SignerWithAddress; let deployer: HypERC20Deployer; let multiProvider: MultiProvider; let coreApp: TestCoreApp; - let routerConfigMap: ChainMap; let config: WarpRouteDeployConfig; + let token: Address; before(async () => { [signer] = await hre.ethers.getSigners(); @@ -44,19 +37,27 @@ describe('TokenDeployer', async () => { ); const ismFactory = new HyperlaneIsmFactory(factories, multiProvider); coreApp = await new TestCoreDeployer(multiProvider, ismFactory).deployApp(); - routerConfigMap = coreApp.getRouterConfig(signer.address); + const routerConfigMap = coreApp.getRouterConfig(signer.address); config = objMap( routerConfigMap, - (chain, c): HypERC20Config => ({ + (chain, c): TokenRouterConfig => ({ type: TokenType.synthetic, name: chain, symbol: `u${chain}`, decimals: 18, - totalSupply: 100_000, - gas: 65_000, + totalSupply: '100000', ...c, }), ); + + const { name, decimals, symbol, totalSupply } = config[chain]; + const contract = await new ERC20Test__factory(signer).deploy( + name!, + symbol!, + totalSupply!, + decimals!, + ); + token = contract.address; }); beforeEach(async () => { @@ -64,66 +65,36 @@ describe('TokenDeployer', async () => { }); it('deploys', async () => { - await deployer.deploy(config as ChainMap); + await deployer.deploy(config); }); - describe('ERC20WarpRouterReader', async () => { - const TOKEN_NAME = 'fake'; - const TOKEN_SUPPLY = '100000000000000000000'; - const TOKEN_DECIMALS = 18; - let erc20Factory: ERC20Test__factory; - let token: ERC20Test; + for (const type of [TokenType.collateral, TokenType.synthetic]) { + describe('ERC20WarpRouterReader', async () => { + let reader: EvmWarpRouteReader; + let routerAddress: Address; - before(async () => { - erc20Factory = new ERC20Test__factory(signer); - token = await erc20Factory.deploy( - TOKEN_NAME, - TOKEN_NAME, - TOKEN_SUPPLY, - TOKEN_DECIMALS, - ); - }); - async function deriveWarpConfig(chainName: string, address: string) { - return new EvmERC20WarpRouteReader( - multiProvider, - chainName, - ).deriveWarpRouteConfig(address); - } - it('should derive ERC20RouterConfig from collateral correctly', async () => { - const baseConfig = routerConfigMap[TestChainName.test1]; - const mailbox = Mailbox__factory.connect(baseConfig.mailbox, signer); + before(() => { + reader = new EvmWarpRouteReader(multiProvider, TestChainName.test1); + }); - // Create config - const config: { [key: string]: any } = { - [TestChainName.test1]: { - type: TokenType.collateral, - token: token.address, - hook: await mailbox.defaultHook(), - gas: 65_000, - ...baseConfig, - }, - }; - // Deploy with config - const warpRoute = await deployer.deploy( - config as ChainMap, - ); + beforeEach(async () => { + config[chain] = { + ...config[chain], + type, + // @ts-ignore + token: type === TokenType.collateral ? token : undefined, + }; + const warpRoute = await deployer.deploy(config); + routerAddress = warpRoute[chain][type].address; + }); - // Derive config and check if each value matches - const derivedConfig: Partial = - await deriveWarpConfig( - TestChainName.test1, - warpRoute[TestChainName.test1].collateral.address, + it(`should derive TokenRouterConfig from ${type} correctly`, async () => { + const derivedConfig = await reader.deriveWarpRouteConfig( + routerAddress, + type, ); - - for (const [key, value] of Object.entries(derivedConfig)) { - const deployedValue = config[TestChainName.test1][key]; - if (deployedValue) expect(deployedValue).to.equal(value); - } - - // Check if token values matches - expect(derivedConfig.name).to.equal(TOKEN_NAME); - expect(derivedConfig.symbol).to.equal(TOKEN_NAME); - expect(derivedConfig.decimals).to.equal(TOKEN_DECIMALS); + expect(derivedConfig).to.include(config[chain]); + }); }); - }); + } }); diff --git a/typescript/sdk/src/token/deploy.ts b/typescript/sdk/src/token/deploy.ts index f4f4e93b99..ad865c4f02 100644 --- a/typescript/sdk/src/token/deploy.ts +++ b/typescript/sdk/src/token/deploy.ts @@ -1,92 +1,70 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { constants, providers } from 'ethers'; +import { constants } from 'ethers'; import { ERC20__factory, - ERC721EnumerableUpgradeable__factory, + ERC721Enumerable__factory, GasRouter, - MailboxClient, } from '@hyperlane-xyz/core'; -import { objKeys, objMap, rootLogger } from '@hyperlane-xyz/utils'; +import { assert, objKeys, objMap, rootLogger } from '@hyperlane-xyz/utils'; import { HyperlaneContracts } from '../contracts/types.js'; import { ContractVerifier } from '../deploy/verify/ContractVerifier.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { GasRouterDeployer } from '../router/GasRouterDeployer.js'; -import { GasConfig, RouterConfig } from '../router/types.js'; -import { ChainMap, ChainName } from '../types.js'; +import { ChainName } from '../types.js'; -import { - CollateralConfig, - ERC20Metadata, - ERC20RouterConfig, - ERC721RouterConfig, - HypERC20Config, - HypERC721Config, - TokenConfig, - TokenMetadata, - TokenType, - isCollateralConfig, - isErc20Metadata, - isNativeConfig, - isSyntheticConfig, - isTokenMetadata, - isUriConfig, -} from './config.js'; +import { gasOverhead } from './config.js'; import { HypERC20Factories, - HypERC20contracts, HypERC721Factories, - HypERC721contracts, + TokenFactories, hypERC20contracts, hypERC20factories, hypERC721contracts, hypERC721factories, } from './contracts.js'; +import { + TokenRouterConfig, + isCollateralConfig, + isNativeConfig, + isSyntheticConfig, + isTokenMetadata, +} from './schemas.js'; +import { TokenMetadata, WarpRouteDeployConfig } from './types.js'; -export class HypERC20Deployer extends GasRouterDeployer< - ERC20RouterConfig, - HypERC20Factories -> { +abstract class TokenDeployer< + Factories extends TokenFactories, +> extends GasRouterDeployer { constructor( multiProvider: MultiProvider, + factories: Factories, + loggerName: string, ismFactory?: HyperlaneIsmFactory, contractVerifier?: ContractVerifier, ) { - super(multiProvider, hypERC20factories, { - logger: rootLogger.child({ module: 'HypERC20Deployer' }), + super(multiProvider, factories, { + logger: rootLogger.child({ module: loggerName }), ismFactory, contractVerifier, }); // factories not used in deploy } - routerContractName(config: ERC20RouterConfig): string { - return hypERC20contracts[this.routerContractKey(config)]; - } - - routerContractKey(config: ERC20RouterConfig) { - return config.type as keyof HypERC20contracts; - } - - async constructorArgs( - _: ChainName, - config: ERC20RouterConfig, - ): Promise> { + async constructorArgs(_: ChainName, config: TokenRouterConfig): Promise { if (isCollateralConfig(config)) { - return [config.token, config.mailbox] as any; + return [config.token, config.mailbox]; } else if (isNativeConfig(config)) { - return config.scale - ? [config.scale, config.mailbox] - : ([config.mailbox] as any); + return config.scale ? [config.scale, config.mailbox] : [config.mailbox]; } else if (isSyntheticConfig(config)) { - return [config.decimals, config.mailbox] as any; + assert(config.decimals); // decimals must be defined by this point + return [config.decimals, config.mailbox]; } else { - throw new Error('Unknown collateral type when constructing arguments'); + throw new Error('Unknown token type when constructing arguments'); } } - async initializeArgs(_: ChainName, config: HypERC20Config): Promise { + async initializeArgs(_: ChainName, config: TokenRouterConfig): Promise { // ISM config can be an object, but is not supported right now. if (typeof config.interchainSecurityModule === 'object') { throw new Error('Token deployer does not support ISM objects currently'); @@ -96,318 +74,144 @@ export class HypERC20Deployer extends GasRouterDeployer< config.interchainSecurityModule ?? constants.AddressZero, config.owner, ]; - if (isCollateralConfig(config)) { - return defaultArgs as any; - } else if (isNativeConfig(config)) { - return defaultArgs as any; + if (isCollateralConfig(config) || isNativeConfig(config)) { + return defaultArgs; } else if (isSyntheticConfig(config)) { - return [ - config.totalSupply, - config.name, - config.symbol, - ...defaultArgs, - ] as any; + return [config.totalSupply, config.name, config.symbol, ...defaultArgs]; } else { throw new Error('Unknown collateral type when initializing arguments'); } } - static async fetchMetadata( - provider: providers.Provider, - config: CollateralConfig, - ): Promise { - const erc20 = ERC20__factory.connect(config.token, provider); - - const [name, symbol, totalSupply, decimals] = await Promise.all([ - erc20.name(), - erc20.symbol(), - erc20.totalSupply(), - erc20.decimals(), - ]); - - return { name, symbol, totalSupply, decimals }; - } - - static gasOverheadDefault(config: TokenConfig): number { - switch (config.type) { - case 'fastSynthetic': - return 64_000; - case 'synthetic': - return 64_000; - case 'native': - return 44_000; - case 'collateral': - case 'fastCollateral': - default: - return 68_000; - } - } - - // Gets the metadata for a collateral token, favoring the config - // and getting any on-chain metadata that is missing. - async getCollateralMetadata( - chain: ChainName, - config: CollateralConfig, - ): Promise { - const metadata = { - name: config.name, - symbol: config.symbol, - decimals: config.decimals, - totalSupply: 0, - }; - - if ( - metadata.name && - metadata.symbol && - metadata.decimals !== undefined && - metadata.decimals !== null - ) { - return metadata as ERC20Metadata; - } - const fetchedMetadata = await HypERC20Deployer.fetchMetadata( - this.multiProvider.getProvider(chain), - config, - ); - // Filter out undefined values - const definedConfigMetadata = Object.fromEntries( - Object.entries(metadata).filter(([k, v]) => !!k && !!v), - ); - return { - ...fetchedMetadata, - ...definedConfigMetadata, - } as ERC20Metadata; - } - - router(contracts: HyperlaneContracts) { - for (const key of objKeys(hypERC20factories)) { - if (contracts[key]) { - return contracts[key] as GasRouter; + static async deriveTokenMetadata( + multiProvider: MultiProvider, + configMap: WarpRouteDeployConfig, + ): Promise { + for (const [chain, config] of Object.entries(configMap)) { + if (isTokenMetadata(config)) { + return config; } - } - throw new Error('No matching contract found'); - } - async deployContracts(chain: ChainName, config: HypERC20Config) { - const deployedContracts = await super.deployContracts(chain, config); - const router = deployedContracts[this.routerContractKey(config)]; - await this.configureClient(chain, router, config); - return { - [config.type]: router, - ...deployedContracts, - } as any; - } - - async buildTokenMetadata( - configMap: ChainMap, - ): Promise> { - let tokenMetadata: ERC20Metadata | undefined; + if (isNativeConfig(config)) { + const nativeToken = multiProvider.getChainMetadata(chain).nativeToken; + if (nativeToken) { + return { totalSupply: 0, ...nativeToken }; + } + } - for (const [chain, config] of Object.entries(configMap)) { if (isCollateralConfig(config)) { - const collateralMetadata = await this.getCollateralMetadata( - chain, - config, - ); - tokenMetadata = { - ...collateralMetadata, - totalSupply: 0, - }; - } else if (isNativeConfig(config)) { - const chainMetadata = this.multiProvider.getChainMetadata(chain); - if (chainMetadata.nativeToken) { - tokenMetadata = { - ...chainMetadata.nativeToken, - totalSupply: 0, - }; - } else { - throw new Error( - `Warp route config specifies native token but chain metadata for ${chain} does not provide native token details`, + const provider = multiProvider.getProvider(chain); + + if (config.isNft) { + const erc721 = ERC721Enumerable__factory.connect( + config.token, + provider, ); + const [name, symbol, totalSupply] = await Promise.all([ + erc721.name(), + erc721.symbol(), + erc721.totalSupply(), + ]); + return { + name, + symbol, + totalSupply: totalSupply.toString(), + }; } - } else if (isErc20Metadata(config)) { - tokenMetadata = config; - } - } - if (!isErc20Metadata(tokenMetadata)) { - throw new Error('Invalid ERC20 token metadata'); + const erc20 = ERC20__factory.connect(config.token, provider); + const [name, symbol, totalSupply, decimals] = await Promise.all([ + erc20.name(), + erc20.symbol(), + erc20.totalSupply(), + erc20.decimals(), + ]); + + return { name, symbol, totalSupply: totalSupply.toString(), decimals }; + } } - return objMap(configMap, () => tokenMetadata!); + return undefined; } - buildGasOverhead(configMap: ChainMap): ChainMap { - return objMap(configMap, (_, config) => ({ - gas: HypERC20Deployer.gasOverheadDefault(config), + async deploy(configMap: WarpRouteDeployConfig) { + const tokenMetadata = await TokenDeployer.deriveTokenMetadata( + this.multiProvider, + configMap, + ); + const resolvedConfigMap = objMap(configMap, (_, config) => ({ + ...tokenMetadata, + gas: gasOverhead(config.type), + ...config, })); - } - - async deploy(configMap: ChainMap) { - const tokenMetadata = await this.buildTokenMetadata(configMap); - const gasOverhead = this.buildGasOverhead(configMap); - const mergedConfig = objMap(configMap, (chain, config) => { - return { - ...tokenMetadata[chain], - ...gasOverhead[chain], - ...config, - }; - }) as ChainMap; - - return super.deploy(mergedConfig); + return super.deploy(resolvedConfigMap); } } -export class HypERC721Deployer extends GasRouterDeployer< - ERC721RouterConfig, - HypERC721Factories -> { +export class HypERC20Deployer extends TokenDeployer { constructor( multiProvider: MultiProvider, + ismFactory?: HyperlaneIsmFactory, contractVerifier?: ContractVerifier, ) { - super(multiProvider, hypERC721factories, { - logger: rootLogger.child({ module: 'HypERC721Deployer' }), + super( + multiProvider, + hypERC20factories, + 'HypERC20Deployer', + ismFactory, contractVerifier, - }); - } - routerContractName(config: ERC721RouterConfig): string { - return hypERC721contracts[this.routerContractKey(config)]; + ); } - routerContractKey( - config: ERC721RouterConfig, - ): K { - if (isCollateralConfig(config)) { - return ( - isUriConfig(config) ? TokenType.collateralUri : TokenType.collateral - ) as K; - } else { - // if isSyntheticConfig - return ( - isUriConfig(config) ? TokenType.syntheticUri : TokenType.synthetic - ) as K; + router(contracts: HyperlaneContracts): GasRouter { + for (const key of objKeys(hypERC20factories)) { + if (contracts[key]) { + return contracts[key]; + } } + throw new Error('No matching contract found'); } - async constructorArgs( - _: ChainName, - config: ERC721RouterConfig, - ): Promise { - if (isCollateralConfig(config)) { - return [config.token, config.mailbox]; - } else if (isSyntheticConfig(config)) { - return [config.mailbox]; - } else { - throw new Error('Unknown collateral type when constructing arguments'); - } + routerContractKey(config: TokenRouterConfig): keyof HypERC20Factories { + assert(config.type in hypERC20factories, 'Invalid ERC20 token type'); + return config.type as keyof HypERC20Factories; } - async initializeArgs(_: ChainName, config: ERC721RouterConfig): Promise { - const defaultArgs = [ - config.hook ?? constants.AddressZero, - config.interchainSecurityModule ?? constants.AddressZero, - config.owner, - ]; - if (isCollateralConfig(config)) { - return defaultArgs; - } else if (isSyntheticConfig(config)) { - return [config.totalSupply, config.name, config.symbol, ...defaultArgs]; - } else { - throw new Error('Unknown collateral type when initializing arguments'); - } + routerContractName(config: TokenRouterConfig): string { + return hypERC20contracts[this.routerContractKey(config)]; } +} - static async fetchMetadata( - provider: providers.Provider, - config: CollateralConfig, - ): Promise { - const erc721 = ERC721EnumerableUpgradeable__factory.connect( - config.token, - provider, +export class HypERC721Deployer extends TokenDeployer { + constructor( + multiProvider: MultiProvider, + ismFactory?: HyperlaneIsmFactory, + contractVerifier?: ContractVerifier, + ) { + super( + multiProvider, + hypERC721factories, + 'HypERC721Deployer', + ismFactory, + contractVerifier, ); - const [name, symbol, totalSupply] = await Promise.all([ - erc721.name(), - erc721.symbol(), - erc721.totalSupply(), - ]); - - return { name, symbol, totalSupply }; } - static gasOverheadDefault(config: TokenConfig): number { - switch (config.type) { - case 'synthetic': - return 160_000; - case 'syntheticUri': - return 163_000; - case 'collateral': - case 'collateralUri': - default: - return 80_000; - } - } - - router(contracts: HyperlaneContracts) { + router(contracts: HyperlaneContracts): GasRouter { for (const key of objKeys(hypERC721factories)) { if (contracts[key]) { - return contracts[key] as GasRouter; + return contracts[key]; } } throw new Error('No matching contract found'); } - async deployContracts(chain: ChainName, config: HypERC721Config) { - const { [this.routerContractKey(config)]: router } = - await super.deployContracts(chain, config); - - await this.configureClient(chain, router as MailboxClient, config); - return { [config.type]: router } as any; - } - - async buildTokenMetadata( - configMap: ChainMap, - ): Promise> { - let tokenMetadata: TokenMetadata | undefined; - - for (const [chain, config] of Object.entries(configMap)) { - if (isCollateralConfig(config)) { - const collateralMetadata = await HypERC721Deployer.fetchMetadata( - this.multiProvider.getProvider(chain), - config, - ); - tokenMetadata = { - ...collateralMetadata, - totalSupply: 0, - }; - } else if (isTokenMetadata(config)) { - tokenMetadata = config; - } - } - - if (!isTokenMetadata(tokenMetadata)) { - throw new Error('Invalid ERC721 token metadata'); - } - - return objMap(configMap, () => tokenMetadata!); - } - - buildGasOverhead(configMap: ChainMap): ChainMap { - return objMap(configMap, (_, config) => ({ - gas: HypERC721Deployer.gasOverheadDefault(config), - })); + routerContractKey(config: TokenRouterConfig): keyof HypERC721Factories { + assert(config.type in hypERC721factories, 'Invalid ERC721 token type'); + return config.type as keyof HypERC721Factories; } - async deploy(configMap: ChainMap) { - const tokenMetadata = await this.buildTokenMetadata(configMap); - const gasOverhead = this.buildGasOverhead(configMap); - const mergedConfig = objMap(configMap, (chain, config) => { - return { - ...tokenMetadata[chain], - ...gasOverhead[chain], - ...config, - }; - }) as ChainMap; - - return super.deploy(mergedConfig); + routerContractName(config: TokenRouterConfig): string { + return hypERC721contracts[this.routerContractKey(config)]; } } diff --git a/typescript/sdk/src/token/schemas.test.ts b/typescript/sdk/src/token/schemas.test.ts index 394b4c9fb2..65f780cd03 100644 --- a/typescript/sdk/src/token/schemas.test.ts +++ b/typescript/sdk/src/token/schemas.test.ts @@ -1,8 +1,8 @@ import { expect } from 'chai'; import { ethers } from 'ethers'; -import { constants } from 'ethers'; -import { TokenType, WarpRouteDeployConfigSchema } from '@hyperlane-xyz/sdk'; +import { TokenType } from './config.js'; +import { WarpRouteDeployConfigSchema } from './schemas.js'; const SOME_ADDRESS = ethers.Wallet.createRandom().address; const COLLATERAL_TYPES = [ @@ -16,58 +16,43 @@ const NON_COLLATERAL_TYPES = [ TokenType.synthetic, TokenType.syntheticUri, TokenType.fastSynthetic, - TokenType.native, ]; describe('WarpRouteDeployConfigSchema refine', () => { - it('should require type address', () => { - const config: any = { + let config: any; + beforeEach(() => { + config = { arbitrum: { type: TokenType.collateral, token: SOME_ADDRESS, + owner: SOME_ADDRESS, + mailbox: SOME_ADDRESS, }, }; + }); + + it('should require token type', () => { expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; delete config.arbitrum.type; expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false; }); it('should require token address', () => { - const config: any = { - arbitrum: { - type: TokenType.collateral, - token: SOME_ADDRESS, - }, - }; expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; delete config.arbitrum.token; expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false; }); - it('should allow mailbox to be optional', () => { - const config: any = { - arbitrum: { - type: TokenType.collateral, - token: constants.AddressZero, - mailbox: SOME_ADDRESS, - }, - }; + it('should require mailbox address', () => { expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; delete config.arbitrum.mailbox; - expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; + expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false; }); it('should throw if collateral type and token is empty', async () => { for (const type of COLLATERAL_TYPES) { - const config: any = { - arbitrum: { - type, - mailbox: SOME_ADDRESS, - name: 'Arby Coin', - symbol: 'ARBY', - totalSupply: '10000', - }, - }; + config.arbitrum.type = type; + config.arbitrum.token = undefined; expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false; // Set to some address @@ -76,17 +61,23 @@ describe('WarpRouteDeployConfigSchema refine', () => { } }); - it('should succeed if non-collateral type and token is empty', async () => { + it('should accept native type if token is empty', async () => { + config.arbitrum.type = TokenType.native; + config.arbitrum.token = undefined; + expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; + }); + + it('should succeed if non-collateral type, token is empty, metadata is defined', async () => { + delete config.arbitrum.token; + config.arbitrum.totalSupply = '0'; + config.arbitrum.name = 'name'; + for (const type of NON_COLLATERAL_TYPES) { - const config: any = { - arbitrum: { - type, - mailbox: SOME_ADDRESS, - name: 'Arby Coin', - symbol: 'ARBY', - totalSupply: '10000', - }, - }; + config.arbitrum.type = type; + config.arbitrum.symbol = undefined; + expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.false; + + config.arbitrum.symbol = 'symbol'; expect(WarpRouteDeployConfigSchema.safeParse(config).success).to.be.true; } }); diff --git a/typescript/sdk/src/token/schemas.ts b/typescript/sdk/src/token/schemas.ts index 2890261a57..3ec33bf8f3 100644 --- a/typescript/sdk/src/token/schemas.ts +++ b/typescript/sdk/src/token/schemas.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; -import { routerConfigSchema } from '../router/schemas.js'; +import { GasRouterConfigSchema } from '../router/schemas.js'; +import { isCompliant } from '../utils/schemas.js'; import { TokenType } from './config.js'; @@ -8,49 +9,36 @@ export const TokenMetadataSchema = z.object({ name: z.string(), symbol: z.string(), totalSupply: z.string().or(z.number()), -}); - -export const TokenDecimalsSchema = z.object({ - decimals: z.number(), + decimals: z.number().optional(), scale: z.number().optional(), -}); - -export const ERC20MetadataSchema = - TokenMetadataSchema.merge(TokenDecimalsSchema).partial(); - -export const ERC721MetadataSchema = z.object({ isNft: z.boolean().optional(), }); -export const CollateralConfigSchema = ERC721MetadataSchema.merge( - ERC20MetadataSchema, -).merge( - z.object({ - type: z.enum([ - TokenType.collateral, - TokenType.collateralUri, - TokenType.fastCollateral, - TokenType.collateralVault, - ]), - token: z.string(), - }), -); +export const CollateralConfigSchema = TokenMetadataSchema.partial().extend({ + type: z.enum([ + TokenType.collateral, + TokenType.collateralXERC20, + TokenType.collateralFiat, + TokenType.collateralUri, + TokenType.fastCollateral, + TokenType.collateralVault, + ]), + token: z + .string() + .describe('Existing token address to extend with Warp Route functionality'), +}); -export const NativeConfigSchema = TokenDecimalsSchema.partial().merge( - z.object({ - type: z.enum([TokenType.native]), - }), -); +export const NativeConfigSchema = TokenMetadataSchema.partial().extend({ + type: z.enum([TokenType.native, TokenType.nativeScaled]), +}); -export const SyntheticConfigSchema = TokenMetadataSchema.partial().merge( - z.object({ - type: z.enum([ - TokenType.synthetic, - TokenType.syntheticUri, - TokenType.fastSynthetic, - ]), - }), -); +export const SyntheticConfigSchema = TokenMetadataSchema.partial().extend({ + type: z.enum([ + TokenType.synthetic, + TokenType.syntheticUri, + TokenType.fastSynthetic, + ]), +}); /** * @remarks @@ -63,12 +51,26 @@ export const TokenConfigSchema = z.discriminatedUnion('type', [ SyntheticConfigSchema, ]); -export const TokenRouterConfigSchema = z.intersection( - TokenConfigSchema, - routerConfigSchema, +export const TokenRouterConfigSchema = TokenConfigSchema.and( + GasRouterConfigSchema, ); -export const WarpRouteDeployConfigSchema = z.record( - z.string(), - TokenRouterConfigSchema, -); +export type TokenRouterConfig = z.infer; +export type NativeConfig = z.infer; +export type CollateralConfig = z.infer; + +export const isSyntheticConfig = isCompliant(SyntheticConfigSchema); +export const isCollateralConfig = isCompliant(CollateralConfigSchema); +export const isNativeConfig = isCompliant(NativeConfigSchema); +export const isTokenMetadata = isCompliant(TokenMetadataSchema); + +export const WarpRouteDeployConfigSchema = z + .record(TokenRouterConfigSchema) + .refine((configMap) => { + const entries = Object.entries(configMap); + return ( + entries.some( + ([_, config]) => isCollateralConfig(config) || isNativeConfig(config), + ) || entries.every(([_, config]) => isTokenMetadata(config)) + ); + }, `Config must include Native or Collateral OR all synthetics must define token metadata`); diff --git a/typescript/sdk/src/token/types.ts b/typescript/sdk/src/token/types.ts index 0d0a70800f..8cbead3317 100644 --- a/typescript/sdk/src/token/types.ts +++ b/typescript/sdk/src/token/types.ts @@ -1,9 +1,11 @@ import { z } from 'zod'; import { + TokenMetadataSchema, TokenRouterConfigSchema, WarpRouteDeployConfigSchema, } from './schemas.js'; +export type TokenMetadata = z.infer; export type TokenRouterConfig = z.infer; export type WarpRouteDeployConfig = z.infer; diff --git a/typescript/sdk/src/utils/schemas.ts b/typescript/sdk/src/utils/schemas.ts new file mode 100644 index 0000000000..2babea6c0e --- /dev/null +++ b/typescript/sdk/src/utils/schemas.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; + +export function isCompliant(schema: S) { + return (config: unknown): config is z.infer => + schema.safeParse(config).success; +}