Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multi collateral warp routes #3820

Merged
merged 20 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/nice-rivers-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@hyperlane-xyz/infra': minor
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---

Implement multi collateral warp routes
2 changes: 1 addition & 1 deletion typescript/cli/src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down
6 changes: 3 additions & 3 deletions typescript/cli/src/config/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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(
Expand All @@ -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,
);
Expand Down
182 changes: 117 additions & 65 deletions typescript/cli/src/config/warp.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,87 @@
import { confirm, input } from '@inquirer/prompts';
import { ethers } from 'ethers';
import { input, select } from '@inquirer/prompts';

import {
ChainMetadata,
ChainMap,
MailboxClientConfig,
TokenType,
WarpCoreConfig,
WarpCoreConfigSchema,
WarpRouteDeployConfig,
WarpRouteDeployConfigSchema,
} from '@hyperlane-xyz/sdk';
import { objFilter } from '@hyperlane-xyz/utils';
import { assert, objMap, promiseObjAll } from '@hyperlane-xyz/utils';

import { CommandContext } from '../context/types.js';
import { errorRed, logBlue, logGreen } from '../logger.js';
import {
detectAndConfirmOrPrompt,
runMultiChainSelectionStep,
runSingleChainSelectionStep,
} from '../utils/chains.js';
import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js';

export function readWarpRouteDeployConfig(
const TYPE_DESCRIPTIONS: Record<TokenType, string> = {
[TokenType.synthetic]: 'A new ERC20 with remote transfer functionality',
[TokenType.collateral]:
'Extends an existing ERC20 with remote transfer functionality',
[TokenType.native]:
'Extends the native token with remote transfer functionality',
[TokenType.collateralVault]:
'Extends an existing ERC4626 with remote transfer functionality',
[TokenType.collateralFiat]:
'Extends an existing FiatToken with remote transfer functionality',
[TokenType.collateralXERC20]:
'Extends an existing xERC20 with Warp Route functionality',
// TODO: describe
[TokenType.fastSynthetic]: '',
[TokenType.syntheticUri]: '',
[TokenType.fastCollateral]: '',
[TokenType.collateralUri]: '',
[TokenType.nativeScaled]: '',
};

const TYPE_CHOICES = Object.values(TokenType).map((type) => ({
name: type,
value: type,
description: TYPE_DESCRIPTIONS[type],
}));

async function fillDefaults(
context: CommandContext,
config: ChainMap<Partial<MailboxClientConfig>>,
): Promise<ChainMap<MailboxClientConfig>> {
return promiseObjAll(
objMap(config, async (chain, config): Promise<MailboxClientConfig> => {
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<WarpRouteDeployConfig> {
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);
}

Expand All @@ -40,75 +97,70 @@ export async function createWarpRouteDeployConfig({
outPath: string;
}) {
logBlue('Creating a new warp route deployment config');
const baseChain = await runSingleChainSelectionStep(
context.chainMetadata,
'Select base chain with the original token to warp',

const owner = await detectAndConfirmOrPrompt(
async () => context.signer?.getAddress(),
'Enter the desired',
'owner address',
);

const isNative = await confirm({
message:
'Are you creating a route for the native token of the base chain (e.g. Ether on Ethereum)?',
});

const isNft = isNative
? false
: await confirm({ message: 'Is this an NFT (i.e. ERC-721)?' });
const isYieldBearing =
isNative || isNft
? false
: await confirm({
message:
'Do you want this warp route to be yield-bearing (i.e. deposits into ERC-4626 vault)?',
});

const addressMessage = `Enter the ${
isYieldBearing ? 'ERC-4626 vault' : 'collateral token'
} address`;
const baseAddress = isNative
? ethers.constants.AddressZero
: await input({ message: addressMessage });

const metadataWithoutBase = objFilter(
const warpChains = await runMultiChainSelectionStep(
context.chainMetadata,
(chain, _): _ is ChainMetadata => chain !== baseChain,
);
const syntheticChains = await runMultiChainSelectionStep(
metadataWithoutBase,
'Select chains to which the base token will be connected',
'Select chains to connect',
);

// TODO add more prompts here to support customizing the token metadata
let result: WarpRouteDeployConfig;
if (isNative) {
result = {
[baseChain]: {
type: TokenType.native,
},
};
} else {
result = {
[baseChain]: {
type: isYieldBearing ? TokenType.collateralVault : TokenType.collateral,
token: baseAddress,
isNft,
const result: WarpRouteDeployConfig = {};
for (const chain of warpChains) {
logBlue(`Configuring warp route for chain ${chain}`);
const type = await select({
message: `Select ${chain}'s token type`,
choices: TYPE_CHOICES,
});

// TODO: restore NFT prompting
const isNft =
type === TokenType.syntheticUri || type === TokenType.collateralUri;

const mailbox = await detectAndConfirmOrPrompt(
async () => {
const addresses = await context.registry.getChainAddresses(chain);
return addresses?.mailbox;
},
};
}
`For ${chain}, enter the`,
'mailbox address',
);

syntheticChains.map((chain) => {
result[chain] = {
type: TokenType.synthetic,
};
});
switch (type) {
case TokenType.collateral:
case TokenType.collateralXERC20:
case TokenType.collateralFiat:
case TokenType.collateralUri:
case TokenType.fastCollateral:
case TokenType.collateralVault:
result[chain] = {
mailbox,
type,
owner,
isNft,
token: await input({
message: `Enter the existing token address on chain ${chain}`,
}),
};
break;
default:
result[chain] = { mailbox, type, owner, isNft };
}
}

if (isValidWarpRouteDeployConfig(result)) {
try {
const parsed = WarpRouteDeployConfigSchema.parse(result);
logGreen(`Warp Route config is valid, writing to file ${outPath}`);
writeYamlOrJson(outPath, result);
} else {
writeYamlOrJson(outPath, parsed);
} catch (e) {
errorRed(
`Warp route deployment config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/warp-route-deployment.yaml for an example`,
yorhodes marked this conversation as resolved.
Show resolved Hide resolved
);
throw new Error('Invalid multisig config');
throw e;
}
}

Expand Down
30 changes: 0 additions & 30 deletions typescript/cli/src/deploy/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading