feat(cli): Add hyperlane warp extend (#4107)

### Description
- Adds "extend warp route" logic to `hyperlane warp update`
- To extend, provide the existing `warp-route-deployment.yaml` with the
additional `TokenRouterConfig`

For example, considering `hyperlane warp deploy` is called with this
config:
```yaml
alpha:
  interchainSecurityModule:
    modules:
      - relayer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
        type: trustedRelayerIsm
      - domains: {}
        owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
        type: defaultFallbackRoutingIsm
    threshold: 1
    type: staticAggregationIsm
  isNft: false
  mailbox: '0xe04e5D673F3f253BF43A82ce66Bba0Ae771CF09B'
  owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
  type: native
```

To extend, add a new TokenRouter config:
```yaml
alpha:
  interchainSecurityModule:
    modules:
      - relayer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
        type: trustedRelayerIsm
      - domains: {}
        owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
        type: defaultFallbackRoutingIsm
    threshold: 1
    type: staticAggregationIsm
  isNft: false
  mailbox: '0xe04e5D673F3f253BF43A82ce66Bba0Ae771CF09B'
  owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
  type: native
arbitrum:
  interchainSecurityModule:
    modules:
      - relayer: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
        type: trustedRelayerIsm
      - domains: {}
        owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
        type: defaultFallbackRoutingIsm
    threshold: 1
    type: staticAggregationIsm
  isNft: false
  mailbox: '0x979Ca5202784112f4738403dBec5D0F3B9daabB9'
  owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
  type: native
```

The newly deployed `arbitrum` `TokenRouterConfig` will deploy the
Router, and then enroll the existing `alpha` Router with each other.

### Drive by
- Fix native deriving to send 0 wei instead of 1

### Related issues

https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4078

### Testing
Manual 
- Tested with deploying 
  - native and extending with native
  - native and extending with native, and native
  - native and extending with synthetic
- use `cast abi-decode "domain()(uint32[])" $(cast call $TOKEN_ROUTER
"domains()")` to test enrollment

E2e test

---------

Co-authored-by: Noah Bayindirli 🥂 <noah@primeprotocol.xyz>
mo/verify-proxy-contracts
Lee 4 months ago committed by GitHub
parent bf1dad7f77
commit 62d71fad35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/shy-ants-wave.md
  2. 310
      typescript/cli/src/deploy/warp.ts
  3. 3
      typescript/sdk/src/index.ts
  4. 4
      typescript/sdk/src/router/schemas.ts
  5. 4
      typescript/sdk/src/router/types.ts
  6. 4
      typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts
  7. 55
      typescript/sdk/src/token/EvmERC20WarpModule.ts
  8. 8
      typescript/sdk/src/token/EvmERC20WarpRouteReader.ts

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---
Add hyperlane warp update to extend a warp config

@ -1,19 +1,25 @@
import { confirm } from '@inquirer/prompts';
import { ContractReceipt } from 'ethers';
import { stringify as yamlStringify } from 'yaml';
import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js';
import { IRegistry } from '@hyperlane-xyz/registry';
import {
AggregationIsmConfig,
AnnotatedEV5Transaction,
ChainMap,
ChainName,
ContractVerifier,
EvmERC20WarpModule,
EvmERC20WarpRouteReader,
EvmIsmModule,
ExplorerLicenseType,
HypERC20Deployer,
HypERC20Factories,
HypERC721Deployer,
HypERC721Factories,
HyperlaneAddresses,
HyperlaneContracts,
HyperlaneContractsMap,
HyperlaneProxyFactoryDeployer,
IsmType,
@ -22,20 +28,29 @@ import {
OpStackIsmConfig,
PausableIsmConfig,
ProxyFactoryFactoriesAddresses,
RemoteRouters,
RoutingIsmConfig,
TOKEN_TYPE_TO_STANDARD,
TokenFactories,
TrustedRelayerIsmConfig,
WarpCoreConfig,
WarpCoreConfigSchema,
WarpRouteDeployConfig,
WarpRouteDeployConfigSchema,
attachContractsMap,
connectContractsMap,
getTokenConnectionId,
hypERC20factories,
isCollateralConfig,
isTokenMetadata,
serializeContracts,
} from '@hyperlane-xyz/sdk';
import {
Address,
ProtocolType,
assert,
objFilter,
objKeys,
objMap,
promiseObjAll,
} from '@hyperlane-xyz/utils';
@ -126,7 +141,14 @@ export async function runWarpRouteDeploy({
const initialBalances = await prepareDeploy(context, userAddress, chains);
await executeDeploy(deploymentParams, apiKeys);
const deployedContracts = await executeDeploy(deploymentParams, apiKeys);
const warpCoreConfig = await getWarpCoreConfig(
deploymentParams,
deployedContracts,
);
await writeDeploymentArtifacts(warpCoreConfig, context);
await completeDeploy(context, 'warp', initialBalances, userAddress, chains);
}
@ -144,7 +166,10 @@ async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) {
if (!isConfirmed) throw new Error('Deployment cancelled');
}
async function executeDeploy(params: DeployParams, apiKeys: ChainMap<string>) {
async function executeDeploy(
params: DeployParams,
apiKeys: ChainMap<string>,
): Promise<HyperlaneContractsMap<HypERC20Factories | HypERC721Factories>> {
logBlue('🚀 All systems ready, captain! Beginning deployment...');
const {
@ -154,7 +179,7 @@ async function executeDeploy(params: DeployParams, apiKeys: ChainMap<string>) {
const deployer = warpDeployConfig.isNft
? new HypERC721Deployer(multiProvider)
: new HypERC20Deployer(multiProvider);
: new HypERC20Deployer(multiProvider); // TODO: replace with EvmERC20WarpModule
const config: WarpRouteDeployConfig =
isDryRun && dryRunChain
@ -185,12 +210,17 @@ async function executeDeploy(params: DeployParams, apiKeys: ChainMap<string>) {
const deployedContracts = await deployer.deploy(modifiedConfig);
const warpCoreConfig = await getWarpCoreConfig(params, deployedContracts);
logGreen('✅ Warp contract deployments complete');
return deployedContracts;
}
if (!isDryRun) {
async function writeDeploymentArtifacts(
warpCoreConfig: WarpCoreConfig,
context: WriteCommandContext,
) {
if (!context.isDryRun) {
log('Writing deployment artifacts...');
await registry.addWarpRoute(warpCoreConfig);
await context.registry.addWarpRoute(warpCoreConfig);
}
log(indentYamlOrJson(yamlStringify(warpCoreConfig, null, 2), 4));
}
@ -204,7 +234,6 @@ async function deployAndResolveWarpIsm(
): Promise<WarpRouteDeployConfig> {
return promiseObjAll(
objMap(warpConfig, async (chain, config) => {
// Skip deployment if Ism is empty, or a string
if (
!config.interchainSecurityModule ||
typeof config.interchainSecurityModule === 'string'
@ -214,23 +243,25 @@ async function deployAndResolveWarpIsm(
!config.interchainSecurityModule
? 'empty'
: config.interchainSecurityModule
}, skipping deployment`,
}, skipping deployment.`,
);
return config;
}
logBlue('Loading Registry factory addresses');
let chainAddresses = await registry.getChainAddresses(chain); // Can includes other addresses
logBlue(`Loading registry factory addresses for ${chain}...`);
let chainAddresses = await registry.getChainAddresses(chain);
if (!chainAddresses) {
logGray('Registry factory addresses not found, deploying');
logGray(
`Registry factory addresses not found for ${chain}. Deploying...`,
);
chainAddresses = serializeContracts(
await ismFactoryDeployer.deployContracts(chain),
) as Record<string, string>;
}
logGray(
`Creating ${config.interchainSecurityModule.type} Ism for ${config.type} token on ${chain} chain`,
`Creating ${config.interchainSecurityModule.type} ISM for ${config.type} token on ${chain} chain...`,
);
const deployedIsm = await createWarpIsm(
@ -252,7 +283,7 @@ async function deployAndResolveWarpIsm(
);
logGreen(
`Finished creating ${config.interchainSecurityModule.type} Ism for ${config.type} token on ${chain} chain`,
`Finished creating ${config.interchainSecurityModule.type} ISM for ${config.type} token on ${chain} chain.`,
);
return { ...warpConfig[chain], interchainSecurityModule: deployedIsm };
}),
@ -314,7 +345,31 @@ async function getWarpCoreConfig(
const { decimals, symbol, name } = tokenMetadata;
assert(decimals, 'Missing decimals on token metadata');
// First pass, create token configs
generateTokenConfigs(
warpCoreConfig,
warpDeployConfig,
contracts,
symbol,
name,
decimals,
);
fullyConnectTokens(warpCoreConfig);
return warpCoreConfig;
}
/**
* Creates token configs.
*/
function generateTokenConfigs(
warpCoreConfig: WarpCoreConfig,
warpDeployConfig: WarpRouteDeployConfig,
contracts: HyperlaneContractsMap<TokenFactories>,
symbol: string,
name: string,
decimals: number,
): void {
for (const [chainName, contract] of Object.entries(contracts)) {
const config = warpDeployConfig[chainName];
const collateralAddressOrDenom = isCollateralConfig(config)
@ -333,10 +388,15 @@ async function getWarpCoreConfig(
collateralAddressOrDenom,
});
}
}
// Second pass, add connections between tokens
// Assumes full interconnectivity between all tokens for now b.c. that's
// what the deployers do by default.
/**
* Adds connections between tokens.
*
* Assumes full interconnectivity between all tokens for now b.c. that's
* what the deployers do by default.
*/
function fullyConnectTokens(warpCoreConfig: WarpCoreConfig): void {
for (const token1 of warpCoreConfig.tokens) {
for (const token2 of warpCoreConfig.tokens) {
if (
@ -354,24 +414,20 @@ async function getWarpCoreConfig(
});
}
}
return warpCoreConfig;
}
export async function runWarpRouteApply(params: ApplyParams) {
const {
warpDeployConfig,
warpCoreConfig,
context: { registry, multiProvider, chainMetadata, skipConfirmation },
} = params;
// Addresses used to get static Ism factories
export async function runWarpRouteApply(params: ApplyParams): Promise<void> {
const { warpDeployConfig, warpCoreConfig, context } = params;
const { registry, multiProvider, chainMetadata, skipConfirmation } = context;
WarpRouteDeployConfigSchema.parse(warpDeployConfig);
WarpCoreConfigSchema.parse(warpCoreConfig);
const addresses = await registry.getAddresses();
// Convert warpCoreConfig.tokens[] into a mapping of { [chainName]: Config }
// This allows O(1) reads within the loop
const warpCoreByChain = Object.fromEntries(
warpCoreConfig.tokens.map((token) => [token.chainName, token]),
const warpCoreConfigByChain = Object.fromEntries(
warpCoreConfig.tokens.map((token) => [
token.chainName,
token,
]) /* Necessary for O(1) reads below */,
);
const chains = Object.keys(warpDeployConfig);
@ -387,48 +443,172 @@ export async function runWarpRouteApply(params: ApplyParams) {
ExplorerLicenseType.MIT,
);
// Attempt to update Warp Routes
// Can update existing or deploy new contracts
logGray(`Comparing target and onchain Warp configs`);
await promiseObjAll(
objMap(warpDeployConfig, async (chain, config) => {
try {
// Update Warp
config.ismFactoryAddresses = addresses[
chain
] as ProxyFactoryFactoriesAddresses;
const evmERC20WarpModule = new EvmERC20WarpModule(
multiProvider,
{
config,
chain,
addresses: {
deployedTokenRoute: warpCoreByChain[chain].addressOrDenom!,
const warpDeployChains = Object.keys(warpDeployConfig);
const warpCoreChains = Object.keys(warpCoreConfigByChain);
if (warpDeployChains.length === warpCoreChains.length) {
logGray('Updating deployed Warp Routes');
await promiseObjAll(
objMap(warpDeployConfig, async (chain, config) => {
try {
config.ismFactoryAddresses = addresses[
chain
] as ProxyFactoryFactoriesAddresses;
const evmERC20WarpModule = new EvmERC20WarpModule(
multiProvider,
{
config,
chain,
addresses: {
deployedTokenRoute:
warpCoreConfigByChain[chain].addressOrDenom!,
},
},
},
contractVerifier,
);
const transactions = await evmERC20WarpModule.update(config);
// Send Txs
if (transactions.length) {
for (const transaction of transactions) {
await multiProvider.sendTransaction(chain, transaction);
}
logGreen(`Warp config updated on ${chain}.`);
} else {
logGreen(
`Warp config on ${chain} is the same as target. No updates needed.`,
contractVerifier,
);
const transactions = await evmERC20WarpModule.update(config);
if (transactions.length) {
for (const transaction of transactions) {
await multiProvider.sendTransaction(chain, transaction);
}
logGreen(`Warp config updated on ${chain}.`);
} else {
logGreen(
`Warp config on ${chain} is the same as target. No updates needed.`,
);
}
} catch (e) {
logRed(`Warp config on ${chain} failed to update.`, e);
}
} catch (e) {
logRed(`Warp config on ${chain} failed to update.`, e);
}),
);
} else if (warpDeployChains.length > warpCoreChains.length) {
logGray('Extending deployed Warp configs');
// Split between the existing and additional config
const existingConfigs: WarpRouteDeployConfig = objFilter(
warpDeployConfig,
(chain, _config): _config is any => warpCoreChains.includes(chain),
);
let extendedConfigs: WarpRouteDeployConfig = objFilter(
warpDeployConfig,
(chain, _config): _config is any => !warpCoreChains.includes(chain),
);
const existingTokenMetadata = await HypERC20Deployer.deriveTokenMetadata(
multiProvider,
existingConfigs,
);
extendedConfigs = objMap(extendedConfigs, (_chain, extendedConfig) => {
return {
...extendedConfig,
...existingTokenMetadata,
};
});
const newExtensionContracts = await executeDeploy(
{
// TODO: use EvmERC20WarpModule when it's ready
context,
warpDeployConfig: extendedConfigs,
},
apiKeys,
);
const existingContractAddresses = objMap(
existingConfigs,
(chain, config) => ({
[config.type]: warpCoreConfigByChain[chain].addressOrDenom!,
}),
);
const mergedRouters = {
...connectContractsMap(
attachContractsMap(existingContractAddresses, hypERC20factories),
multiProvider,
),
...newExtensionContracts,
} as HyperlaneContractsMap<HypERC20Factories>;
await enrollRemoteRouters(mergedRouters, multiProvider);
const updatedWarpCoreConfig = await getWarpCoreConfig(
params,
mergedRouters,
);
WarpCoreConfigSchema.parse(updatedWarpCoreConfig);
await writeDeploymentArtifacts(updatedWarpCoreConfig, context);
} else {
throw new Error('Unenrolling warp routes is currently not supported');
}
}
/**
* Enroll all deployed routers with each other.
*
* @param deployedContractsMap - A map of deployed Hyperlane contracts by chain.
* @param multiProvider - A MultiProvider instance to interact with multiple chains.
*/
async function enrollRemoteRouters(
deployedContractsMap: HyperlaneContractsMap<HypERC20Factories>,
multiProvider: MultiProvider,
): Promise<void> {
logBlue(`Enrolling deployed routers with each other (if not already)...`);
const deployedRouters: ChainMap<Address> = objMap(
deployedContractsMap,
(_, contracts) => getRouter(contracts).address,
);
const allChains = Object.keys(deployedRouters);
await promiseObjAll(
objMap(deployedContractsMap, async (chain, contracts) => {
const router = getRouter(contracts); // Assume deployedContract always has 1 value
// Mutate the config.remoteRouters by setting it to all other routers to update
const warpRouteReader = new EvmERC20WarpRouteReader(multiProvider, chain);
const mutatedWarpRouteConfig =
await warpRouteReader.deriveWarpRouteConfig(router.address);
const evmERC20WarpModule = new EvmERC20WarpModule(multiProvider, {
config: mutatedWarpRouteConfig,
chain,
addresses: { deployedTokenRoute: router.address },
});
const otherChains = multiProvider
.getRemoteChains(chain)
.filter((c) => allChains.includes(c));
mutatedWarpRouteConfig.remoteRouters = otherChains.reduce<RemoteRouters>(
(remoteRouters, chain) => {
remoteRouters[multiProvider.getDomainId(chain)] =
deployedRouters[chain];
return remoteRouters;
},
{},
);
const mutatedConfigTxs: AnnotatedEV5Transaction[] =
await evmERC20WarpModule.update(mutatedWarpRouteConfig);
for (const transaction of mutatedConfigTxs) {
const receipt: ContractReceipt = await multiProvider.sendTransaction(
chain,
transaction,
);
logGreen(
`Successfully enrolled routers on ${chain}: ${receipt.transactionHash}`,
);
}
}),
);
}
function getRouter(contracts: HyperlaneContracts<HypERC20Factories>) {
for (const key of objKeys(hypERC20factories)) {
if (contracts[key]) return contracts[key];
}
throw new Error('No matching contract found.');
}
function displayWarpDeployPlan(deployConfig: WarpRouteDeployConfig) {
logBlue('\nWarp Route Deployment Plan');
logGray('==========================');

@ -30,6 +30,7 @@ export {
attachContracts,
attachContractsMap,
attachContractsMapAndGetForeignDeployments,
attachAndConnectContracts,
connectContracts,
connectContractsMap,
filterAddressesMap,
@ -377,6 +378,7 @@ export {
RouterConfig,
RouterViolation,
RouterViolationType,
RemoteRouters,
proxiedFactories,
} from './router/types.js';
export {
@ -507,3 +509,4 @@ export { DeployedCoreAdresses, EvmCoreModule } from './core/EvmCoreModule.js';
export { EvmIsmModule } from './ism/EvmIsmModule.js';
export { EvmERC20WarpModule } from './token/EvmERC20WarpModule.js';
export { ProxyFactoryFactoriesAddresses } from './deploy/schemas.js';
export { AnnotatedEV5Transaction } from './providers/ProviderType.js';

@ -19,7 +19,7 @@ export const ForeignDeploymentConfigSchema = z.object({
const RemoteRouterDomain = z.string();
const RemoteRouterRouter = z.string().startsWith('0x');
export const RemoteRouterSchema = z.record(
export const RemoteRoutersSchema = z.record(
RemoteRouterDomain,
RemoteRouterRouter,
);
@ -28,7 +28,7 @@ export const RouterConfigSchema = MailboxClientConfigSchema.merge(
ForeignDeploymentConfigSchema,
).merge(
z.object({
remoteRouters: RemoteRouterSchema.optional(),
remoteRouters: RemoteRoutersSchema.optional(),
}),
);

@ -15,7 +15,7 @@ import { CheckerViolation } from '../deploy/types.js';
import {
GasRouterConfigSchema,
MailboxClientConfigSchema,
RemoteRouterSchema,
RemoteRoutersSchema,
RouterConfigSchema,
} from './schemas.js';
@ -63,4 +63,4 @@ export interface RouterViolation extends CheckerViolation {
description?: string;
}
export type RemoteRouter = z.infer<typeof RemoteRouterSchema>;
export type RemoteRouters = z.infer<typeof RemoteRoutersSchema>;

@ -33,7 +33,7 @@ import { ProxyFactoryFactories } from '../deploy/contracts.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { RemoteRouter } from '../router/types.js';
import { RemoteRouters } from '../router/types.js';
import { randomAddress } from '../test/testUtils.js';
import { ChainMap } from '../types.js';
@ -42,7 +42,7 @@ import { TokenType } from './config.js';
import { TokenRouterConfig } from './schemas.js';
const randomRemoteRouters = (n: number) => {
const routers: RemoteRouter = {};
const routers: RemoteRouters = {};
for (let domain = 0; domain < n; domain++) {
routers[domain] = randomAddress();
}

@ -134,7 +134,7 @@ export class EvmERC20WarpModule extends HyperlaneModule<
);
updateTransactions.push({
annotation: `Enrolling Router ${this.args.addresses.deployedTokenRoute}}`,
annotation: `Enrolling Router ${this.args.addresses.deployedTokenRoute} on ${this.args.chain}`,
chainId: this.domainId,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData(
@ -166,34 +166,37 @@ export class EvmERC20WarpModule extends HyperlaneModule<
if (!expectedConfig.interchainSecurityModule) {
return [];
}
const actualDeployedIsm = (
actualConfig.interchainSecurityModule as DerivedIsmConfig
).address;
// Try to update (may also deploy) Ism with the expected config
const {
deployedIsm: expectedDeployedIsm,
updateTransactions: ismUpdateTransactions,
} = await this.deployOrUpdateIsm(actualConfig, expectedConfig);
if (expectedConfig.ismFactoryAddresses) {
const actualDeployedIsm = (
actualConfig.interchainSecurityModule as DerivedIsmConfig
).address;
// If an ISM is updated in-place, push the update txs
updateTransactions.push(...ismUpdateTransactions);
// Try to update (may also deploy) Ism with the expected config
const {
deployedIsm: expectedDeployedIsm,
updateTransactions: ismUpdateTransactions,
} = await this.deployOrUpdateIsm(actualConfig, expectedConfig);
// If a new ISM is deployed, push the setInterchainSecurityModule tx
if (actualDeployedIsm !== expectedDeployedIsm) {
const contractToUpdate = MailboxClient__factory.connect(
this.args.addresses.deployedTokenRoute,
this.multiProvider.getProvider(this.domainId),
);
updateTransactions.push({
annotation: `Setting ISM for Warp Route to ${expectedDeployedIsm}`,
chainId: this.domainId,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData(
'setInterchainSecurityModule',
[expectedDeployedIsm],
),
});
// If an ISM is updated in-place, push the update txs
updateTransactions.push(...ismUpdateTransactions);
// If a new ISM is deployed, push the setInterchainSecurityModule tx
if (actualDeployedIsm !== expectedDeployedIsm) {
const contractToUpdate = MailboxClient__factory.connect(
this.args.addresses.deployedTokenRoute,
this.multiProvider.getProvider(this.domainId),
);
updateTransactions.push({
annotation: `Setting ISM for Warp Route to ${expectedDeployedIsm}`,
chainId: this.domainId,
to: contractToUpdate.address,
data: contractToUpdate.interface.encodeFunctionData(
'setInterchainSecurityModule',
[expectedDeployedIsm],
),
});
}
}
return updateTransactions;

@ -23,7 +23,7 @@ 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 { RemoteRouter } from '../router/types.js';
import { RemoteRouters } from '../router/types.js';
import { ChainNameOrId } from '../types.js';
import { CollateralExtensions } from './config.js';
@ -115,12 +115,12 @@ export class EvmERC20WarpRouteReader {
}
// Finally check native
// Using estimateGas to send 1 wei. Success implies that the Warp Route has a receive() function
// Using estimateGas to send 0 wei. Success implies that the Warp Route has a receive() function
try {
await this.multiProvider.estimateGas(this.chain, {
to: warpRouteAddress,
from: await this.multiProvider.getSignerAddress(this.chain),
value: BigNumber.from(1),
value: BigNumber.from(0),
});
return TokenType.native;
} catch (e) {
@ -219,7 +219,7 @@ export class EvmERC20WarpRouteReader {
return { name, symbol, decimals, totalSupply: totalSupply.toString() };
}
async fetchRemoteRouters(warpRouteAddress: Address): Promise<RemoteRouter> {
async fetchRemoteRouters(warpRouteAddress: Address): Promise<RemoteRouters> {
const warpRoute = TokenRouter__factory.connect(
warpRouteAddress,
this.provider,

Loading…
Cancel
Save