From 2e5653b7cac136dea6d6474d6147f36efe2c1583 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Mon, 20 May 2024 16:13:31 -0400 Subject: [PATCH 01/17] Ensure dir exists before reading in fixture tests --- typescript/sdk/src/ism/metadata/aggregation.test.ts | 4 ++-- typescript/sdk/src/ism/metadata/multisig.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/typescript/sdk/src/ism/metadata/aggregation.test.ts b/typescript/sdk/src/ism/metadata/aggregation.test.ts index cb6c3818ca..117f059a63 100644 --- a/typescript/sdk/src/ism/metadata/aggregation.test.ts +++ b/typescript/sdk/src/ism/metadata/aggregation.test.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { readFileSync, readdirSync } from 'fs'; +import { existsSync, readFileSync, readdirSync } from 'fs'; import { AggregationIsmMetadata, @@ -8,7 +8,7 @@ import { import { Fixture } from './types.test.js'; const path = '../../solidity/fixtures/aggregation'; -const files = readdirSync(path); +const files = existsSync(path) ? readdirSync(path) : []; const fixtures: Fixture[] = files .map((f) => JSON.parse(readFileSync(`${path}/${f}`, 'utf8'))) .map((contents) => { diff --git a/typescript/sdk/src/ism/metadata/multisig.test.ts b/typescript/sdk/src/ism/metadata/multisig.test.ts index 1afca2bdb1..93a42e597e 100644 --- a/typescript/sdk/src/ism/metadata/multisig.test.ts +++ b/typescript/sdk/src/ism/metadata/multisig.test.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { readFileSync, readdirSync } from 'fs'; +import { existsSync, readFileSync, readdirSync } from 'fs'; import { SignatureLike } from '@hyperlane-xyz/utils'; @@ -9,7 +9,7 @@ import { MultisigMetadata, MultisigMetadataBuilder } from './multisig.js'; import { Fixture } from './types.test.js'; const path = '../../solidity/fixtures/multisig'; -const files = readdirSync(path); +const files = existsSync(path) ? readdirSync(path) : []; const fixtures: Fixture[] = files .map((f) => JSON.parse(readFileSync(`${path}/${f}`, 'utf8'))) .map((contents) => { From 4168ac827039263cc4ca796e3179128a503e0ebe Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Mon, 20 May 2024 16:47:45 -0400 Subject: [PATCH 02/17] Stash diff --- typescript/sdk/src/core/HyperlaneCore.ts | 27 +- typescript/sdk/src/deploy/schemas.ts | 4 +- typescript/sdk/src/index.ts | 15 - typescript/sdk/src/ism/schemas.test.ts | 3 +- .../sdk/src/router/GasRouterDeployer.ts | 9 +- .../sdk/src/router/ProxiedRouterDeployer.ts | 37 +- typescript/sdk/src/router/schemas.ts | 22 +- typescript/sdk/src/router/types.ts | 28 +- .../sdk/src/token/EvmERC20WarpRouteReader.ts | 12 +- .../token/adapters/CosmWasmTokenAdapter.ts | 4 +- .../src/token/adapters/CosmosTokenAdapter.ts | 4 +- .../sdk/src/token/adapters/EvmTokenAdapter.ts | 13 +- .../sdk/src/token/adapters/ITokenAdapter.ts | 4 +- .../token/adapters/SealevelTokenAdapter.ts | 13 +- typescript/sdk/src/token/app.ts | 22 +- typescript/sdk/src/token/checker.ts | 14 +- typescript/sdk/src/token/config.ts | 113 ++--- typescript/sdk/src/token/contracts.ts | 10 +- .../sdk/src/token/deploy.hardhat-test.ts | 57 +-- typescript/sdk/src/token/deploy.ts | 422 +++++------------- typescript/sdk/src/token/schemas.test.ts | 3 +- typescript/sdk/src/token/schemas.ts | 85 ++-- typescript/sdk/src/token/types.ts | 2 + 23 files changed, 305 insertions(+), 618 deletions(-) diff --git a/typescript/sdk/src/core/HyperlaneCore.ts b/typescript/sdk/src/core/HyperlaneCore.ts index f3cb21877c..d2a52fe11b 100644 --- a/typescript/sdk/src/core/HyperlaneCore.ts +++ b/typescript/sdk/src/core/HyperlaneCore.ts @@ -5,11 +5,9 @@ import { Mailbox__factory } from '@hyperlane-xyz/core'; import { Address, AddressBytes32, - ProtocolType, bytes32ToAddress, eqAddress, messageId, - objFilter, objMap, parseMessage, pollAsync, @@ -18,14 +16,13 @@ import { import { HyperlaneApp } from '../app/HyperlaneApp.js'; import { appFromAddressesMapHelper } from '../contracts/contracts.js'; import { HyperlaneAddressesMap } from '../contracts/types.js'; -import { OwnableConfig } from '../deploy/types.js'; import { DerivedIsmConfigWithAddress, EvmIsmReader, } from '../ism/EvmIsmReader.js'; import { IsmType, ModuleType, ismTypeToModuleType } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; -import { RouterConfig } from '../router/types.js'; +import { MailboxClientConfig } from '../router/types.js'; import { ChainMap, ChainName } from '../types.js'; import { CoreFactories, coreFactories } from './contracts.js'; @@ -44,23 +41,11 @@ export class HyperlaneCore extends HyperlaneApp { return new HyperlaneCore(helper.contractsMap, helper.multiProvider); } - getRouterConfig = ( - owners: Address | ChainMap, - ): ChainMap => { - // get config - const config = objMap( - this.contractsMap, - (chain, contracts): RouterConfig => ({ - mailbox: contracts.mailbox.address, - owner: typeof owners === 'string' ? owners : owners[chain].owner, - }), - ); - // filter for EVM chains - return objFilter( - config, - (chainName, _): _ is RouterConfig => - this.multiProvider.getProtocol(chainName) === ProtocolType.Ethereum, - ); + getRouterConfig = (owner: Address): ChainMap => { + return objMap(this.contractsMap, (_, contracts) => ({ + mailbox: contracts.mailbox.address, + owner, + })); }; quoteGasPayment = ( 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/index.ts b/typescript/sdk/src/index.ts index fd338351e7..11d612dc39 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, @@ -421,24 +419,11 @@ 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 { HypERC20Factories, 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/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..5559bd1005 100644 --- a/typescript/sdk/src/router/schemas.ts +++ b/typescript/sdk/src/router/schemas.ts @@ -4,18 +4,20 @@ import { OwnableConfigSchema } from '../deploy/schemas.js'; import { ZHash } from '../index.js'; import { IsmConfigSchema } from '../ism/schemas.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..809752c8eb 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 type { 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..472a094fb0 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts @@ -4,7 +4,6 @@ import { ERC20__factory, HypERC20Collateral__factory, } from '@hyperlane-xyz/core'; -import { ERC20Metadata, ERC20RouterConfig } from '@hyperlane-xyz/sdk'; import { Address } from '@hyperlane-xyz/utils'; import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js'; @@ -13,14 +12,17 @@ import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainName } from '../types.js'; +import { TokenRouterConfig } from './config.js'; +import { TokenMetadata } from './types.js'; + type WarpRouteBaseMetadata = Record< 'mailbox' | 'owner' | 'token' | 'hook' | 'interchainSecurityModule', string >; -type DerivedERC20WarpRouteConfig = Omit; +type DerivedERC20WarpRouteConfig = Omit; -export class EvmERC20WarpRouteReader { +export class EvmWarpRouteReader { provider: providers.Provider; evmHookReader: EvmHookReader; evmIsmReader: EvmIsmReader; @@ -111,7 +113,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 +122,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..b45d5a5336 100644 --- a/typescript/sdk/src/token/app.ts +++ b/typescript/sdk/src/token/app.ts @@ -1,20 +1,14 @@ import { TokenRouter } from '@hyperlane-xyz/core'; import { objKeys } from '@hyperlane-xyz/utils'; -import { appFromAddressesMapHelper } from '../contracts/contracts.js'; import { - HyperlaneAddressesMap, HyperlaneContracts, HyperlaneContractsMap, } from '../contracts/types.js'; 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,23 +19,11 @@ 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; } } throw new Error('No router found in contracts'); } - - static fromAddressesMap( - addressesMap: HyperlaneAddressesMap, - multiProvider: MultiProvider, - ): HypERC20App { - const helper = appFromAddressesMapHelper( - addressesMap, - hypERC20factories, - multiProvider, - ); - return new HypERC20App(helper.contractsMap, helper.multiProvider); - } } diff --git a/typescript/sdk/src/token/checker.ts b/typescript/sdk/src/token/checker.ts index 9b0e366f8e..a890e2c507 100644 --- a/typescript/sdk/src/token/checker.ts +++ b/typescript/sdk/src/token/checker.ts @@ -9,19 +9,18 @@ import { ChainName } from '../types.js'; import { HypERC20App } from './app.js'; import { - ERC20RouterConfig, - HypERC20Config, - TokenMetadata, + TokenRouterConfig, isCollateralConfig, isNativeConfig, isSyntheticConfig, } from './config.js'; import { HypERC20Factories } from './contracts.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,7 +30,7 @@ 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'; @@ -43,6 +42,11 @@ export class HypERC20Checker extends HyperlaneRouterChecker< ]; for (const check of checks) { + if (!(check.method in token)) { + continue; + } + + // @ts-ignore const actual = await token[check.method](); const expected = config[check.method]; if (actual !== expected) { diff --git a/typescript/sdk/src/token/config.ts b/typescript/sdk/src/token/config.ts index e8d57a4a7d..5a478f4981 100644 --- a/typescript/sdk/src/token/config.ts +++ b/typescript/sdk/src/token/config.ts @@ -1,9 +1,12 @@ -import { ethers } from 'ethers'; -import z from 'zod'; +import { z } from 'zod'; -import { GasRouterConfig } from '../router/types.js'; - -import { SyntheticConfigSchema } from './schemas.js'; +import { + CollateralConfigSchema, + NativeConfigSchema, + SyntheticConfigSchema, + TokenMetadataSchema, + TokenRouterConfigSchema, +} from './schemas.js'; export enum TokenType { synthetic = 'synthetic', @@ -19,86 +22,28 @@ 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 type TokenRouterConfig = z.infer; +export type NativeConfig = z.infer; +export type CollateralConfig = z.infer; -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; +function isCompliant(schema: S) { + return (config: unknown): config is z.infer => + schema.safeParse(config).success; +} -export type HypERC721Config = GasRouterConfig & SyntheticConfig; -export type HypERC721CollateralConfig = GasRouterConfig & CollateralConfig; -export type ERC721RouterConfig = HypERC721Config | HypERC721CollateralConfig; +export const isSyntheticConfig = isCompliant(SyntheticConfigSchema); +export const isCollateralConfig = isCompliant(CollateralConfigSchema); +export const isNativeConfig = isCompliant(NativeConfigSchema); +export const isTokenMetadata = isCompliant(TokenMetadataSchema); 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..aeefbaa80a 100644 --- a/typescript/sdk/src/token/deploy.hardhat-test.ts +++ b/typescript/sdk/src/token/deploy.hardhat-test.ts @@ -2,28 +2,18 @@ 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 { ERC20Test, ERC20Test__factory } from '@hyperlane-xyz/core'; import { 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 { CollateralConfig, TokenRouterConfig, TokenType } from './config.js'; import { HypERC20Deployer } from './deploy.js'; import { WarpRouteDeployConfig } from './types.js'; @@ -32,7 +22,6 @@ describe('TokenDeployer', async () => { let deployer: HypERC20Deployer; let multiProvider: MultiProvider; let coreApp: TestCoreApp; - let routerConfigMap: ChainMap; let config: WarpRouteDeployConfig; before(async () => { @@ -44,16 +33,15 @@ 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, ...c, }), ); @@ -64,7 +52,7 @@ describe('TokenDeployer', async () => { }); it('deploys', async () => { - await deployer.deploy(config as ChainMap); + await deployer.deploy(config); }); describe('ERC20WarpRouterReader', async () => { @@ -84,41 +72,32 @@ describe('TokenDeployer', async () => { ); }); async function deriveWarpConfig(chainName: string, address: string) { - return new EvmERC20WarpRouteReader( + return new EvmWarpRouteReader( 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); - // Create config - const config: { [key: string]: any } = { + const collateralConfig: WarpRouteDeployConfig = { [TestChainName.test1]: { + ...config[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, - ); + const warpRoute = await deployer.deploy(collateralConfig); // Derive config and check if each value matches - const derivedConfig: Partial = - await deriveWarpConfig( - TestChainName.test1, - warpRoute[TestChainName.test1].collateral.address, - ); + const derivedConfig: Partial = await deriveWarpConfig( + TestChainName.test1, + warpRoute[TestChainName.test1].collateral.address, + ); - for (const [key, value] of Object.entries(derivedConfig)) { - const deployedValue = config[TestChainName.test1][key]; - if (deployedValue) expect(deployedValue).to.equal(value); - } + expect(derivedConfig).to.deep.equal( + collateralConfig[TestChainName.test1], + ); // Check if token values matches expect(derivedConfig.name).to.equal(TOKEN_NAME); diff --git a/typescript/sdk/src/token/deploy.ts b/typescript/sdk/src/token/deploy.ts index f4f4e93b99..b11858682a 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, + TokenRouterConfig, + gasOverhead, isCollateralConfig, - isErc20Metadata, isNativeConfig, isSyntheticConfig, isTokenMetadata, - isUriConfig, } from './config.js'; import { HypERC20Factories, - HypERC20contracts, HypERC721Factories, - HypERC721contracts, + TokenFactories, hypERC20contracts, hypERC20factories, hypERC721contracts, hypERC721factories, } from './contracts.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); + return [config.decimals, config.mailbox]; } else { throw new Error('Unknown collateral 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..a10a23308a 100644 --- a/typescript/sdk/src/token/schemas.test.ts +++ b/typescript/sdk/src/token/schemas.test.ts @@ -1,6 +1,5 @@ import { expect } from 'chai'; -import { ethers } from 'ethers'; -import { constants } from 'ethers'; +import { constants, ethers } from 'ethers'; import { TokenType, WarpRouteDeployConfigSchema } from '@hyperlane-xyz/sdk'; diff --git a/typescript/sdk/src/token/schemas.ts b/typescript/sdk/src/token/schemas.ts index 2890261a57..2226d8ab95 100644 --- a/typescript/sdk/src/token/schemas.ts +++ b/typescript/sdk/src/token/schemas.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { routerConfigSchema } from '../router/schemas.js'; +import { GasRouterConfigSchema } from '../router/schemas.js'; import { TokenType } from './config.js'; @@ -8,49 +8,34 @@ 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(), +}); -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 +48,22 @@ 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 const WarpRouteDeployConfigSchema = z + .record(TokenRouterConfigSchema) + .refine((configMap) => { + const entries = Object.entries(configMap); + return ( + entries.some( + ([_, config]) => + CollateralConfigSchema.safeParse(config).success || + NativeConfigSchema.safeParse(config).success, + ) || + entries.filter( + ([_, config]) => TokenMetadataSchema.safeParse(config).success, + ).length === entries.length + ); + }, `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; From e4fa30c597a8efd02dd4eb41c49c9f11a354b69e Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Mon, 20 May 2024 16:56:48 -0400 Subject: [PATCH 03/17] More fixes --- typescript/sdk/src/ism/schemas.ts | 2 +- typescript/sdk/src/middleware/account/schemas.ts | 3 +-- typescript/sdk/src/router/schemas.ts | 2 +- typescript/sdk/src/token/schemas.test.ts | 3 ++- 4 files changed, 5 insertions(+), 5 deletions(-) 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/schemas.ts b/typescript/sdk/src/router/schemas.ts index 5559bd1005..6c6ffa0311 100644 --- a/typescript/sdk/src/router/schemas.ts +++ b/typescript/sdk/src/router/schemas.ts @@ -1,8 +1,8 @@ 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 MailboxClientConfigSchema = OwnableConfigSchema.extend({ mailbox: ZHash, diff --git a/typescript/sdk/src/token/schemas.test.ts b/typescript/sdk/src/token/schemas.test.ts index a10a23308a..a478f2f800 100644 --- a/typescript/sdk/src/token/schemas.test.ts +++ b/typescript/sdk/src/token/schemas.test.ts @@ -1,7 +1,8 @@ import { expect } from 'chai'; import { constants, ethers } 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 = [ From 8ef1c602bf518750f8e3394cf46d45f82c9c4cf1 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 21 May 2024 10:10:20 -0400 Subject: [PATCH 04/17] Fix unit tests --- typescript/sdk/src/index.ts | 16 ++--- typescript/sdk/src/router/types.ts | 2 +- .../sdk/src/token/EvmERC20WarpRouteReader.ts | 2 +- typescript/sdk/src/token/checker.ts | 4 +- typescript/sdk/src/token/config.ts | 24 ------- .../sdk/src/token/deploy.hardhat-test.ts | 3 +- typescript/sdk/src/token/deploy.ts | 16 ++--- typescript/sdk/src/token/schemas.test.ts | 67 ++++++++----------- typescript/sdk/src/token/schemas.ts | 14 ++++ 9 files changed, 64 insertions(+), 84 deletions(-) diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 11d612dc39..a191816436 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -417,14 +417,7 @@ export { } from './token/adapters/serialization.js'; export { HypERC20App } from './token/app.js'; export { HypERC20Checker } from './token/checker.js'; -export { - CollateralConfig, - NativeConfig, - TokenType, - isCollateralConfig, - isNativeConfig, - isSyntheticConfig, -} from './token/config.js'; +export { TokenType } from './token/config.js'; export { HypERC20Factories, HypERC721Factories, @@ -465,7 +458,12 @@ 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, } from './token/schemas.js'; export { TokenRouterConfig, WarpRouteDeployConfig } from './token/types.js'; diff --git a/typescript/sdk/src/router/types.ts b/typescript/sdk/src/router/types.ts index 809752c8eb..1d4202e05b 100644 --- a/typescript/sdk/src/router/types.ts +++ b/typescript/sdk/src/router/types.ts @@ -6,7 +6,7 @@ import { Router, TimelockController__factory, } from '@hyperlane-xyz/core'; -import type { Address } from '@hyperlane-xyz/utils'; +import { Address } from '@hyperlane-xyz/utils'; import { HyperlaneFactories } from '../contracts/types.js'; import { UpgradeConfig } from '../deploy/proxy.js'; diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts index 472a094fb0..d077eca40a 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts @@ -12,7 +12,7 @@ import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainName } from '../types.js'; -import { TokenRouterConfig } from './config.js'; +import { TokenRouterConfig } from './schemas.js'; import { TokenMetadata } from './types.js'; type WarpRouteBaseMetadata = Record< diff --git a/typescript/sdk/src/token/checker.ts b/typescript/sdk/src/token/checker.ts index a890e2c507..3855b1e8da 100644 --- a/typescript/sdk/src/token/checker.ts +++ b/typescript/sdk/src/token/checker.ts @@ -8,13 +8,13 @@ import { HyperlaneRouterChecker } from '../router/HyperlaneRouterChecker.js'; import { ChainName } from '../types.js'; import { HypERC20App } from './app.js'; +import { HypERC20Factories } from './contracts.js'; import { 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< diff --git a/typescript/sdk/src/token/config.ts b/typescript/sdk/src/token/config.ts index 5a478f4981..59a2e597dc 100644 --- a/typescript/sdk/src/token/config.ts +++ b/typescript/sdk/src/token/config.ts @@ -1,13 +1,3 @@ -import { z } from 'zod'; - -import { - CollateralConfigSchema, - NativeConfigSchema, - SyntheticConfigSchema, - TokenMetadataSchema, - TokenRouterConfigSchema, -} from './schemas.js'; - export enum TokenType { synthetic = 'synthetic', fastSynthetic = 'fastSynthetic', @@ -33,17 +23,3 @@ export const gasOverhead = (tokenType: TokenType) => { return 68_000; } }; - -export type TokenRouterConfig = z.infer; -export type NativeConfig = z.infer; -export type CollateralConfig = z.infer; - -function isCompliant(schema: S) { - return (config: unknown): config is z.infer => - schema.safeParse(config).success; -} - -export const isSyntheticConfig = isCompliant(SyntheticConfigSchema); -export const isCollateralConfig = isCompliant(CollateralConfigSchema); -export const isNativeConfig = isCompliant(NativeConfigSchema); -export const isTokenMetadata = isCompliant(TokenMetadataSchema); diff --git a/typescript/sdk/src/token/deploy.hardhat-test.ts b/typescript/sdk/src/token/deploy.hardhat-test.ts index aeefbaa80a..246480419c 100644 --- a/typescript/sdk/src/token/deploy.hardhat-test.ts +++ b/typescript/sdk/src/token/deploy.hardhat-test.ts @@ -13,8 +13,9 @@ import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { EvmWarpRouteReader } from './EvmERC20WarpRouteReader.js'; -import { CollateralConfig, TokenRouterConfig, TokenType } from './config.js'; +import { TokenType } from './config.js'; import { HypERC20Deployer } from './deploy.js'; +import { CollateralConfig, TokenRouterConfig } from './schemas.js'; import { WarpRouteDeployConfig } from './types.js'; describe('TokenDeployer', async () => { diff --git a/typescript/sdk/src/token/deploy.ts b/typescript/sdk/src/token/deploy.ts index b11858682a..479e94c587 100644 --- a/typescript/sdk/src/token/deploy.ts +++ b/typescript/sdk/src/token/deploy.ts @@ -15,14 +15,7 @@ import { MultiProvider } from '../providers/MultiProvider.js'; import { GasRouterDeployer } from '../router/GasRouterDeployer.js'; import { ChainName } from '../types.js'; -import { - TokenRouterConfig, - gasOverhead, - isCollateralConfig, - isNativeConfig, - isSyntheticConfig, - isTokenMetadata, -} from './config.js'; +import { gasOverhead } from './config.js'; import { HypERC20Factories, HypERC721Factories, @@ -32,6 +25,13 @@ import { hypERC721contracts, hypERC721factories, } from './contracts.js'; +import { + TokenRouterConfig, + isCollateralConfig, + isNativeConfig, + isSyntheticConfig, + isTokenMetadata, +} from './schemas.js'; import { TokenMetadata, WarpRouteDeployConfig } from './types.js'; abstract class TokenDeployer< diff --git a/typescript/sdk/src/token/schemas.test.ts b/typescript/sdk/src/token/schemas.test.ts index a478f2f800..65f780cd03 100644 --- a/typescript/sdk/src/token/schemas.test.ts +++ b/typescript/sdk/src/token/schemas.test.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { constants, ethers } from 'ethers'; +import { ethers } from 'ethers'; import { TokenType } from './config.js'; import { WarpRouteDeployConfigSchema } from './schemas.js'; @@ -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 2226d8ab95..2b01b95a8c 100644 --- a/typescript/sdk/src/token/schemas.ts +++ b/typescript/sdk/src/token/schemas.ts @@ -67,3 +67,17 @@ export const WarpRouteDeployConfigSchema = z ).length === entries.length ); }, `Config must include Native or Collateral OR all synthetics must define token metadata`); + +export type TokenRouterConfig = z.infer; +export type NativeConfig = z.infer; +export type CollateralConfig = z.infer; + +function isCompliant(schema: S) { + return (config: unknown): config is z.infer => + schema.safeParse(config).success; +} + +export const isSyntheticConfig = isCompliant(SyntheticConfigSchema); +export const isCollateralConfig = isCompliant(CollateralConfigSchema); +export const isNativeConfig = isCompliant(NativeConfigSchema); +export const isTokenMetadata = isCompliant(TokenMetadataSchema); From e0b1216db3deaffed50767eea532d43b43945ceb Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 21 May 2024 15:17:53 -0400 Subject: [PATCH 05/17] Fix hardhat tests --- .../sdk/src/token/EvmERC20WarpRouteReader.ts | 91 +++++++++---------- .../sdk/src/token/deploy.hardhat-test.ts | 91 +++++++++---------- typescript/sdk/src/token/deploy.ts | 2 +- 3 files changed, 87 insertions(+), 97 deletions(-) diff --git a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts index d077eca40a..d62285705a 100644 --- a/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts @@ -3,24 +3,22 @@ import { ethers, providers } from 'ethers'; import { ERC20__factory, HypERC20Collateral__factory, + MailboxClient__factory, } from '@hyperlane-xyz/core'; -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'; +import { TokenType } from './config.js'; import { TokenRouterConfig } from './schemas.js'; import { TokenMetadata } from './types.js'; -type WarpRouteBaseMetadata = Record< - 'mailbox' | 'owner' | 'token' | 'hook' | 'interchainSecurityModule', - string ->; - -type DerivedERC20WarpRouteConfig = Omit; +const { AddressZero } = ethers.constants; export class EvmWarpRouteReader { provider: providers.Provider; @@ -46,34 +44,32 @@ export class EvmWarpRouteReader { */ 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; } /** @@ -82,28 +78,31 @@ export class EvmWarpRouteReader { * @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, }; } diff --git a/typescript/sdk/src/token/deploy.hardhat-test.ts b/typescript/sdk/src/token/deploy.hardhat-test.ts index 246480419c..49323191d1 100644 --- a/typescript/sdk/src/token/deploy.hardhat-test.ts +++ b/typescript/sdk/src/token/deploy.hardhat-test.ts @@ -2,8 +2,8 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js'; import { expect } from 'chai'; import hre from 'hardhat'; -import { ERC20Test, ERC20Test__factory } from '@hyperlane-xyz/core'; -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'; @@ -15,15 +15,18 @@ import { MultiProvider } from '../providers/MultiProvider.js'; import { EvmWarpRouteReader } from './EvmERC20WarpRouteReader.js'; import { TokenType } from './config.js'; import { HypERC20Deployer } from './deploy.js'; -import { CollateralConfig, TokenRouterConfig } from './schemas.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 config: WarpRouteDeployConfig; + let token: Address; before(async () => { [signer] = await hre.ethers.getSigners(); @@ -42,10 +45,19 @@ describe('TokenDeployer', async () => { name: chain, symbol: `u${chain}`, decimals: 18, - totalSupply: 100_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 () => { @@ -56,54 +68,33 @@ describe('TokenDeployer', async () => { 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 EvmWarpRouteReader( - multiProvider, - chainName, - ).deriveWarpRouteConfig(address); - } - it('should derive ERC20RouterConfig from collateral correctly', async () => { - // Create config - const collateralConfig: WarpRouteDeployConfig = { - [TestChainName.test1]: { - ...config[TestChainName.test1], - type: TokenType.collateral, - token: token.address, - }, - }; - // Deploy with config - const warpRoute = await deployer.deploy(collateralConfig); - - // Derive config and check if each value matches - const derivedConfig: Partial = await deriveWarpConfig( - TestChainName.test1, - warpRoute[TestChainName.test1].collateral.address, - ); + before(() => { + reader = new EvmWarpRouteReader(multiProvider, TestChainName.test1); + }); - expect(derivedConfig).to.deep.equal( - collateralConfig[TestChainName.test1], - ); + 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; + }); - // 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); + it(`should derive TokenRouterConfig from ${type} correctly`, async () => { + const derivedConfig = await reader.deriveWarpRouteConfig( + routerAddress, + type, + ); + expect(derivedConfig).to.include(config[chain]); + }); }); - }); + } }); diff --git a/typescript/sdk/src/token/deploy.ts b/typescript/sdk/src/token/deploy.ts index 479e94c587..e53eaeb766 100644 --- a/typescript/sdk/src/token/deploy.ts +++ b/typescript/sdk/src/token/deploy.ts @@ -60,7 +60,7 @@ abstract class TokenDeployer< assert(config.decimals); return [config.decimals, config.mailbox]; } else { - throw new Error('Unknown collateral type when constructing arguments'); + throw new Error('Unknown token type when constructing arguments'); } } From 73f3e00da018ac6a23fb23912f9c1476633bafb0 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 21 May 2024 16:15:38 -0400 Subject: [PATCH 06/17] Remove base chain from CLI warp commands --- typescript/cli/src/config/warp.ts | 132 ++++++------ typescript/cli/src/deploy/utils.ts | 30 --- typescript/cli/src/deploy/warp.ts | 221 +++++--------------- typescript/cli/src/send/message.ts | 9 +- typescript/cli/src/send/transfer.ts | 9 +- typescript/cli/src/tests/deployTestErc20.ts | 9 +- 6 files changed, 135 insertions(+), 275 deletions(-) diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 19f1a1fbde..e6911efc70 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -1,24 +1,44 @@ -import { confirm, input } from '@inquirer/prompts'; -import { ethers } from 'ethers'; +import { input, select } from '@inquirer/prompts'; import { - ChainMetadata, TokenType, WarpCoreConfig, WarpCoreConfigSchema, WarpRouteDeployConfig, WarpRouteDeployConfigSchema, } from '@hyperlane-xyz/sdk'; -import { objFilter } from '@hyperlane-xyz/utils'; import { CommandContext } from '../context/types.js'; import { errorRed, logBlue, logGreen } from '../logger.js'; -import { - runMultiChainSelectionStep, - runSingleChainSelectionStep, -} from '../utils/chains.js'; +import { runMultiChainSelectionStep } from '../utils/chains.js'; import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js'; +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], +})); + export function readWarpRouteDeployConfig( filePath: string, ): WarpRouteDeployConfig { @@ -40,67 +60,55 @@ export async function createWarpRouteDeployConfig({ outPath: string; }) { logBlue('Creating a new warp route deployment config'); - const baseChain = await runSingleChainSelectionStep( + + const owner = + (await context.signer?.getAddress()) ?? + (await input({ + message: 'Enter owner address', + })); + + const warpChains = await runMultiChainSelectionStep( context.chainMetadata, - 'Select base chain with the original token to warp', + 'Select chains to connect', ); - const isNative = await confirm({ - message: - 'Are you creating a route for the native token of the base chain (e.g. Ether on Ethereum)?', - }); + 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, + }); - 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)?', - }); + // TODO: restore NFT prompting + const isNft = + type === TokenType.syntheticUri || type === TokenType.collateralUri; - const addressMessage = `Enter the ${ - isYieldBearing ? 'ERC-4626 vault' : 'collateral token' - } address`; - const baseAddress = isNative - ? ethers.constants.AddressZero - : await input({ message: addressMessage }); + // TODO: migrate to detectAndConfirmOrPrompt + const addresses = await context.registry.getChainAddresses(chain); + const mailbox = + addresses?.mailbox ?? + (await input({ + message: `Enter the mailbox address for chain ${chain}`, + })); - const metadataWithoutBase = objFilter( - context.chainMetadata, - (chain, _): _ is ChainMetadata => chain !== baseChain, - ); - const syntheticChains = await runMultiChainSelectionStep( - metadataWithoutBase, - 'Select chains to which the base token will be connected', - ); - - // 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, - }, - }; + switch (type) { + case TokenType.collateral: + case TokenType.collateralXERC20: + case TokenType.collateralFiat: + case TokenType.collateralUri: + case TokenType.fastCollateral: + case TokenType.collateralVault: + const token = await input({ + message: `Enter the existing token address for chain ${chain}`, + }); + result[chain] = { mailbox, type, token, owner, isNft }; + break; + default: + result[chain] = { mailbox, type, owner, isNft }; + } } - syntheticChains.map((chain) => { - result[chain] = { - type: TokenType.synthetic, - }; - }); - if (isValidWarpRouteDeployConfig(result)) { logGreen(`Warp Route config is valid, writing to file ${outPath}`); writeYamlOrJson(outPath, result); @@ -108,7 +116,7 @@ export async function createWarpRouteDeployConfig({ 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'); + WarpRouteDeployConfigSchema.parse(result); // throws error } } 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..8d4514e241 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -1,38 +1,34 @@ -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, } 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, @@ -63,26 +59,22 @@ export async function runWarpRouteDeploy({ warpRouteDeploymentConfigPath, ); - const configs = await runBuildConfigStep({ - context, - warpRouteConfig, - }); - const deploymentParams = { context, - ...configs, + configMap: warpRouteConfig, }; logBlue('Warp route deployment plan'); await runDeployPlanStep(deploymentParams); - await runPreflightChecks({ - ...deploymentParams, + await runPreflightChecksForChains({ + context, + chains: Object.keys(warpRouteConfig), minGas: MINIMUM_WARP_DEPLOY_GAS, }); const userAddress = await signer.getAddress(); - const chains = [deploymentParams.origin, ...configs.remotes]; + const chains = Object.keys(warpRouteConfig); const initialBalances = await prepareDeploy(context, userAddress, chains); @@ -91,111 +83,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, - }; -} - -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]; +async function runDeployPlanStep({ context, configMap }: DeployParams) { + const { skipConfirmation } = context; - 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 +104,63 @@ 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.`, - ); - } -} +const DEFAULT_METADATA = { + name: 'unknown', + symbol: 'unknown', + totalSupply: '0', + decimals: 18, +}; -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 = { + ...DEFAULT_METADATA, + ...tokenMetadata, + ...config, + }; + 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, 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..eeea603dd3 100644 --- a/typescript/cli/src/tests/deployTestErc20.ts +++ b/typescript/cli/src/tests/deployTestErc20.ts @@ -1,3 +1,4 @@ +import { AddressZero } from '@ethersproject/constants'; import { Wallet, providers } from 'ethers'; import fs from 'fs'; @@ -24,8 +25,14 @@ async function deployERC20() { type: TokenType.collateral, token: contract.address, isNft: false, + owner: signer.address, + mailbox: AddressZero, + }, + [chain2]: { + type: TokenType.synthetic, + owner: signer.address, + mailbox: AddressZero, }, - [chain2]: { type: TokenType.synthetic }, }; console.log('Writing deployment config to', outPath); From ed1385c998d7377dac2ef233750cd194525fdc05 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 21 May 2024 16:31:54 -0400 Subject: [PATCH 07/17] Fix infra build and tests --- typescript/infra/config/warp.ts | 9 +++---- .../infra/src/govern/HyperlaneAppGovernor.ts | 8 +----- typescript/infra/test/govern.hardhat-test.ts | 16 +++++------ typescript/sdk/src/core/HyperlaneCore.ts | 27 ++++++++++++++----- typescript/sdk/src/deploy/types.ts | 2 +- typescript/sdk/src/token/app.ts | 14 ++++++++++ 6 files changed, 48 insertions(+), 28 deletions(-) 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 31eb1dc38b..26ec2b13c3 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, @@ -135,12 +134,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/HyperlaneCore.ts b/typescript/sdk/src/core/HyperlaneCore.ts index d2a52fe11b..f3cb21877c 100644 --- a/typescript/sdk/src/core/HyperlaneCore.ts +++ b/typescript/sdk/src/core/HyperlaneCore.ts @@ -5,9 +5,11 @@ import { Mailbox__factory } from '@hyperlane-xyz/core'; import { Address, AddressBytes32, + ProtocolType, bytes32ToAddress, eqAddress, messageId, + objFilter, objMap, parseMessage, pollAsync, @@ -16,13 +18,14 @@ import { import { HyperlaneApp } from '../app/HyperlaneApp.js'; import { appFromAddressesMapHelper } from '../contracts/contracts.js'; import { HyperlaneAddressesMap } from '../contracts/types.js'; +import { OwnableConfig } from '../deploy/types.js'; import { DerivedIsmConfigWithAddress, EvmIsmReader, } from '../ism/EvmIsmReader.js'; import { IsmType, ModuleType, ismTypeToModuleType } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; -import { MailboxClientConfig } from '../router/types.js'; +import { RouterConfig } from '../router/types.js'; import { ChainMap, ChainName } from '../types.js'; import { CoreFactories, coreFactories } from './contracts.js'; @@ -41,11 +44,23 @@ export class HyperlaneCore extends HyperlaneApp { return new HyperlaneCore(helper.contractsMap, helper.multiProvider); } - getRouterConfig = (owner: Address): ChainMap => { - return objMap(this.contractsMap, (_, contracts) => ({ - mailbox: contracts.mailbox.address, - owner, - })); + getRouterConfig = ( + owners: Address | ChainMap, + ): ChainMap => { + // get config + const config = objMap( + this.contractsMap, + (chain, contracts): RouterConfig => ({ + mailbox: contracts.mailbox.address, + owner: typeof owners === 'string' ? owners : owners[chain].owner, + }), + ); + // filter for EVM chains + return objFilter( + config, + (chainName, _): _ is RouterConfig => + this.multiProvider.getProtocol(chainName) === ProtocolType.Ethereum, + ); }; quoteGasPayment = ( 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/token/app.ts b/typescript/sdk/src/token/app.ts index b45d5a5336..0eeb41dbca 100644 --- a/typescript/sdk/src/token/app.ts +++ b/typescript/sdk/src/token/app.ts @@ -1,7 +1,9 @@ import { TokenRouter } from '@hyperlane-xyz/core'; import { objKeys } from '@hyperlane-xyz/utils'; +import { appFromAddressesMapHelper } from '../contracts/contracts.js'; import { + HyperlaneAddressesMap, HyperlaneContracts, HyperlaneContractsMap, } from '../contracts/types.js'; @@ -26,4 +28,16 @@ export class HypERC20App extends GasRouterApp { } throw new Error('No router found in contracts'); } + + static fromAddressesMap( + addressesMap: HyperlaneAddressesMap, + multiProvider: MultiProvider, + ): HypERC20App { + const helper = appFromAddressesMapHelper( + addressesMap, + hypERC20factories, + multiProvider, + ); + return new HypERC20App(helper.contractsMap, helper.multiProvider); + } } From 5ecfc033fe15449eb47a7c4e9baf04f7d52b32d6 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 21 May 2024 16:33:59 -0400 Subject: [PATCH 08/17] Remove remaining ICA logic from deployers --- .../sdk/src/core/HyperlaneCoreDeployer.ts | 11 +------- .../sdk/src/deploy/HyperlaneDeployer.ts | 27 ++----------------- .../sdk/src/hook/HyperlaneHookDeployer.ts | 2 +- 3 files changed, 4 insertions(+), 36 deletions(-) diff --git a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts index dc36a05b9b..cd4e1f4677 100644 --- a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts +++ b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts @@ -69,11 +69,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( @@ -111,15 +106,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 46f4db67ea..1fe6bd6533 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, @@ -709,10 +709,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 }, @@ -736,24 +733,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/hook/HyperlaneHookDeployer.ts b/typescript/sdk/src/hook/HyperlaneHookDeployer.ts index 3e521b094a..de5031f11f 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, ]); } From 5894e9f645683281aeda80b3540d37d07eb13bd0 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 21 May 2024 16:40:55 -0400 Subject: [PATCH 09/17] Fix lint --- typescript/cli/src/config/warp.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index e6911efc70..e5649b7b2a 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -99,10 +99,15 @@ export async function createWarpRouteDeployConfig({ case TokenType.collateralUri: case TokenType.fastCollateral: case TokenType.collateralVault: - const token = await input({ - message: `Enter the existing token address for chain ${chain}`, - }); - result[chain] = { mailbox, type, token, owner, isNft }; + result[chain] = { + mailbox, + type, + owner, + isNft, + token: await input({ + message: `Enter the existing token address for chain ${chain}`, + }), + }; break; default: result[chain] = { mailbox, type, owner, isNft }; From d6eba4fec28a1b4158f02e2106f8ab1fe1b0a5f7 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 21 May 2024 16:42:34 -0400 Subject: [PATCH 10/17] Add changeset --- .changeset/nice-rivers-own.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/nice-rivers-own.md 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 From 7c5f0ba7809bcb89f9fcabfc273d5ab778c07a08 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Wed, 22 May 2024 19:13:55 -0400 Subject: [PATCH 11/17] Export isCompliant util --- typescript/sdk/src/index.ts | 1 + typescript/sdk/src/token/schemas.ts | 6 +----- typescript/sdk/src/utils/schemas.ts | 6 ++++++ 3 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 typescript/sdk/src/utils/schemas.ts diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index a191816436..aaccf2f890 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -465,6 +465,7 @@ export { isNativeConfig, isSyntheticConfig, } 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/token/schemas.ts b/typescript/sdk/src/token/schemas.ts index 2b01b95a8c..5c7635b0f6 100644 --- a/typescript/sdk/src/token/schemas.ts +++ b/typescript/sdk/src/token/schemas.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { GasRouterConfigSchema } from '../router/schemas.js'; +import { isCompliant } from '../utils/schemas.js'; import { TokenType } from './config.js'; @@ -72,11 +73,6 @@ export type TokenRouterConfig = z.infer; export type NativeConfig = z.infer; export type CollateralConfig = z.infer; -function isCompliant(schema: S) { - return (config: unknown): config is z.infer => - schema.safeParse(config).success; -} - export const isSyntheticConfig = isCompliant(SyntheticConfigSchema); export const isCollateralConfig = isCompliant(CollateralConfigSchema); export const isNativeConfig = isCompliant(NativeConfigSchema); 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; +} From 570a8c09024a63259e1ee7b34f639012145a2204 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Wed, 22 May 2024 19:23:50 -0400 Subject: [PATCH 12/17] Address pr comments --- typescript/cli/src/config/chain.ts | 6 +++--- typescript/cli/src/config/warp.ts | 33 +++++++++++++++++------------ typescript/cli/src/deploy/warp.ts | 19 ++++++++++------- typescript/cli/src/utils/chains.ts | 16 ++++++++------ typescript/sdk/src/index.ts | 1 + typescript/sdk/src/token/schemas.ts | 31 ++++++++++++--------------- 6 files changed, 57 insertions(+), 49 deletions(-) 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 e5649b7b2a..e43a739bc6 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -10,7 +10,10 @@ import { import { CommandContext } from '../context/types.js'; import { errorRed, logBlue, logGreen } from '../logger.js'; -import { runMultiChainSelectionStep } from '../utils/chains.js'; +import { + detectAndConfirmOrPrompt, + runMultiChainSelectionStep, +} from '../utils/chains.js'; import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js'; const TYPE_DESCRIPTIONS: Record = { @@ -61,11 +64,11 @@ export async function createWarpRouteDeployConfig({ }) { logBlue('Creating a new warp route deployment config'); - const owner = - (await context.signer?.getAddress()) ?? - (await input({ - message: 'Enter owner address', - })); + const owner = await detectAndConfirmOrPrompt( + async () => context.signer?.getAddress(), + 'Enter the desired', + 'owner address', + ); const warpChains = await runMultiChainSelectionStep( context.chainMetadata, @@ -73,6 +76,7 @@ export async function createWarpRouteDeployConfig({ ); const result: WarpRouteDeployConfig = {}; + WarpRouteDeployConfigSchema; for (const chain of warpChains) { logBlue(`Configuring warp route for chain ${chain}`); const type = await select({ @@ -84,13 +88,14 @@ export async function createWarpRouteDeployConfig({ const isNft = type === TokenType.syntheticUri || type === TokenType.collateralUri; - // TODO: migrate to detectAndConfirmOrPrompt - const addresses = await context.registry.getChainAddresses(chain); - const mailbox = - addresses?.mailbox ?? - (await input({ - message: `Enter the mailbox address for chain ${chain}`, - })); + const mailbox = await detectAndConfirmOrPrompt( + async () => { + const addresses = await context.registry.getChainAddresses(chain); + return addresses?.mailbox; + }, + `For ${chain}, enter the`, + 'mailbox address', + ); switch (type) { case TokenType.collateral: @@ -105,7 +110,7 @@ export async function createWarpRouteDeployConfig({ owner, isNft, token: await input({ - message: `Enter the existing token address for chain ${chain}`, + message: `Enter the existing token address on chain ${chain}`, }), }; break; diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 8d4514e241..98ca107412 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -10,6 +10,7 @@ import { WarpCoreConfig, WarpRouteDeployConfig, getTokenConnectionId, + isTokenMetadata, } from '@hyperlane-xyz/sdk'; import { ProtocolType } from '@hyperlane-xyz/utils'; @@ -127,13 +128,6 @@ async function executeDeploy(params: DeployParams) { logBlue('Deployment is complete!'); } -const DEFAULT_METADATA = { - name: 'unknown', - symbol: 'unknown', - totalSupply: '0', - decimals: 18, -}; - async function getWarpCoreConfig( { configMap, context }: DeployParams, contracts: HyperlaneContractsMap, @@ -150,17 +144,26 @@ async function getWarpCoreConfig( for (const [chainName, contract] of Object.entries(contracts)) { const config = configMap[chainName]; const metadata = { - ...DEFAULT_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], ...metadata, + decimals, addressOrDenom: contract[configMap[chainName].type as keyof TokenFactories].address, collateralAddressOrDenom, 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/sdk/src/index.ts b/typescript/sdk/src/index.ts index aaccf2f890..aceb6509ed 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -464,6 +464,7 @@ export { isCollateralConfig, isNativeConfig, isSyntheticConfig, + isTokenMetadata, } from './token/schemas.js'; export { isCompliant } from './utils/schemas.js'; export { TokenRouterConfig, WarpRouteDeployConfig } from './token/types.js'; diff --git a/typescript/sdk/src/token/schemas.ts b/typescript/sdk/src/token/schemas.ts index 5c7635b0f6..3ec33bf8f3 100644 --- a/typescript/sdk/src/token/schemas.ts +++ b/typescript/sdk/src/token/schemas.ts @@ -23,7 +23,9 @@ export const CollateralConfigSchema = TokenMetadataSchema.partial().extend({ TokenType.fastCollateral, TokenType.collateralVault, ]), - token: z.string(), + token: z + .string() + .describe('Existing token address to extend with Warp Route functionality'), }); export const NativeConfigSchema = TokenMetadataSchema.partial().extend({ @@ -53,22 +55,6 @@ export const TokenRouterConfigSchema = TokenConfigSchema.and( GasRouterConfigSchema, ); -export const WarpRouteDeployConfigSchema = z - .record(TokenRouterConfigSchema) - .refine((configMap) => { - const entries = Object.entries(configMap); - return ( - entries.some( - ([_, config]) => - CollateralConfigSchema.safeParse(config).success || - NativeConfigSchema.safeParse(config).success, - ) || - entries.filter( - ([_, config]) => TokenMetadataSchema.safeParse(config).success, - ).length === entries.length - ); - }, `Config must include Native or Collateral OR all synthetics must define token metadata`); - export type TokenRouterConfig = z.infer; export type NativeConfig = z.infer; export type CollateralConfig = z.infer; @@ -77,3 +63,14 @@ 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`); From 80b2ebe825b642ef724171bac34b13da7953b160 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Wed, 22 May 2024 19:27:17 -0400 Subject: [PATCH 13/17] Address more pr comments --- typescript/cli/src/config/warp.ts | 9 +++++---- typescript/cli/src/deploy/warp.ts | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index e43a739bc6..04a5f2d4b0 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -119,14 +119,15 @@ export async function createWarpRouteDeployConfig({ } } - 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`, ); - WarpRouteDeployConfigSchema.parse(result); // throws error + throw e; } } diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 98ca107412..20831c4e07 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -68,14 +68,15 @@ export async function runWarpRouteDeploy({ logBlue('Warp route deployment plan'); await runDeployPlanStep(deploymentParams); + const chains = Object.keys(warpRouteConfig); + await runPreflightChecksForChains({ context, - chains: Object.keys(warpRouteConfig), + chains, minGas: MINIMUM_WARP_DEPLOY_GAS, }); const userAddress = await signer.getAddress(); - const chains = Object.keys(warpRouteConfig); const initialBalances = await prepareDeploy(context, userAddress, chains); From 583586f1c648001e3f96f1b6907f3ee629194808 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Thu, 23 May 2024 10:18:38 -0400 Subject: [PATCH 14/17] Fix ts ignore --- typescript/sdk/src/token/checker.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/typescript/sdk/src/token/checker.ts b/typescript/sdk/src/token/checker.ts index 3855b1e8da..48b38841fb 100644 --- a/typescript/sdk/src/token/checker.ts +++ b/typescript/sdk/src/token/checker.ts @@ -33,7 +33,7 @@ export class HypERC20Checker extends HyperlaneRouterChecker< config: TokenRouterConfig, ): Promise => { const checks: { - method: keyof TokenMetadata | 'decimals'; + method: keyof ERC20 & keyof TokenMetadata; violationType: string; }[] = [ { method: 'symbol', violationType: 'TokenSymbolMismatch' }, @@ -42,11 +42,6 @@ export class HypERC20Checker extends HyperlaneRouterChecker< ]; for (const check of checks) { - if (!(check.method in token)) { - continue; - } - - // @ts-ignore const actual = await token[check.method](); const expected = config[check.method]; if (actual !== expected) { From fb94b85edd503b2c365bc7409372940aa9fb9862 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Thu, 23 May 2024 11:07:13 -0400 Subject: [PATCH 15/17] Add comment about assertion --- typescript/sdk/src/token/deploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/sdk/src/token/deploy.ts b/typescript/sdk/src/token/deploy.ts index e53eaeb766..ad865c4f02 100644 --- a/typescript/sdk/src/token/deploy.ts +++ b/typescript/sdk/src/token/deploy.ts @@ -57,7 +57,7 @@ abstract class TokenDeployer< } else if (isNativeConfig(config)) { return config.scale ? [config.scale, config.mailbox] : [config.mailbox]; } else if (isSyntheticConfig(config)) { - assert(config.decimals); + assert(config.decimals); // decimals must be defined by this point return [config.decimals, config.mailbox]; } else { throw new Error('Unknown token type when constructing arguments'); From 0bd82f1ce892481e90120ca005e040a9b6ecadb4 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Thu, 23 May 2024 11:37:19 -0400 Subject: [PATCH 16/17] Fill warp route defaults after reading from file --- typescript/cli/src/commands/config.ts | 2 +- typescript/cli/src/config/warp.ts | 41 ++++++++++++++++++++++++--- typescript/cli/src/deploy/warp.ts | 3 +- 3 files changed, 40 insertions(+), 6 deletions(-) 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/warp.ts b/typescript/cli/src/config/warp.ts index 04a5f2d4b0..189d5198cf 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -1,12 +1,15 @@ import { input, select } from '@inquirer/prompts'; import { + ChainMap, + MailboxClientConfig, TokenType, WarpCoreConfig, WarpCoreConfigSchema, WarpRouteDeployConfig, WarpRouteDeployConfigSchema, } from '@hyperlane-xyz/sdk'; +import { assert, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; import { CommandContext } from '../context/types.js'; import { errorRed, logBlue, logGreen } from '../logger.js'; @@ -42,12 +45,43 @@ const TYPE_CHOICES = Object.values(TokenType).map((type) => ({ description: TYPE_DESCRIPTIONS[type], })); -export function readWarpRouteDeployConfig( +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); } @@ -76,7 +110,6 @@ export async function createWarpRouteDeployConfig({ ); const result: WarpRouteDeployConfig = {}; - WarpRouteDeployConfigSchema; for (const chain of warpChains) { logBlue(`Configuring warp route for chain ${chain}`); const type = await select({ diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 20831c4e07..eb07f189af 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -56,8 +56,9 @@ export async function runWarpRouteDeploy({ `Using warp route deployment config at ${warpRouteDeploymentConfigPath}`, ); } - const warpRouteConfig = readWarpRouteDeployConfig( + const warpRouteConfig = await readWarpRouteDeployConfig( warpRouteDeploymentConfigPath, + context, ); const deploymentParams = { From 5b10569e2cf243681b201960387c3a80c303b81a Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Thu, 23 May 2024 11:58:59 -0400 Subject: [PATCH 17/17] Fix e2e tests --- typescript/cli/src/tests/deployTestErc20.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/typescript/cli/src/tests/deployTestErc20.ts b/typescript/cli/src/tests/deployTestErc20.ts index eeea603dd3..be49a13daf 100644 --- a/typescript/cli/src/tests/deployTestErc20.ts +++ b/typescript/cli/src/tests/deployTestErc20.ts @@ -1,9 +1,8 @@ -import { AddressZero } from '@ethersproject/constants'; 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); @@ -20,18 +19,13 @@ 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, - owner: signer.address, - mailbox: AddressZero, }, [chain2]: { type: TokenType.synthetic, - owner: signer.address, - mailbox: AddressZero, }, };