Skip to content

Commit

Permalink
add 'submit' command
Browse files Browse the repository at this point in the history
  • Loading branch information
nbayindirli committed Jul 9, 2024
1 parent 6cf3982 commit 547b383
Show file tree
Hide file tree
Showing 20 changed files with 261 additions and 58 deletions.
5 changes: 5 additions & 0 deletions .changeset/wild-fans-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/cli': minor
---

Add 'submit' command to CLI.
1 change: 1 addition & 0 deletions typescript/cli/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/artifacts
/chains
/deployments
/generated

# Test artifacts
/test-configs/**/addresses.yaml
Expand Down
2 changes: 2 additions & 0 deletions typescript/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { registryCommand } from './src/commands/registry.js';
import { sendCommand } from './src/commands/send.js';
import { statusCommand } from './src/commands/status.js';
import { submitCommand } from './src/commands/submit.js';
import { validatorCommand } from './src/commands/validator.js';
import { warpCommand } from './src/commands/warp.js';
import { contextMiddleware } from './src/context/context.js';
Expand Down Expand Up @@ -63,6 +64,7 @@ try {
.command(statusCommand)
.command(validatorCommand)
.command(warpCommand)
.command(submitCommand)
.version(VERSION)
.demandCommand()
.strict()
Expand Down
11 changes: 11 additions & 0 deletions typescript/cli/examples/submit/strategy/gnosis-ica-strategy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
chain: avalanche
submitter:
type: gnosisSafe
chain: avalanche
safeAddress: '0x7fd32493Ca3A38cDf78A4cb74F32f6292f822aBe'
transforms:
- type: interchainAccount
chain: ethereum
config:
origin: avalanche
owner: '0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb'
5 changes: 5 additions & 0 deletions typescript/cli/examples/submit/strategy/gnosis-strategy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
chain: avalanche
submitter:
type: gnosisSafe
chain: avalanche
safeAddress: '0x7fd32493Ca3A38cDf78A4cb74F32f6292f822aBe'
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
chain: alfajores
submitter:
type: impersonatedAccount
userAddress: '0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb'
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
chain: alfajores
submitter:
type: jsonRpc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"data": "0x0e72cc06000000000000000000000000744ad987ee7c65d3b3333bfc3e6ecbf963eb872a",
"to": "0x9a4a3124F2a86bB5BE46267De85D31762b7a05Fd",
"from": "0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb",
"chainId": 44787
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"data": "0x0e72cc06000000000000000000000000744ad987ee7c65d3b3333bfc3e6ecbf963eb872a",
"to": "0x9a4a3124F2a86bB5BE46267De85D31762b7a05Fd",
"from": "0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb",
"chainId": 43114
}
]
14 changes: 14 additions & 0 deletions typescript/cli/src/commands/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,20 @@ export const validatorCommandOption: Options = {
demandOption: true,
};

export const transactionsCommandOption: Options = {
type: 'string',
description: 'The transaction input file path.',
alias: ['t', 'txs', 'txns'],
demandOption: true,
};

export const strategyCommandOption: Options = {
type: 'string',
description: 'The submission strategy input file path.',
alias: 's',
demandOption: true,
};

export const addressCommandOption = (
description: string,
demandOption = false,
Expand Down
2 changes: 1 addition & 1 deletion typescript/cli/src/commands/signCommands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Commands that send tx and require a key to sign.
// It's useful to have this listed here so the context
// middleware can request keys up front when required.
export const SIGN_COMMANDS = ['deploy', 'send'];
export const SIGN_COMMANDS = ['deploy', 'send', 'submit'];

export function isSignCommand(argv: any): boolean {
return (
Expand Down
42 changes: 42 additions & 0 deletions typescript/cli/src/commands/submit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { runSubmit } from '../config/submit.js';
import { CommandModuleWithWriteContext } from '../context/types.js';
import { logBlue, logGray } from '../logger.js';

import {
dryRunCommandOption,
outputFileCommandOption,
strategyCommandOption,
transactionsCommandOption,
} from './options.js';

/**
* Submit command
*/
export const submitCommand: CommandModuleWithWriteContext<{
transactions: string;
strategy: string;
'dry-run': string;
receipts: string;
}> = {
command: 'submit',
describe: 'Submit transactions',
builder: {
transactions: transactionsCommandOption,
strategy: strategyCommandOption,
'dry-run': dryRunCommandOption,
receipts: outputFileCommandOption('./generated/transactions/receipts.yaml'),
},
handler: async ({ context, transactions, receipts }) => {
logGray(`Hyperlane Submit`);
logGray(`----------------`);

await runSubmit({
context,
transactionsFilepath: transactions,
receiptsFilepath: receipts,
});

logBlue(`✅ Submission complete`);
process.exit(0);
},
};
75 changes: 75 additions & 0 deletions typescript/cli/src/config/submit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { stringify as yamlStringify } from 'yaml';

import {
PopulatedTransaction,
PopulatedTransactionSchema,
} from '@hyperlane-xyz/sdk';
import { assert } from '@hyperlane-xyz/utils';

import { WriteCommandContext } from '../context/types.js';
import { logGray, logRed } from '../logger.js';
import { getSubmitterBuilder } from '../submit/submit.js';
import {
indentYamlOrJson,
readYamlOrJson,
writeYamlOrJson,
} from '../utils/files.js';

export async function runSubmit({
context,
transactionsFilepath,
receiptsFilepath,
}: {
context: WriteCommandContext;
transactionsFilepath: string;
receiptsFilepath: string;
}) {
const { submissionStrategy, chainMetadata, multiProvider, isDryRun } =
context;

assert(
submissionStrategy,
'Submission strategy required to submit transactions.\nPlease create a submission strategy, e.g. ./strategy.yaml.',
);

const chain = submissionStrategy.chain;
const protocol = chainMetadata[chain].protocol;
const submitterBuilder = await getSubmitterBuilder<typeof protocol>({
submitterMetadata: submissionStrategy.submitter,
transformersMetadata: submissionStrategy.transforms ?? [],
multiProvider,
chain: submissionStrategy.chain,
isDryRun,
});
const transactions = getTransactions(transactionsFilepath);

try {
const transactionReceipts = await submitterBuilder.submit(...transactions);
if (transactionReceipts) {
logGray(
'🧾 Transaction receipts:\n\n',
indentYamlOrJson(yamlStringify(transactionReceipts, null, 2), 4),
);
writeYamlOrJson(receiptsFilepath, transactionReceipts, 'yaml');
}
} catch (error) {
logRed(
`⛔️ Failed to submit ${transactions.length} transactions:`,
JSON.stringify(error),
);
throw new Error('Failed to submit transactions.');
}
}

function getTransactions(transactionsFilepath: string): PopulatedTransaction[] {
const transactionsFileContent = readYamlOrJson<any[] | undefined>(
transactionsFilepath.trim(),
);
assert(
transactionsFileContent,
'Transactions required to submit transactions.\nPlease add a transactions file, e.g. ./transactions.json.',
);
return transactionsFileContent.map((tx) =>
PopulatedTransactionSchema.parse(tx),
);
}
48 changes: 42 additions & 6 deletions typescript/cli/src/context/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@ import {
MergedRegistry,
} from '@hyperlane-xyz/registry';
import { FileSystemRegistry } from '@hyperlane-xyz/registry/fs';
import { ChainName, MultiProvider } from '@hyperlane-xyz/sdk';
import {
ChainName,
MultiProvider,
SubmissionStrategy,
SubmissionStrategySchema,
TxSubmitterType,
} from '@hyperlane-xyz/sdk';
import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils';

import { isSignCommand } from '../commands/signCommands.js';
import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js';
import { logBlue } from '../logger.js';
import { runSingleChainSelectionStep } from '../utils/chains.js';
import { readYamlOrJson } from '../utils/files.js';
import { getImpersonatedSigner, getSigner } from '../utils/keys.js';

import {
Expand All @@ -22,7 +29,7 @@ import {
} from './types.js';

export async function contextMiddleware(argv: Record<string, any>) {
const isDryRun = !isNullish(argv.dryRun);
let isDryRun = !isNullish(argv.dryRun);
const requiresKey = isSignCommand(argv);
const settings: ContextSettings = {
registryUri: argv.registry,
Expand All @@ -36,6 +43,15 @@ export async function contextMiddleware(argv: Record<string, any>) {
throw new Error(
"'--from-address' or '-f' should only be used for dry-runs",
);
if (argv.strategy) {
settings.submissionStrategy = getSubmissionStrategy(argv.strategy);
if (
settings.submissionStrategy.submitter.type ===
TxSubmitterType.IMPERSONATED_ACCOUNT
) {
isDryRun = true;
}
}
const context = isDryRun
? await getDryRunContext(settings, argv.dryRun)
: await getContext(settings);
Expand All @@ -52,6 +68,7 @@ export async function getContext({
key,
requiresKey,
skipConfirmation,
submissionStrategy,
}: ContextSettings): Promise<CommandContext> {
const registry = getRegistry(registryUri, registryOverrideUri);

Expand All @@ -68,6 +85,7 @@ export async function getContext({
key,
signer,
skipConfirmation: !!skipConfirmation,
submissionStrategy,
} as CommandContext;
}

Expand All @@ -82,6 +100,7 @@ export async function getDryRunContext(
key,
fromAddress,
skipConfirmation,
submissionStrategy,
}: ContextSettings,
chain?: ChainName,
): Promise<CommandContext> {
Expand All @@ -90,10 +109,12 @@ export async function getDryRunContext(

if (!chain) {
if (skipConfirmation) throw new Error('No chains provided');
chain = await runSingleChainSelectionStep(
chainMetadata,
'Select chain to dry-run against:',
);
chain = submissionStrategy
? submissionStrategy.chain
: await runSingleChainSelectionStep(
chainMetadata,
'Select chain to dry-run against:',
);
}

logBlue(`Dry-running against chain: ${chain}`);
Expand All @@ -117,6 +138,7 @@ export async function getDryRunContext(
skipConfirmation: !!skipConfirmation,
isDryRun: true,
dryRunChain: chain,
submissionStrategy,
} as WriteCommandContext;
}

Expand Down Expand Up @@ -163,3 +185,17 @@ async function getMultiProvider(registry: IRegistry, signer?: ethers.Signer) {
if (signer) multiProvider.setSharedSigner(signer);
return multiProvider;
}

/**
* Retrieves a submission strategy from the provided filepath.
* @param submissionStrategyFilepath a filepath to the submission strategy file
* @returns a formatted submission strategy
*/
function getSubmissionStrategy(
submissionStrategyFilepath: string,
): SubmissionStrategy {
const submissionStrategyFileContent = readYamlOrJson(
submissionStrategyFilepath.trim(),
);
return SubmissionStrategySchema.parse(submissionStrategyFileContent);
}
3 changes: 3 additions & 0 deletions typescript/cli/src/context/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
ChainMap,
ChainMetadata,
MultiProvider,
SubmissionStrategy,
} from '@hyperlane-xyz/sdk';

export interface ContextSettings {
Expand All @@ -15,13 +16,15 @@ export interface ContextSettings {
fromAddress?: string;
requiresKey?: boolean;
skipConfirmation?: boolean;
submissionStrategy?: SubmissionStrategy;
}

export interface CommandContext {
registry: IRegistry;
chainMetadata: ChainMap<ChainMetadata>;
multiProvider: MultiProvider;
skipConfirmation: boolean;
submissionStrategy?: SubmissionStrategy;
key?: string;
signer?: ethers.Signer;
}
Expand Down
Loading

0 comments on commit 547b383

Please sign in to comment.