feat(cli): add `warp --symbol` flag (#3992)

### Description

- This allows users to interact read warp routes from the registry with
symbol identifier rather than chain/address

### Drive-by

Rename `wei` to `amount`

### Backward compatibility

No, outputs chain map

### Testing

- Manual

(single)
```sh
$ yarn hyperlane warp read --symbol EZETH
$ yarn hyperlane warp send --symbol EZETH
```

(multiple)
```sh
$ yarn hyperlane warp read --symbol USDC
$ yarn hyperlane warp send --symbol USDC
```

---------

Co-authored-by: Noah Bayindirli 🥂 <noah@hyperlane.xyz>
pull/4011/head
Yorke Rhodes 5 months ago committed by GitHub
parent e0f226806e
commit bf7ad09da3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      .changeset/mean-books-clean.md
  2. 2
      typescript/cli/package.json
  3. 7
      typescript/cli/src/commands/options.ts
  4. 106
      typescript/cli/src/commands/warp.ts
  5. 2
      typescript/cli/src/config/warp.ts
  6. 21
      typescript/cli/src/send/transfer.ts
  7. 34
      typescript/cli/src/utils/tokens.ts
  8. 2
      typescript/helloworld/package.json
  9. 2
      typescript/infra/package.json
  10. 14
      yarn.lock

@ -0,0 +1,7 @@
---
"@hyperlane-xyz/cli": minor
"@hyperlane-xyz/helloworld": minor
"@hyperlane-xyz/infra": minor
---
feat(cli): add `warp --symbol` flag

@ -5,7 +5,7 @@
"dependencies": {
"@aws-sdk/client-kms": "^3.577.0",
"@aws-sdk/client-s3": "^3.577.0",
"@hyperlane-xyz/registry": "1.3.0",
"@hyperlane-xyz/registry": "^2.1.0",
"@hyperlane-xyz/sdk": "3.15.0",
"@hyperlane-xyz/utils": "3.15.0",
"@inquirer/prompts": "^3.0.0",

@ -96,8 +96,6 @@ export const warpCoreConfigCommandOption: Options = {
type: 'string',
description: 'File path to Warp Route config',
alias: 'w',
// TODO make this optional and have the commands get it from the registry
demandOption: true,
};
export const agentConfigCommandOption = (
@ -155,6 +153,11 @@ export const chainCommandOption: Options = {
description: 'The specific chain to perform operations with.',
};
export const symbolCommandOption: Options = {
type: 'string',
description: 'Token symbol (e.g. ETH, USDC)',
};
export const validatorCommandOption: Options = {
type: 'string',
description: 'Comma separated list of validator addresses',

@ -1,18 +1,27 @@
import { stringify as yamlStringify } from 'yaml';
import { CommandModule } from 'yargs';
import { EvmERC20WarpRouteReader } from '@hyperlane-xyz/sdk';
import {
ChainMap,
EvmERC20WarpRouteReader,
WarpCoreConfig,
} from '@hyperlane-xyz/sdk';
import { objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import { createWarpRouteDeployConfig } from '../config/warp.js';
import {
createWarpRouteDeployConfig,
readWarpCoreConfig,
} from '../config/warp.js';
import {
CommandModuleWithContext,
CommandModuleWithWriteContext,
} from '../context/types.js';
import { evaluateIfDryRunFailure } from '../deploy/dry-run.js';
import { runWarpRouteDeploy } from '../deploy/warp.js';
import { log, logGray, logGreen } from '../logger.js';
import { log, logGray, logGreen, logRed } from '../logger.js';
import { sendTestTransfer } from '../send/transfer.js';
import { indentYamlOrJson, writeYamlOrJson } from '../utils/files.js';
import { selectRegistryWarpRoute } from '../utils/tokens.js';
import {
addressCommandOption,
@ -20,6 +29,7 @@ import {
dryRunCommandOption,
fromAddressCommandOption,
outputFileCommandOption,
symbolCommandOption,
warpCoreConfigCommandOption,
warpDeploymentConfigCommandOption,
} from './options.js';
@ -100,51 +110,77 @@ export const configure: CommandModuleWithContext<{
};
export const read: CommandModuleWithContext<{
chain: string;
address: string;
out: string;
chain?: string;
address?: string;
out?: string;
symbol?: string;
}> = {
command: 'read',
describe: 'Reads the warp route config at the given path.',
describe: 'Derive the warp route config from onchain artifacts',
builder: {
symbol: {
...symbolCommandOption,
demandOption: false,
},
chain: {
...chainCommandOption,
demandOption: true,
demandOption: false,
},
address: addressCommandOption(
'Address of the router contract to read.',
true,
false,
),
out: outputFileCommandOption(),
},
handler: async ({ context, chain, address, out }) => {
handler: async ({ context, chain, address, out, symbol }) => {
logGray('Hyperlane Warp Reader');
logGray('---------------------');
const { multiProvider } = context;
const evmERC20WarpRouteReader = new EvmERC20WarpRouteReader(
multiProvider,
chain,
);
const warpRouteConfig = await evmERC20WarpRouteReader.deriveWarpRouteConfig(
address,
let addresses: ChainMap<string>;
if (symbol) {
const warpCoreConfig = await selectRegistryWarpRoute(
context.registry,
symbol,
);
addresses = Object.fromEntries(
warpCoreConfig.tokens.map((t) => [t.chainName, t.addressOrDenom!]),
);
} else if (chain && address) {
addresses = {
[chain]: address,
};
} else {
logGreen(`Please specify either a symbol or chain and address`);
process.exit(0);
}
const config = await promiseObjAll(
objMap(addresses, async (chain, address) =>
new EvmERC20WarpRouteReader(multiProvider, chain).deriveWarpRouteConfig(
address,
),
),
);
if (out) {
writeYamlOrJson(out, warpRouteConfig, 'yaml');
writeYamlOrJson(out, config, 'yaml');
logGreen(`✅ Warp route config written successfully to ${out}:\n`);
} else {
logGreen(`✅ Warp route config read successfully:\n`);
}
log(indentYamlOrJson(yamlStringify(warpRouteConfig, null, 2), 4));
log(indentYamlOrJson(yamlStringify(config, null, 2), 4));
process.exit(0);
},
};
const send: CommandModuleWithWriteContext<
MessageOptionsArgTypes & {
warp: string;
warp?: string;
symbol?: string;
router?: string;
wei: string;
amount: string;
recipient?: string;
}
> = {
@ -152,10 +188,17 @@ const send: CommandModuleWithWriteContext<
describe: 'Send a test token transfer on a warp route',
builder: {
...messageOptions,
warp: warpCoreConfigCommandOption,
wei: {
symbol: {
...symbolCommandOption,
demandOption: false,
},
warp: {
...warpCoreConfigCommandOption,
demandOption: false,
},
amount: {
type: 'string',
description: 'Amount in wei to send',
description: 'Amount to send (in smallest unit)',
default: 1,
},
recipient: {
@ -170,16 +213,27 @@ const send: CommandModuleWithWriteContext<
timeout,
quick,
relay,
symbol,
warp,
wei,
amount,
recipient,
}) => {
let warpCoreConfig: WarpCoreConfig;
if (symbol) {
warpCoreConfig = await selectRegistryWarpRoute(context.registry, symbol);
} else if (warp) {
warpCoreConfig = readWarpCoreConfig(warp);
} else {
logRed(`Please specify either a symbol or warp config`);
process.exit(0);
}
await sendTestTransfer({
context,
warpConfigPath: warp,
warpCoreConfig,
origin,
destination,
wei,
amount,
recipient,
timeoutSec: timeout,
skipWaitForDelivery: quick,

@ -195,7 +195,7 @@ export async function createWarpRouteDeployConfig({
// Note, this is different than the function above which reads a config
// for a DEPLOYMENT. This gets a config for using a warp route (aka WarpCoreConfig)
export function readWarpRouteConfig(filePath: string): WarpCoreConfig {
export function readWarpCoreConfig(filePath: string): WarpCoreConfig {
const config = readYamlOrJson(filePath);
if (!config) throw new Error(`No warp route config found at ${filePath}`);
return WarpCoreConfigSchema.parse(config);

@ -10,7 +10,6 @@ import {
} from '@hyperlane-xyz/sdk';
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 { runPreflightChecksForChains } from '../deploy/utils.js';
@ -20,20 +19,20 @@ import { runTokenSelectionStep } from '../utils/tokens.js';
export async function sendTestTransfer({
context,
warpConfigPath,
warpCoreConfig,
origin,
destination,
wei,
amount,
recipient,
timeoutSec,
skipWaitForDelivery,
selfRelay,
}: {
context: WriteCommandContext;
warpConfigPath: string;
warpCoreConfig: WarpCoreConfig;
origin?: ChainName;
destination?: ChainName;
wei: string;
amount: string;
recipient?: string;
timeoutSec: number;
skipWaitForDelivery: boolean;
@ -41,8 +40,6 @@ export async function sendTestTransfer({
}) {
const { chainMetadata } = context;
const warpCoreConfig = readWarpRouteConfig(warpConfigPath);
if (!origin) {
origin = await runSingleChainSelectionStep(
chainMetadata,
@ -70,7 +67,7 @@ export async function sendTestTransfer({
origin,
destination,
warpCoreConfig,
wei,
amount,
recipient,
skipWaitForDelivery,
selfRelay,
@ -85,7 +82,7 @@ async function executeDelivery({
origin,
destination,
warpCoreConfig,
wei,
amount,
recipient,
skipWaitForDelivery,
selfRelay,
@ -94,7 +91,7 @@ async function executeDelivery({
origin: ChainName;
destination: ChainName;
warpCoreConfig: WarpCoreConfig;
wei: string;
amount: string;
recipient?: string;
skipWaitForDelivery: boolean;
selfRelay?: boolean;
@ -131,7 +128,7 @@ async function executeDelivery({
const senderAddress = await signer.getAddress();
const errors = await warpCore.validateTransfer({
originTokenAmount: token.amount(wei),
originTokenAmount: token.amount(amount),
destination,
recipient: recipient ?? senderAddress,
sender: senderAddress,
@ -142,7 +139,7 @@ async function executeDelivery({
}
const transferTxs = await warpCore.getTransferRemoteTxs({
originTokenAmount: new TokenAmount(wei, token),
originTokenAmount: new TokenAmount(amount, token),
destination,
sender: senderAddress,
recipient: recipient ?? senderAddress,

@ -1,6 +1,9 @@
import select from '@inquirer/select';
import { Token } from '@hyperlane-xyz/sdk';
import { IRegistry } from '@hyperlane-xyz/registry';
import { Token, WarpCoreConfig } from '@hyperlane-xyz/sdk';
import { logGreen, logRed } from '../logger.js';
export async function runTokenSelectionStep(
tokens: Token[],
@ -17,3 +20,32 @@ export async function runTokenSelectionStep(
})) as string;
return routerAddress;
}
export async function selectRegistryWarpRoute(
registry: IRegistry,
symbol: string,
): Promise<WarpCoreConfig> {
const matching = await registry.getWarpRoutes({
symbol,
});
const routes = Object.entries(matching);
let warpCoreConfig: WarpCoreConfig;
if (routes.length === 0) {
logRed(`No warp routes found for symbol ${symbol}`);
process.exit(0);
} else if (routes.length === 1) {
warpCoreConfig = routes[0][1];
} else {
logGreen(`Multiple warp routes found for symbol ${symbol}`);
const chosenRouteId = await select({
message: 'Select from matching warp routes',
choices: routes.map(([routeId, _]) => ({
value: routeId,
})),
});
warpCoreConfig = matching[chosenRouteId];
}
return warpCoreConfig;
}

@ -4,7 +4,7 @@
"version": "3.15.0",
"dependencies": {
"@hyperlane-xyz/core": "3.15.0",
"@hyperlane-xyz/registry": "1.3.0",
"@hyperlane-xyz/registry": "^2.1.0",
"@hyperlane-xyz/sdk": "3.15.0",
"@openzeppelin/contracts-upgradeable": "^4.9.3",
"ethers": "^5.7.2"

@ -14,7 +14,7 @@
"@ethersproject/providers": "^5.7.2",
"@google-cloud/secret-manager": "^5.5.0",
"@hyperlane-xyz/helloworld": "3.15.0",
"@hyperlane-xyz/registry": "1.3.0",
"@hyperlane-xyz/registry": "^2.1.0",
"@hyperlane-xyz/sdk": "3.15.0",
"@hyperlane-xyz/utils": "3.15.0",
"@nomiclabs/hardhat-etherscan": "^3.0.3",

@ -5683,7 +5683,7 @@ __metadata:
"@aws-sdk/client-s3": "npm:^3.577.0"
"@ethersproject/abi": "npm:*"
"@ethersproject/providers": "npm:*"
"@hyperlane-xyz/registry": "npm:1.3.0"
"@hyperlane-xyz/registry": "npm:^2.1.0"
"@hyperlane-xyz/sdk": "npm:3.15.0"
"@hyperlane-xyz/utils": "npm:3.15.0"
"@inquirer/prompts": "npm:^3.0.0"
@ -5775,7 +5775,7 @@ __metadata:
resolution: "@hyperlane-xyz/helloworld@workspace:typescript/helloworld"
dependencies:
"@hyperlane-xyz/core": "npm:3.15.0"
"@hyperlane-xyz/registry": "npm:1.3.0"
"@hyperlane-xyz/registry": "npm:^2.1.0"
"@hyperlane-xyz/sdk": "npm:3.15.0"
"@nomiclabs/hardhat-ethers": "npm:^2.2.3"
"@nomiclabs/hardhat-waffle": "npm:^2.0.6"
@ -5824,7 +5824,7 @@ __metadata:
"@ethersproject/providers": "npm:^5.7.2"
"@google-cloud/secret-manager": "npm:^5.5.0"
"@hyperlane-xyz/helloworld": "npm:3.15.0"
"@hyperlane-xyz/registry": "npm:1.3.0"
"@hyperlane-xyz/registry": "npm:^2.1.0"
"@hyperlane-xyz/sdk": "npm:3.15.0"
"@hyperlane-xyz/utils": "npm:3.15.0"
"@nomiclabs/hardhat-ethers": "npm:^2.2.3"
@ -5876,13 +5876,13 @@ __metadata:
languageName: unknown
linkType: soft
"@hyperlane-xyz/registry@npm:1.3.0":
version: 1.3.0
resolution: "@hyperlane-xyz/registry@npm:1.3.0"
"@hyperlane-xyz/registry@npm:^2.1.0":
version: 2.1.0
resolution: "@hyperlane-xyz/registry@npm:2.1.0"
dependencies:
yaml: "npm:^2"
zod: "npm:^3.21.2"
checksum: 2cbdfd9e8958d0babde7104dfb0c98def7edb5f87f5f4679b09467a6a9b531884f187fcbc16fd85b00e304ef8fa3beb0a0779555b2c3edc1936541a0e878a73d
checksum: cfcd441dcbb4886a4ecd90dffaeb7a0fd81c0a126423b23cf100cd554470fe88ceb35b41271d779f0390c42293fd2c799742eeff6dd42ca42f3c799a8e88b96b
languageName: node
linkType: hard

Loading…
Cancel
Save