Skip to content
This repository has been archived by the owner on Dec 7, 2023. It is now read-only.

Commit

Permalink
Improve UX of send and status commands (hyperlane-xyz#3006)
Browse files Browse the repository at this point in the history
### Description

- Improve shared context utility with conditional typing and core artifact handling
- Improve UX of send and status commands with prompts
- Prompt for key if required and not provided

### Drive-by Changes

Update the token readme for hyperlane-xyz/issues#715

### Backward compatibility

Yes

### Testing

Manual
  • Loading branch information
jmrossy committed Dec 4, 2023
1 parent d0c1f8f commit 9705079
Show file tree
Hide file tree
Showing 14 changed files with 217 additions and 119 deletions.
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;
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;
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 }) &
(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);
}

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

0 comments on commit 9705079

Please sign in to comment.