Update CLI Warp deployment output shape (#3322)

### Description

- Update CLI Warp deployment output shape to new WarpCore config
- Clarify that CLI warp config example is for deployment

### Related issues

Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3298

### Backward compatibility

No, rollout should correspond with Warp UI update:
https://github.com/hyperlane-xyz/hyperlane-warp-ui-template/pull/134

### Testing

Executed a basic warp route deployment
pull/3258/head
J M Rossy 8 months ago committed by GitHub
parent 76bd730103
commit 985adc91eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/short-boats-suffer.md
  2. 4
      typescript/cli/ci-test.sh
  3. 0
      typescript/cli/examples/fork/warp-route-deployment.yaml
  4. 0
      typescript/cli/examples/warp-route-deployment.yaml
  5. 17
      typescript/cli/src/commands/config.ts
  6. 13
      typescript/cli/src/commands/deploy.ts
  7. 28
      typescript/cli/src/config/warp.ts
  8. 27
      typescript/cli/src/deploy/types.ts
  9. 162
      typescript/cli/src/deploy/warp.ts
  10. 3
      typescript/sdk/src/index.ts
  11. 35
      typescript/sdk/src/token/TokenConnection.ts
  12. 12
      typescript/sdk/src/token/TokenStandard.ts
  13. 11
      typescript/sdk/src/warp/WarpCore.ts

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/cli': patch
---
Update CLI Warp route deployment output shape to new WarpCore config

@ -117,7 +117,7 @@ echo "Deploying warp routes"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy warp \
--chains ${EXAMPLES_PATH}/anvil-chains.yaml \
--core $CORE_ARTIFACTS_PATH \
--config ${EXAMPLES_PATH}/warp-tokens.yaml \
--config ${EXAMPLES_PATH}/warp-route-deployment.yaml \
--out /tmp \
--key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \
--yes
@ -146,7 +146,7 @@ echo "Gas used: $MSG_MIN_GAS"
MESSAGE1_ID=`cat /tmp/message1 | grep "Message ID" | grep -E -o '0x[0-9a-f]+'`
echo "Message 1 ID: $MESSAGE1_ID"
WARP_ARTIFACTS_FILE=`find /tmp/warp-deployment* -type f -exec ls -t1 {} + | head -1`
WARP_ARTIFACTS_FILE=`find /tmp/warp-route-deployment* -type f -exec ls -t1 {} + | head -1`
CHAIN1_ROUTER="${CHAIN1_CAPS}_ROUTER"
declare $CHAIN1_ROUTER=$(cat $WARP_ARTIFACTS_FILE | jq -r ".${CHAIN1}.router")

@ -8,7 +8,10 @@ import {
createMultisigConfig,
readMultisigConfig,
} from '../config/multisig.js';
import { createWarpConfig, readWarpRouteConfig } from '../config/warp.js';
import {
createWarpRouteDeployConfig,
readWarpRouteDeployConfig,
} from '../config/warp.js';
import { FileFormat } from '../utils/files.js';
import {
@ -43,7 +46,7 @@ const createCommand: CommandModule = {
.command(createChainConfigCommand)
.command(createIsmConfigCommand)
.command(createHookConfigCommand)
.command(createWarpConfigCommand)
.command(createWarpRouteDeployConfigCommand)
.version(false)
.demandCommand(),
handler: () => log('Command required'),
@ -113,12 +116,12 @@ const createHookConfigCommand: CommandModule = {
},
};
const createWarpConfigCommand: CommandModule = {
const createWarpRouteDeployConfigCommand: CommandModule = {
command: 'warp',
describe: 'Create a new Warp Route tokens config',
describe: 'Create a new Warp Route deployment config',
builder: (yargs) =>
yargs.options({
output: outputFileOption('./configs/warp-tokens.yaml'),
output: outputFileOption('./configs/warp-route-deployment.yaml'),
format: fileFormatOption,
chains: chainsCommandOption,
}),
@ -126,7 +129,7 @@ const createWarpConfigCommand: CommandModule = {
const format: FileFormat = argv.format;
const outPath: string = argv.output;
const chainConfigPath: string = argv.chains;
await createWarpConfig({ format, outPath, chainConfigPath });
await createWarpRouteDeployConfig({ format, outPath, chainConfigPath });
process.exit(0);
},
};
@ -217,7 +220,7 @@ const validateWarpCommand: CommandModule = {
}),
handler: async (argv) => {
const path = argv.path as string;
readWarpRouteConfig(path);
readWarpRouteDeployConfig(path);
logGreen('Config is valid');
process.exit(0);
},

@ -3,7 +3,7 @@ import { CommandModule } from 'yargs';
import { log, logGray } from '../../logger.js';
import { runKurtosisAgentDeploy } from '../deploy/agent.js';
import { runCoreDeploy } from '../deploy/core.js';
import { runWarpDeploy } from '../deploy/warp.js';
import { runWarpRouteDeploy } from '../deploy/warp.js';
import { ENV } from '../utils/env.js';
import {
@ -133,8 +133,9 @@ const warpCommand: CommandModule = {
yargs.options({
config: {
type: 'string',
description: 'A path to a JSON or YAML file with a warp config.',
default: './configs/warp-tokens.yaml',
description:
'A path to a JSON or YAML file with a warp route deployment config.',
default: './configs/warp-route-deployment.yaml',
},
core: coreArtifactsOption,
chains: chainsCommandOption,
@ -145,14 +146,14 @@ const warpCommand: CommandModule = {
handler: async (argv: any) => {
const key: string = argv.key || ENV.HYP_KEY;
const chainConfigPath: string = argv.chains;
const warpConfigPath: string | undefined = argv.config;
const warpRouteDeploymentConfigPath: string | undefined = argv.config;
const coreArtifactsPath: string | undefined = argv.core;
const outPath: string = argv.out;
const skipConfirmation: boolean = argv.yes;
await runWarpDeploy({
await runWarpRouteDeploy({
key,
chainConfigPath,
warpConfigPath,
warpRouteDeploymentConfigPath,
coreArtifactsPath,
outPath,
skipConfirmation,

@ -15,12 +15,11 @@ import { readChainConfigsIfExists } from './chain.js';
const ConnectionConfigSchema = {
mailbox: ZHash.optional(),
interchainGasPaymaster: ZHash.optional(),
interchainSecurityModule: ZHash.optional(),
foreignDeployment: z.string().optional(),
};
export const WarpRouteConfigSchema = z.object({
export const WarpRouteDeployConfigSchema = z.object({
base: z.object({
type: z.literal(TokenType.native).or(z.literal(TokenType.collateral)),
chainName: z.string(),
@ -44,17 +43,18 @@ export const WarpRouteConfigSchema = z.object({
.nonempty(),
});
type InferredType = z.infer<typeof WarpRouteConfigSchema>;
type InferredType = z.infer<typeof WarpRouteDeployConfigSchema>;
// A workaround for Zod's terrible typing for nonEmpty arrays
export type WarpRouteConfig = {
export type WarpRouteDeployConfig = {
base: InferredType['base'];
synthetics: Array<InferredType['synthetics'][0]>;
};
export function readWarpRouteConfig(filePath: string) {
export function readWarpRouteDeployConfig(filePath: string) {
const config = readYamlOrJson(filePath);
if (!config) throw new Error(`No warp config found at ${filePath}`);
const result = WarpRouteConfigSchema.safeParse(config);
if (!config)
throw new Error(`No warp route deploy config found at ${filePath}`);
const result = WarpRouteDeployConfigSchema.safeParse(config);
if (!result.success) {
const firstIssue = result.error.issues[0];
throw new Error(
@ -64,11 +64,11 @@ export function readWarpRouteConfig(filePath: string) {
return result.data;
}
export function isValidWarpRouteConfig(config: any) {
return WarpRouteConfigSchema.safeParse(config).success;
export function isValidWarpRouteDeployConfig(config: any) {
return WarpRouteDeployConfigSchema.safeParse(config).success;
}
export async function createWarpConfig({
export async function createWarpRouteDeployConfig({
format,
outPath,
chainConfigPath,
@ -77,7 +77,7 @@ export async function createWarpConfig({
outPath: string;
chainConfigPath: string;
}) {
logBlue('Creating a new warp route config');
logBlue('Creating a new warp route deployment config');
const customChains = readChainConfigsIfExists(chainConfigPath);
const baseChain = await runSingleChainSelectionStep(
customChains,
@ -104,7 +104,7 @@ export async function createWarpConfig({
// TODO add more prompts here to support customizing the token metadata
const result: WarpRouteConfig = {
const result: WarpRouteDeployConfig = {
base: {
chainName: baseChain,
type: baseType,
@ -114,12 +114,12 @@ export async function createWarpConfig({
synthetics: syntheticChains.map((chain) => ({ chainName: chain })),
};
if (isValidWarpRouteConfig(result)) {
if (isValidWarpRouteDeployConfig(result)) {
logGreen(`Warp Route config is valid, writing to file ${outPath}`);
writeYamlOrJson(outPath, result, format);
} else {
errorRed(
`Warp config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/warp-tokens.yaml for an example`,
`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');
}

@ -1,27 +0,0 @@
import type { ERC20Metadata, TokenType } from '@hyperlane-xyz/sdk';
import type { Address } from '@hyperlane-xyz/utils';
export type MinimalTokenMetadata = Omit<ERC20Metadata, 'totalSupply'>;
// Types below must match the Warp UI token config schema
// It is used to generate the configs for the Warp UI
// https://github.com/hyperlane-xyz/hyperlane-warp-ui-template/blob/main/src/features/tokens/types.ts
interface BaseWarpUITokenConfig extends MinimalTokenMetadata {
type: TokenType.collateral | TokenType.native;
chainId: number;
logoURI?: string;
isNft?: boolean;
}
interface CollateralTokenConfig extends BaseWarpUITokenConfig {
type: TokenType.collateral;
address: Address;
hypCollateralAddress: Address;
}
interface NativeTokenConfig extends BaseWarpUITokenConfig {
type: TokenType.native;
hypNativeAddress: Address;
}
export type WarpUITokenConfig = CollateralTokenConfig | NativeTokenConfig;

@ -1,26 +1,32 @@
import { confirm, input } from '@inquirer/prompts';
import { ethers } from 'ethers';
import { ERC20__factory, ERC721__factory } from '@hyperlane-xyz/core';
import {
ChainMap,
ChainName,
ConnectionClientConfig,
EvmHypCollateralAdapter,
HypERC20Deployer,
HypERC721Deployer,
HyperlaneContractsMap,
MinimalTokenMetadata,
MultiProtocolProvider,
MultiProvider,
RouterConfig,
TOKEN_TYPE_TO_STANDARD,
TokenConfig,
TokenFactories,
TokenType,
chainMetadata as defaultChainMetadata,
getChainIdNumber,
WarpCoreConfig,
getTokenConnectionId,
} from '@hyperlane-xyz/sdk';
import { Address, ProtocolType, objMap } from '@hyperlane-xyz/utils';
import { log, logBlue, logGray, logGreen } from '../../logger.js';
import { WarpRouteConfig, readWarpRouteConfig } from '../config/warp.js';
import {
WarpRouteDeployConfig,
readWarpRouteDeployConfig,
} from '../config/warp.js';
import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js';
import { getContext, getMergedContractAddresses } from '../context.js';
import {
@ -30,20 +36,19 @@ import {
writeJson,
} from '../utils/files.js';
import { MinimalTokenMetadata, WarpUITokenConfig } from './types.js';
import { runPreflightChecks } from './utils.js';
export async function runWarpDeploy({
export async function runWarpRouteDeploy({
key,
chainConfigPath,
warpConfigPath,
warpRouteDeploymentConfigPath,
coreArtifactsPath,
outPath,
skipConfirmation,
}: {
key: string;
chainConfigPath: string;
warpConfigPath?: string;
warpRouteDeploymentConfigPath?: string;
coreArtifactsPath?: string;
outPath: string;
skipConfirmation: boolean;
@ -55,17 +60,25 @@ export async function runWarpDeploy({
skipConfirmation,
});
if (!warpConfigPath || !isFile(warpConfigPath)) {
if (skipConfirmation) throw new Error('Warp config required');
warpConfigPath = await runFileSelectionStep(
if (
!warpRouteDeploymentConfigPath ||
!isFile(warpRouteDeploymentConfigPath)
) {
if (skipConfirmation)
throw new Error('Warp route deployment config required');
warpRouteDeploymentConfigPath = await runFileSelectionStep(
'./configs',
'Warp config',
'Warp route deployment config',
'warp',
);
} else {
log(`Using warp config at ${warpConfigPath}`);
log(
`Using warp route deployment config at ${warpRouteDeploymentConfigPath}`,
);
}
const warpRouteConfig = readWarpRouteConfig(warpConfigPath);
const warpRouteConfig = readWarpRouteDeployConfig(
warpRouteDeploymentConfigPath,
);
const configs = await runBuildConfigStep({
warpRouteConfig,
@ -83,7 +96,7 @@ export async function runWarpDeploy({
skipConfirmation,
};
logBlue('WARP Deployment plan');
logBlue('Warp route deployment plan');
await runDeployPlanStep(deploymentParams);
await runPreflightChecks({
@ -100,7 +113,7 @@ async function runBuildConfigStep({
coreArtifacts,
skipConfirmation,
}: {
warpRouteConfig: WarpRouteConfig;
warpRouteConfig: WarpRouteDeployConfig;
multiProvider: MultiProvider;
signer: ethers.Signer;
coreArtifacts?: HyperlaneContractsMap<any>;
@ -239,7 +252,7 @@ async function executeDeploy(params: DeployParams) {
const { configMap, isNft, multiProvider, outPath } = params;
const [contractsFilePath, tokenConfigPath] = prepNewArtifactsFiles(outPath, [
{ filename: 'warp-deployment', description: 'Contract addresses' },
{ filename: 'warp-route-deployment', description: 'Contract addresses' },
{ filename: 'warp-ui-token-config', description: 'Warp UI token config' },
]);
@ -259,12 +272,11 @@ async function executeDeploy(params: DeployParams) {
logBlue(`Warp UI token config is in ${tokenConfigPath}`);
}
// TODO move into token classes in the SDK
async function fetchBaseTokenMetadata(
base: WarpRouteConfig['base'],
base: WarpRouteDeployConfig['base'],
multiProvider: MultiProvider,
): Promise<MinimalTokenMetadata> {
const { type, name, symbol, chainName, address, decimals, isNft } = base;
const { type, name, symbol, chainName, address, decimals } = base;
// Skip fetching metadata if it's already provided in the config
if (name && symbol && decimals) {
@ -272,31 +284,24 @@ async function fetchBaseTokenMetadata(
}
if (type === TokenType.native) {
return (
multiProvider.getChainMetadata(base.chainName).nativeToken ||
defaultChainMetadata.ethereum.nativeToken!
);
// If it's a native token, use the chain's native token metadata
const chainNativeToken =
multiProvider.getChainMetadata(chainName).nativeToken;
if (chainNativeToken) return chainNativeToken;
else throw new Error(`No native token metadata for ${chainName}`);
} else if (base.type === TokenType.collateral && address) {
// If it's a collateral type, use a TokenAdapter to query for its metadata
log(`Fetching token metadata for ${address} on ${chainName}}`);
const provider = multiProvider.getProvider(chainName);
if (isNft) {
const erc721Contract = ERC721__factory.connect(address, provider);
const [name, symbol] = await Promise.all([
erc721Contract.name(),
erc721Contract.symbol(),
]);
return { name, symbol, decimals: 0 };
} else {
const erc20Contract = ERC20__factory.connect(address, provider);
const [name, symbol, decimals] = await Promise.all([
erc20Contract.name(),
erc20Contract.symbol(),
erc20Contract.decimals(),
]);
return { name, symbol, decimals };
}
const adapter = new EvmHypCollateralAdapter(
chainName,
MultiProtocolProvider.fromMultiProvider(multiProvider),
{ token: address },
);
return adapter.getMetadata();
} else {
throw new Error(`Unsupported token: ${base}`);
throw new Error(
`Unsupported token: ${base}. Consider setting token metadata in your deployment config.`,
);
}
}
@ -323,43 +328,46 @@ function writeTokenDeploymentArtifacts(
function writeWarpUiTokenConfig(
filePath: string,
contracts: HyperlaneContractsMap<TokenFactories>,
{ configMap, isNft, metadata, origin, multiProvider }: DeployParams,
{ configMap, metadata }: DeployParams,
) {
const baseConfig = configMap[origin];
const hypTokenAddr =
contracts[origin]?.router?.address || configMap[origin]?.foreignDeployment;
if (!hypTokenAddr) {
throw Error(
'No base Hyperlane token address deployed and no foreign deployment specified',
);
const warpCoreConfig: WarpCoreConfig = { tokens: [] };
// First pass, create token configs
for (const [chainName, contract] of Object.entries(contracts)) {
const config = configMap[chainName];
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,
addressOrDenom: contract.router.address,
collateralAddressOrDenom,
});
}
const chain = multiProvider.getChainMetadata(origin);
if (chain.protocol !== ProtocolType.Ethereum) throw Error('Unsupported VM');
const chainMetadata = multiProvider.getChainMetadata(origin);
const commonFields = {
chainId: getChainIdNumber(chainMetadata),
name: metadata.name,
symbol: metadata.symbol,
decimals: metadata.decimals,
};
let tokenConfig: WarpUITokenConfig;
if (baseConfig.type === TokenType.collateral) {
tokenConfig = {
...commonFields,
type: TokenType.collateral,
address: baseConfig.token,
hypCollateralAddress: hypTokenAddr,
isNft,
};
} else if (baseConfig.type === TokenType.native) {
tokenConfig = {
...commonFields,
type: TokenType.native,
hypNativeAddress: hypTokenAddr,
};
} else {
throw new Error(`Unsupported token type: ${baseConfig.type}`);
// Second pass, add connections between tokens
// Assumes full interconnectivity between all tokens for now b.c. that's
// what the deployers do by default.
for (const token1 of warpCoreConfig.tokens) {
for (const token2 of warpCoreConfig.tokens) {
if (
token1.chainName === token2.chainName &&
token1.addressOrDenom === token2.addressOrDenom
)
continue;
token1.connections ||= [];
token1.connections.push({
token: getTokenConnectionId(
ProtocolType.Ethereum,
token2.chainName,
token2.addressOrDenom!,
),
});
}
}
writeJson(filePath, tokenConfig);
writeJson(filePath, warpCoreConfig);
}

@ -334,6 +334,8 @@ export {
TokenConnection,
TokenConnectionConfigSchema,
TokenConnectionType,
getTokenConnectionId,
parseTokenConnectionId,
} from './token/TokenConnection';
export {
PROTOCOL_TO_NATIVE_STANDARD,
@ -343,6 +345,7 @@ export {
TOKEN_MULTI_CHAIN_STANDARDS,
TOKEN_NFT_STANDARDS,
TOKEN_STANDARD_TO_PROTOCOL,
TOKEN_TYPE_TO_STANDARD,
TokenStandard,
} from './token/TokenStandard';
export {

@ -1,6 +1,6 @@
import { z } from 'zod';
import { Address } from '@hyperlane-xyz/utils';
import { Address, ProtocolType, assert } from '@hyperlane-xyz/utils';
import { ZChainName } from '../metadata/customZodTypes';
import { ChainName } from '../types';
@ -71,3 +71,36 @@ export const TokenConnectionConfigSchema = z
intermediateRouterAddress: z.string(),
}),
);
export function getTokenConnectionId(
protocol: ProtocolType,
chainName: ChainName,
address: Address,
): string {
assert(
protocol && chainName && address,
'Invalid token connection id params',
);
return `${protocol}|${chainName}|${address}`;
}
export function parseTokenConnectionId(data: string): {
protocol: ProtocolType;
chainName: ChainName;
addressOrDenom: Address;
} {
assert(
TokenConnectionRegex.test(data),
`Invalid token connection id: ${data}`,
);
const [protocol, chainName, addressOrDenom] = data.split('|') as [
ProtocolType,
ChainName,
Address,
];
assert(
Object.values(ProtocolType).includes(protocol),
`Invalid protocol: ${protocol}`,
);
return { protocol, chainName, addressOrDenom };
}

@ -5,6 +5,8 @@ import {
ProviderType,
} from '../providers/ProviderType';
import { TokenType } from './config';
export enum TokenStandard {
// EVM
ERC20 = 'ERC20',
@ -128,6 +130,16 @@ export const TOKEN_COSMWASM_STANDARDS = [
TokenStandard.CwHypSynthetic,
];
export const TOKEN_TYPE_TO_STANDARD: Record<TokenType, TokenStandard> = {
[TokenType.native]: TokenStandard.EvmHypNative,
[TokenType.collateral]: TokenStandard.EvmHypCollateral,
[TokenType.collateralUri]: TokenStandard.EvmHypCollateral,
[TokenType.fastCollateral]: TokenStandard.EvmHypCollateral,
[TokenType.synthetic]: TokenStandard.EvmHypSynthetic,
[TokenType.syntheticUri]: TokenStandard.EvmHypSynthetic,
[TokenType.fastSynthetic]: TokenStandard.EvmHypSynthetic,
};
export const PROTOCOL_TO_NATIVE_STANDARD: Record<ProtocolType, TokenStandard> =
{
[ProtocolType.Ethereum]: TokenStandard.EvmNative,

@ -12,6 +12,7 @@ import { MultiProtocolProvider } from '../providers/MultiProtocolProvider';
import { IToken } from '../token/IToken';
import { Token } from '../token/Token';
import { TokenAmount } from '../token/TokenAmount';
import { parseTokenConnectionId } from '../token/TokenConnection';
import {
TOKEN_COLLATERALIZED_STANDARDS,
TOKEN_STANDARD_TO_PROVIDER_TYPE,
@ -71,14 +72,16 @@ export class WarpCore {
parsedConfig.tokens.forEach((config, i) => {
for (const connection of config.connections || []) {
const token1 = tokens[i];
// TODO see https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3298
const [_protocol, chainName, addrOrDenom] = connection.token.split('|');
const { chainName, addressOrDenom } = parseTokenConnectionId(
connection.token,
);
const token2 = tokens.find(
(t) => t.chainName === chainName && t.addressOrDenom === addrOrDenom,
(t) =>
t.chainName === chainName && t.addressOrDenom === addressOrDenom,
);
assert(
token2,
`Connected token not found: ${chainName} ${addrOrDenom}`,
`Connected token not found: ${chainName} ${addressOrDenom}`,
);
token1.addConnection({
...connection,

Loading…
Cancel
Save