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

Improve UX of send and status commands #3006

Merged
merged 10 commits into from
Dec 4, 2023
5 changes: 5 additions & 0 deletions .changeset/thin-dolls-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/cli': patch
---

Improve UX of the send and status commands
30 changes: 5 additions & 25 deletions solidity/contracts/token/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Hyperlane Tokens and Warp Routes

This repo contains contracts and SDK tooling for Hyperlane-connected ERC20 and ERC721 tokens. The contracts herein can be used to create [Hyperlane Warp Routes](https://docs.hyperlane.xyz/docs/deploy/deploy-warp-route) across different chains.
This repo contains contracts and SDK tooling for Hyperlane-connected ERC20 and ERC721 tokens. The contracts herein can be used to create [Hyperlane Warp Routes](https://docs.hyperlane.xyz/docs/reference/applications/warp-routes) across different chains.

For instructions on deploying Warp Routes, see [the deployment documentation](https://docs.hyperlane.xyz/docs/deploy/deploy-warp-route/deploy-a-warp-route) and the [Hyperlane-Deploy repository](https://github.com/hyperlane-xyz/hyperlane-deploy).
For instructions on deploying Warp Routes, see [the deployment documentation](https://docs.hyperlane.xyz/docs/deploy-hyperlane#deploy-a-warp-route) and the [Hyperlane CLI](https://www.npmjs.com/package/@hyperlane-xyz/cli).

## Warp Route Architecture

Expand Down Expand Up @@ -51,7 +51,7 @@ The Token Router contract comes in several flavors and a warp route can be compo

## Interchain Security Models

Warp routes are unique amongst token bridging solutions because they provide modular security. Because the `TokenRouter` implements the `IMessageRecipient` interface, it can be configured with a custom interchain security module. Please refer to the relevant guide to specifying interchain security modules on the [Messaging API receive docs](https://docs.hyperlane.xyz/docs/apis/messaging-api/receive#interchain-security-modules).
Warp routes are unique amongst token bridging solutions because they provide modular security. Because the `TokenRouter` implements the `IMessageRecipient` interface, it can be configured with a custom interchain security module. Please refer to the relevant guide to specifying interchain security modules on the [Messaging API receive docs](https://docs.hyperlane.xyz/docs/reference/messaging/messaging-interface).

## Remote Transfer Lifecycle Diagrams

Expand All @@ -67,7 +67,7 @@ interface TokenRouter {
}
```

**NOTE:** The [Relayer](https://docs.hyperlane.xyz/docs/protocol/agents/relayer) shown below must be compensated. Please refer to the relevant guide on [paying for interchain gas](https://docs.hyperlane.xyz/docs/build-with-hyperlane/guides/paying-for-interchain-gas) on the `messageID` returned from the `transferRemote` call.
**NOTE:** The [Relayer](https://docs.hyperlane.xyz/docs/operate/relayer/run-relayer) shown below must be compensated. Please refer to the details on [paying for interchain gas](https://docs.hyperlane.xyz/docs/protocol/interchain-gas-payment).

Depending on the flavor of TokenRouter on the source and destination chain, this flow looks slightly different. The following diagrams illustrate these differences.

Expand Down Expand Up @@ -227,26 +227,6 @@ graph TB
| [audit-v2-remediation]() | 2023-02-15 | Hyperlane V2 Audit remediation |
| [main]() | ~ | Bleeding edge |

## Setup for local development

```sh
# Install dependencies
yarn

# Build source and generate types
yarn build:dev
```

## Unit testing

```sh
# Run all unit tests
yarn test

# Lint check code
yarn lint
```

## Learn more

For more information, see the [Hyperlane introduction documentation](https://docs.hyperlane.xyz/docs/introduction/readme) or the [details about Warp Routes](https://docs.hyperlane.xyz/docs/deploy/deploy-warp-route).
For more information, see the [Hyperlane introduction documentation](https://docs.hyperlane.xyz/docs/intro).
20 changes: 9 additions & 11 deletions typescript/cli/src/commands/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,13 @@ const messageOptions: { [k: string]: Options } = {
origin: {
type: 'string',
description: 'Origin chain to send message from',
demandOption: true,
},
destination: {
type: 'string',
description: 'Destination chain to send message to',
demandOption: true,
},
core: coreArtifactsOption,
chains: chainsCommandOption,
core: coreArtifactsOption,
timeout: {
type: 'number',
description: 'Timeout in seconds',
Expand All @@ -63,9 +61,9 @@ const messageCommand: CommandModule = {
handler: async (argv: any) => {
const key: string = argv.key || process.env.HYP_KEY;
const chainConfigPath: string = argv.chains;
const coreArtifactsPath: string = argv.core;
const origin: string = argv.origin;
const destination: string = argv.destination;
const coreArtifactsPath: string | undefined = argv.core;
const origin: string | undefined = argv.origin;
jmrossy marked this conversation as resolved.
Show resolved Hide resolved
const destination: string | undefined = argv.destination;
const timeoutSec: number = argv.timeout;
const skipWaitForDelivery: boolean = argv.quick;
await sendTestMessage({
Expand Down Expand Up @@ -97,7 +95,7 @@ const transferCommand: CommandModule = {
},
type: {
type: 'string',
description: 'Warp token type (native of collateral)',
description: 'Warp token type (native or collateral)',
default: TokenType.collateral,
choices: [TokenType.collateral, TokenType.native],
},
Expand All @@ -114,11 +112,11 @@ const transferCommand: CommandModule = {
handler: async (argv: any) => {
const key: string = argv.key || process.env.HYP_KEY;
const chainConfigPath: string = argv.chains;
const coreArtifactsPath: string = argv.core;
const origin: string = argv.origin;
const destination: string = argv.destination;
const coreArtifactsPath: string | undefined = argv.core;
const origin: string | undefined = argv.origin;
jmrossy marked this conversation as resolved.
Show resolved Hide resolved
const destination: string | undefined = argv.destination;
const timeoutSec: number = argv.timeout;
const routerAddress: string = argv.router;
const routerAddress: string | undefined = argv.router;
const tokenType: TokenType = argv.type;
const wei: string = argv.wei;
const recipient: string | undefined = argv.recipient;
Expand Down
8 changes: 3 additions & 5 deletions typescript/cli/src/commands/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,19 @@ export const statusCommand: CommandModule = {
id: {
type: 'string',
description: 'Message ID',
demandOption: true,
},
destination: {
type: 'string',
description: 'Destination chain name',
demandOption: true,
},
chains: chainsCommandOption,
core: coreArtifactsOption,
}),
handler: async (argv: any) => {
const chainConfigPath: string = argv.chains;
const coreArtifactsPath: string = argv.core;
const messageId: string = argv.id;
const destination: string = argv.destination;
const coreArtifactsPath: string | undefined = argv.core;
const messageId: string | undefined = argv.id;
const destination: string | undefined = argv.destination;
await checkMessageStatus({
chainConfigPath,
coreArtifactsPath,
Expand Down
14 changes: 8 additions & 6 deletions typescript/cli/src/config/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ZodTypeAny, z } from 'zod';

import { ChainName, HyperlaneContractsMap } from '@hyperlane-xyz/sdk';

import { log, logBlue, logRed } from '../../logger.js';
import { log, logBlue } from '../../logger.js';
import { readYamlOrJson, runFileSelectionStep } from '../utils/files.js';

const RecursiveObjectSchema: ZodTypeAny = z.lazy(() =>
Expand Down Expand Up @@ -37,17 +37,19 @@ export async function runDeploymentArtifactStep(
artifactsPath?: string,
message?: string,
selectedChains?: ChainName[],
) {
defaultArtifactsPath = './artifacts',
defaultArtifactsNamePattern = 'core-deployment',
): Promise<HyperlaneContractsMap<any> | undefined> {
if (!artifactsPath) {
const useArtifacts = await confirm({
message: message || 'Do you want use some existing contract addresses?',
});
if (!useArtifacts) return undefined;

artifactsPath = await runFileSelectionStep(
'./artifacts',
'contract artifacts',
'core-deployment',
defaultArtifactsPath,
'contract deployment artifacts',
defaultArtifactsNamePattern,
);
}
const artifacts = readDeploymentArtifacts(artifactsPath);
Expand All @@ -57,7 +59,7 @@ export async function runDeploymentArtifactStep(
selectedChains.includes(c),
);
if (artifactChains.length === 0) {
logRed('No artifacts found for selected chains');
log('No artifacts found for selected chains');
} else {
log(`Found existing artifacts for chains: ${artifactChains.join(', ')}`);
}
Expand Down
4 changes: 2 additions & 2 deletions typescript/cli/src/config/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ export function readChainConfigs(filePath: string) {
return chainToMetadata;
}

export function readChainConfigsIfExists(filePath: string) {
if (!isFile(filePath)) {
export function readChainConfigsIfExists(filePath?: string) {
if (!filePath || !isFile(filePath)) {
log('No chain config file provided');
return {};
} else {
Expand Down
23 changes: 23 additions & 0 deletions typescript/cli/src/context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { expect } from 'chai';
import { ethers } from 'ethers';

import { getContext } from './context.js';

describe('context', () => {
it('Gets minimal read-only context correctly', async () => {
const context = await getContext({ chainConfigPath: './fakePath' });
expect(!!context.multiProvider).to.be.true;
expect(context.customChains).to.eql({});
});

it('Handles conditional type correctly', async () => {
const randomWallet = ethers.Wallet.createRandom();
const context = await getContext({
chainConfigPath: './fakePath',
keyConfig: { key: randomWallet.privateKey },
});
expect(!!context.multiProvider).to.be.true;
expect(context.customChains).to.eql({});
expect(await context.signer.getAddress()).to.eql(randomWallet.address);
});
});
68 changes: 61 additions & 7 deletions typescript/cli/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { input } from '@inquirer/prompts';
import { ethers } from 'ethers';

import {
Expand All @@ -11,6 +12,7 @@ import {
} from '@hyperlane-xyz/sdk';
import { objFilter, objMap, objMerge } from '@hyperlane-xyz/utils';

import { runDeploymentArtifactStep } from './config/artifacts.js';
import { readChainConfigsIfExists } from './config/chain.js';
import { keyToSigner } from './utils/keys.js';

Expand Down Expand Up @@ -43,17 +45,69 @@ export function getMergedContractAddresses(
) as HyperlaneContractsMap<any>;
}

export function getContext(chainConfigPath: string) {
const customChains = readChainConfigsIfExists(chainConfigPath);
const multiProvider = getMultiProvider(customChains);
return { customChains, multiProvider };
interface ContextSettings {
chainConfigPath?: string;
coreConfig?: {
coreArtifactsPath?: string;
promptMessage?: string;
};
keyConfig?: {
key?: string;
promptMessage?: string;
};
}

interface CommandContextBase {
customChains: ChainMap<ChainMetadata>;
multiProvider: MultiProvider;
}

export function getContextWithSigner(key: string, chainConfigPath: string) {
const signer = keyToSigner(key);
// This makes return type dynamic based on the input settings
type CommandContext<P extends ContextSettings> = CommandContextBase &
(P extends { keyConfig: object }
? { signer: ethers.Signer }
: { signer: undefined }) &
jmrossy marked this conversation as resolved.
Show resolved Hide resolved
(P extends { coreConfig: object }
? { coreArtifacts: HyperlaneContractsMap<any> }
: { coreArtifacts: undefined });

export async function getContext<P extends ContextSettings>({
chainConfigPath,
coreConfig,
keyConfig,
}: P): Promise<CommandContext<P>> {
const customChains = readChainConfigsIfExists(chainConfigPath);

let signer = undefined;
if (keyConfig) {
const key =
keyConfig.key ||
(await input({
message:
keyConfig.promptMessage ||
'Please enter a private key or use the HYP_KEY environment variable',
}));
signer = keyToSigner(key);
jmrossy marked this conversation as resolved.
Show resolved Hide resolved
}

let coreArtifacts = undefined;
if (coreConfig) {
coreArtifacts =
(await runDeploymentArtifactStep(
coreConfig.coreArtifactsPath,
coreConfig.promptMessage ||
'Do you want to use some core deployment address artifacts? This is required for PI chains (non-core chains).',
)) || {};
}

const multiProvider = getMultiProvider(customChains, signer);
return { signer, customChains, multiProvider };

return {
customChains,
signer,
multiProvider,
coreArtifacts,
} as CommandContext<P>;
}

export function getMultiProvider(
Expand Down
2 changes: 1 addition & 1 deletion typescript/cli/src/deploy/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function runKurtosisAgentDeploy({
chainConfigPath: string;
agentConfigurationPath: string;
}) {
const { customChains } = getContext(chainConfigPath);
const { customChains } = await getContext({ chainConfigPath });

if (!originChain) {
originChain = await runSingleChainSelectionStep(
Expand Down
11 changes: 5 additions & 6 deletions typescript/cli/src/deploy/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { readIsmConfig } from '../config/ism.js';
import { readMultisigConfig } from '../config/multisig.js';
import { MINIMUM_CORE_DEPLOY_GAS } from '../consts.js';
import {
getContextWithSigner,
getContext,
getMergedContractAddresses,
sdkContractAddressesMap,
} from '../context.js';
Expand Down Expand Up @@ -76,10 +76,10 @@ export async function runCoreDeploy({
outPath: string;
skipConfirmation: boolean;
}) {
const { customChains, multiProvider, signer } = getContextWithSigner(
key,
const { customChains, multiProvider, signer } = await getContext({
chainConfigPath,
);
keyConfig: { key },
});

if (!chains?.length) {
chains = await runMultiChainSelectionStep(
Expand Down Expand Up @@ -119,8 +119,7 @@ export async function runCoreDeploy({

function runArtifactStep(selectedChains: ChainName[], artifactsPath?: string) {
logBlue(
'\n',
'Deployments can be totally new or can use some existing contract addresses.',
'\nDeployments can be totally new or can use some existing contract addresses.',
);
return runDeploymentArtifactStep(artifactsPath, undefined, selectedChains);
}
Expand Down
Loading
Loading