From bb470aec25c02086921d1305c78cc09a1cda2528 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noah=20Bayindirli=20=F0=9F=A5=82?= <15343884+nbayindirli@users.noreply.github.com> Date: Wed, 10 Jul 2024 11:44:33 -0400 Subject: [PATCH] feat(cli): add submit command (#3818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description * adds `hyperlane submit -t ./transactions.json -s ./strategy.json` * given a set of `transactions` and a submission `strategy`, submit those transactions according to that strategy * example strategies provided under `/cli/examples/submit/` * detailed design: https://www.notion.so/hyperlanexyz/CLI-Submitter-Integration-8a14486803ca4ab18f229e8e7f89b57a?pvs=4 ### Drive-by changes * none ### Related issues - Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3740 ### Backward compatibility - yes ### Testing #### Nice to have: - [ ] ci-test #### Manual: - [x] `hl submit -t ./examples/submit/transactions/transactions.json -s ./examples/submit/strategy/json-rpc-strategy.yaml` ``` Hyperlane Submit ---------------- Submitting 1 transactions to the jsonRpc submitter... Sent tx 0x7158756345a913630497dac948a98b1c73921626f9c17cc936994ff22dcd908f Pending https://alfajores.celoscan.io/tx/0x7158756345a913630497dac948a98b1c73921626f9c17cc936994ff22dcd908f (waiting 1 blocks for confirmation) โœ… Successfully submitted 1 transactions to the jsonRpc submitter. ๐Ÿงพ Transaction receipts: [ { to: '0x9a4a3124F2a86bB5BE46267De85D31762b7a05Fd', from: '0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb', contractAddress: null, transactionIndex: 4, gasUsed: BigNumber { _hex: '0x53b8', _isBigNumber: true }, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', blockHash: '0x174c3355800d73e3ab9643257bf49a60fc539a6961fc6cc8adcfb27d84ab6347', transactionHash: '0x7158756345a913630497dac948a98b1c73921626f9c17cc936994ff22dcd908f', logs: [], blockNumber: 24221122, confirmations: 1, cumulativeGasUsed: BigNumber { _hex: '0x072f3f', _isBigNumber: true }, effectiveGasPrice: BigNumber { _hex: '0x01836e2100', _isBigNumber: true }, status: 1, type: 2, byzantium: true } ] โœ… Hyperlane submission complete ``` - [x] `hl submit -t ./examples/submit/transactions/transactions.json -s ./examples/submit/strategy/json-rpc-strategy.yaml --dry-run` ``` ... Dry-running against chain: alfajores ๐Ÿ”Ž Verifying anvil node is running... โœ… Successfully verified anvil node is running Forking alfajores for dry-run... โœ… Successfully forked alfajores for dry-run Impersonating account (0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb)... โœ… Successfully impersonated account (0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb) Hyperlane Submit ---------------- Submitting 1 transactions to the jsonRpc submitter... Sent tx 0x89fbd7d38d6fc19df0ef24f844756153dece1e0f179b76b036c50596356dc5ce Pending https://alfajores.celoscan.io/tx/0x89fbd7d38d6fc19df0ef24f844756153dece1e0f179b76b036c50596356dc5ce (waiting 1 blocks for confirmation) โœ… Successfully submitted 1 transactions to the jsonRpc submitter. ๐Ÿงพ Transaction receipts: [ { to: '0x9a4a3124F2a86bB5BE46267De85D31762b7a05Fd', from: '0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb', contractAddress: null, transactionIndex: 0, gasUsed: BigNumber { _hex: '0x53b8', _isBigNumber: true }, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', blockHash: '0x5a5379771a3a6d173a8dcbfc189e24f288c30abcd0a754f1df4734f465917cc7', transactionHash: '0x89fbd7d38d6fc19df0ef24f844756153dece1e0f179b76b036c50596356dc5ce', logs: [], blockNumber: 24221165, confirmations: 1, cumulativeGasUsed: BigNumber { _hex: '0x53b8', _isBigNumber: true }, effectiveGasPrice: BigNumber { _hex: '0x0104fd914d', _isBigNumber: true }, status: 1, type: 2, byzantium: true } ] โœ… Hyperlane submission complete ``` - [x] `hl submit -t ./examples/submit/transactions/transactions.json -s ./examples/submit/strategy/impersonated-account-strategy.yaml` - [x] `hl submit -t ./examples/submit/transactions/transactions.json -s ./examples/submit/strategy/impersonated-account-strategy.yaml --dry-run` - `Error: Impersonated account submitters may only be used during dry-runs.` - [x] `hl submit -t ./examples/submit/transactions/transactions.json -s ./examples/submit/strategy/gnosis-strategy.yaml` - Successfully generated proposal: https://app.safe.global/transactions/queue?safe=avax:0x7fd32493Ca3A38cDf78A4cb74F32f6292f822aBe - [ ] `hl submit -t ./examples/submit/transactions/transactions.json -s ./examples/submit/strategy/gnosis-ica-strategy.yaml` - [ ] `hl submit -t ./examples/submit/transactions/transactions.json -s ./examples/submit/strategy/gnosis-ica-strategy.yaml --dry-run` --- .changeset/wild-fans-repair.md | 5 ++ typescript/cli/.gitignore | 1 + typescript/cli/cli.ts | 2 + .../submit/strategy/gnosis-ica-strategy.yaml | 11 ++++ .../submit/strategy/gnosis-strategy.yaml | 5 ++ .../impersonated-account-strategy.yaml | 4 ++ .../submit/strategy/json-rpc-strategy.yaml | 3 + .../transactions/alfajores-transactions.json | 8 +++ .../transactions/avalanche-transactions.json | 8 +++ typescript/cli/src/commands/options.ts | 14 ++++ typescript/cli/src/commands/signCommands.ts | 2 +- typescript/cli/src/commands/submit.ts | 42 ++++++++++++ typescript/cli/src/config/submit.ts | 65 +++++++++++++++++++ typescript/cli/src/context/context.ts | 48 ++++++++++++-- typescript/cli/src/context/types.ts | 3 + typescript/cli/src/submit/submit.ts | 48 ++++++-------- typescript/cli/src/submit/types.ts | 28 ++------ typescript/sdk/src/index.ts | 6 +- .../sdk/src/providers/transactions/schemas.ts | 2 + .../ethersV5/EV5GnosisSafeTxSubmitter.ts | 4 +- .../EV5ImpersonatedAccountTxSubmitter.ts | 4 +- .../ethersV5/EV5JsonRpcTxSubmitter.ts | 5 +- .../transactions/submitter/schemas.ts | 5 +- .../EV5InterchainAccountTxTransformer.ts | 18 +++-- .../transactions/transformer/schemas.ts | 2 +- .../sdk/src/providers/transactions/types.ts | 10 ++- 26 files changed, 277 insertions(+), 76 deletions(-) create mode 100644 .changeset/wild-fans-repair.md create mode 100644 typescript/cli/examples/submit/strategy/gnosis-ica-strategy.yaml create mode 100644 typescript/cli/examples/submit/strategy/gnosis-strategy.yaml create mode 100644 typescript/cli/examples/submit/strategy/impersonated-account-strategy.yaml create mode 100644 typescript/cli/examples/submit/strategy/json-rpc-strategy.yaml create mode 100644 typescript/cli/examples/submit/transactions/alfajores-transactions.json create mode 100644 typescript/cli/examples/submit/transactions/avalanche-transactions.json create mode 100644 typescript/cli/src/commands/submit.ts create mode 100644 typescript/cli/src/config/submit.ts diff --git a/.changeset/wild-fans-repair.md b/.changeset/wild-fans-repair.md new file mode 100644 index 000000000..971eeed66 --- /dev/null +++ b/.changeset/wild-fans-repair.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': minor +--- + +Add 'submit' command to CLI. diff --git a/typescript/cli/.gitignore b/typescript/cli/.gitignore index e417d75c2..4a0b6f51b 100644 --- a/typescript/cli/.gitignore +++ b/typescript/cli/.gitignore @@ -7,6 +7,7 @@ /artifacts /chains /deployments +/generated # Test artifacts /test-configs/**/addresses.yaml diff --git a/typescript/cli/cli.ts b/typescript/cli/cli.ts index 2328c2531..ed2adde08 100644 --- a/typescript/cli/cli.ts +++ b/typescript/cli/cli.ts @@ -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'; @@ -61,6 +62,7 @@ try { .command(registryCommand) .command(sendCommand) .command(statusCommand) + .command(submitCommand) .command(validatorCommand) .command(warpCommand) .version(VERSION) diff --git a/typescript/cli/examples/submit/strategy/gnosis-ica-strategy.yaml b/typescript/cli/examples/submit/strategy/gnosis-ica-strategy.yaml new file mode 100644 index 000000000..9ceeb3f8a --- /dev/null +++ b/typescript/cli/examples/submit/strategy/gnosis-ica-strategy.yaml @@ -0,0 +1,11 @@ +chain: avalanche +submitter: + type: gnosisSafe + chain: avalanche + safeAddress: '0x7fd32493Ca3A38cDf78A4cb74F32f6292f822aBe' +transforms: + - type: interchainAccount + chain: ethereum + config: + origin: avalanche + owner: '0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb' diff --git a/typescript/cli/examples/submit/strategy/gnosis-strategy.yaml b/typescript/cli/examples/submit/strategy/gnosis-strategy.yaml new file mode 100644 index 000000000..d1f74fa0a --- /dev/null +++ b/typescript/cli/examples/submit/strategy/gnosis-strategy.yaml @@ -0,0 +1,5 @@ +chain: avalanche +submitter: + type: gnosisSafe + chain: avalanche + safeAddress: '0x7fd32493Ca3A38cDf78A4cb74F32f6292f822aBe' diff --git a/typescript/cli/examples/submit/strategy/impersonated-account-strategy.yaml b/typescript/cli/examples/submit/strategy/impersonated-account-strategy.yaml new file mode 100644 index 000000000..f1b63d824 --- /dev/null +++ b/typescript/cli/examples/submit/strategy/impersonated-account-strategy.yaml @@ -0,0 +1,4 @@ +chain: alfajores +submitter: + type: impersonatedAccount + userAddress: '0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb' diff --git a/typescript/cli/examples/submit/strategy/json-rpc-strategy.yaml b/typescript/cli/examples/submit/strategy/json-rpc-strategy.yaml new file mode 100644 index 000000000..35430964b --- /dev/null +++ b/typescript/cli/examples/submit/strategy/json-rpc-strategy.yaml @@ -0,0 +1,3 @@ +chain: alfajores +submitter: + type: jsonRpc diff --git a/typescript/cli/examples/submit/transactions/alfajores-transactions.json b/typescript/cli/examples/submit/transactions/alfajores-transactions.json new file mode 100644 index 000000000..b256b9886 --- /dev/null +++ b/typescript/cli/examples/submit/transactions/alfajores-transactions.json @@ -0,0 +1,8 @@ +[ + { + "data": "0x0e72cc06000000000000000000000000744ad987ee7c65d3b3333bfc3e6ecbf963eb872a", + "to": "0x9a4a3124F2a86bB5BE46267De85D31762b7a05Fd", + "from": "0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb", + "chainId": 44787 + } +] diff --git a/typescript/cli/examples/submit/transactions/avalanche-transactions.json b/typescript/cli/examples/submit/transactions/avalanche-transactions.json new file mode 100644 index 000000000..bfb69e64c --- /dev/null +++ b/typescript/cli/examples/submit/transactions/avalanche-transactions.json @@ -0,0 +1,8 @@ +[ + { + "data": "0x0e72cc06000000000000000000000000744ad987ee7c65d3b3333bfc3e6ecbf963eb872a", + "to": "0x9a4a3124F2a86bB5BE46267De85D31762b7a05Fd", + "from": "0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb", + "chainId": 43114 + } +] diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index 00124952e..7b772b601 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -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, diff --git a/typescript/cli/src/commands/signCommands.ts b/typescript/cli/src/commands/signCommands.ts index 8df41a0a0..93b6f3015 100644 --- a/typescript/cli/src/commands/signCommands.ts +++ b/typescript/cli/src/commands/signCommands.ts @@ -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 ( diff --git a/typescript/cli/src/commands/submit.ts b/typescript/cli/src/commands/submit.ts new file mode 100644 index 000000000..78cd5c6bd --- /dev/null +++ b/typescript/cli/src/commands/submit.ts @@ -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); + }, +}; diff --git a/typescript/cli/src/config/submit.ts b/typescript/cli/src/config/submit.ts new file mode 100644 index 000000000..c91389156 --- /dev/null +++ b/typescript/cli/src/config/submit.ts @@ -0,0 +1,65 @@ +import { stringify as yamlStringify } from 'yaml'; + +import { + PopulatedTransactions, + PopulatedTransactionsSchema, +} from '@hyperlane-xyz/sdk'; +import { assert, errorToString } 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 } = context; + + assert( + submissionStrategy, + 'Submission strategy required to submit transactions.\nPlease create a submission strategy. See examples in cli/examples/submit/strategy/*.', + ); + + const chain = submissionStrategy.chain; + const protocol = chainMetadata[chain].protocol; + const submitterBuilder = await getSubmitterBuilder({ + submissionStrategy, + multiProvider, + }); + 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:`, + errorToString(error), + ); + throw new Error('Failed to submit transactions.'); + } +} + +function getTransactions(transactionsFilepath: string): PopulatedTransactions { + const transactionsFileContent = readYamlOrJson( + transactionsFilepath.trim(), + ); + return PopulatedTransactionsSchema.parse(transactionsFileContent); +} diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index bc3258c3f..2b5c13529 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -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 { @@ -22,7 +29,7 @@ import { } from './types.js'; export async function contextMiddleware(argv: Record) { - const isDryRun = !isNullish(argv.dryRun); + let isDryRun = !isNullish(argv.dryRun); const requiresKey = isSignCommand(argv); const settings: ContextSettings = { registryUri: argv.registry, @@ -36,6 +43,15 @@ export async function contextMiddleware(argv: Record) { 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); @@ -52,6 +68,7 @@ export async function getContext({ key, requiresKey, skipConfirmation, + submissionStrategy, }: ContextSettings): Promise { const registry = getRegistry(registryUri, registryOverrideUri); @@ -68,6 +85,7 @@ export async function getContext({ key, signer, skipConfirmation: !!skipConfirmation, + submissionStrategy, } as CommandContext; } @@ -82,6 +100,7 @@ export async function getDryRunContext( key, fromAddress, skipConfirmation, + submissionStrategy, }: ContextSettings, chain?: ChainName, ): Promise { @@ -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}`); @@ -117,6 +138,7 @@ export async function getDryRunContext( skipConfirmation: !!skipConfirmation, isDryRun: true, dryRunChain: chain, + submissionStrategy, } as WriteCommandContext; } @@ -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); +} diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index 80f3121fb..9edf93e73 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -6,6 +6,7 @@ import type { ChainMap, ChainMetadata, MultiProvider, + SubmissionStrategy, } from '@hyperlane-xyz/sdk'; export interface ContextSettings { @@ -15,6 +16,7 @@ export interface ContextSettings { fromAddress?: string; requiresKey?: boolean; skipConfirmation?: boolean; + submissionStrategy?: SubmissionStrategy; } export interface CommandContext { @@ -22,6 +24,7 @@ export interface CommandContext { chainMetadata: ChainMap; multiProvider: MultiProvider; skipConfirmation: boolean; + submissionStrategy?: SubmissionStrategy; key?: string; signer?: ethers.Signer; } diff --git a/typescript/cli/src/submit/submit.ts b/typescript/cli/src/submit/submit.ts index fcde4afa4..6b5c705b3 100644 --- a/typescript/cli/src/submit/submit.ts +++ b/typescript/cli/src/submit/submit.ts @@ -1,11 +1,11 @@ import { EV5GnosisSafeTxSubmitter, - EV5GnosisSafeTxSubmitterProps, EV5ImpersonatedAccountTxSubmitter, - EV5ImpersonatedAccountTxSubmitterProps, EV5InterchainAccountTxTransformer, EV5JsonRpcTxSubmitter, MultiProvider, + SubmitterMetadata, + TransformerMetadata, TxSubmitterBuilder, TxSubmitterInterface, TxSubmitterType, @@ -14,24 +14,19 @@ import { } from '@hyperlane-xyz/sdk'; import { ProtocolType } from '@hyperlane-xyz/utils'; -import { - SubmitterBuilderSettings, - SubmitterMetadata, - TransformerMetadata, -} from './types.js'; +import { SubmitterBuilderSettings } from './types.js'; export async function getSubmitterBuilder({ - submitterMetadata, - transformersMetadata, + submissionStrategy, multiProvider, }: SubmitterBuilderSettings): Promise> { const submitter = await getSubmitter( multiProvider, - submitterMetadata, + submissionStrategy.submitter, ); const transformers = await getTransformers( multiProvider, - transformersMetadata, + submissionStrategy.transforms ?? [], ); return new TxSubmitterBuilder(submitter, transformers); @@ -45,27 +40,25 @@ async function getSubmitter( case TxSubmitterType.JSON_RPC: return new EV5JsonRpcTxSubmitter(multiProvider); case TxSubmitterType.IMPERSONATED_ACCOUNT: - return new EV5ImpersonatedAccountTxSubmitter( - multiProvider, - submitterMetadata.props as EV5ImpersonatedAccountTxSubmitterProps, - ); + return new EV5ImpersonatedAccountTxSubmitter(multiProvider, { + ...submitterMetadata, + }); case TxSubmitterType.GNOSIS_SAFE: - return new EV5GnosisSafeTxSubmitter( - multiProvider, - submitterMetadata.props as EV5GnosisSafeTxSubmitterProps, - ); + return new EV5GnosisSafeTxSubmitter(multiProvider, { + ...submitterMetadata, + }); default: - throw new Error(`Invalid TxSubmitterType: ${submitterMetadata.type}`); + throw new Error(`Invalid TxSubmitterType.`); } } async function getTransformers( multiProvider: MultiProvider, - metadata: TransformerMetadata[], + transformersMetadata: TransformerMetadata[], ): Promise[]> { return Promise.all( - metadata.map(({ type, props: settings }) => - getTransformer(multiProvider, { type, props: settings }), + transformersMetadata.map((transformerMetadata) => + getTransformer(multiProvider, transformerMetadata), ), ); } @@ -76,11 +69,10 @@ async function getTransformer( ): Promise> { switch (transformerMetadata.type) { case TxTransformerType.INTERCHAIN_ACCOUNT: - return new EV5InterchainAccountTxTransformer( - multiProvider, - transformerMetadata.props, - ); + return new EV5InterchainAccountTxTransformer(multiProvider, { + ...transformerMetadata, + }); default: - throw new Error(`Invalid TxTransformerType: ${transformerMetadata.type}`); + throw new Error('Invalid TxTransformerType.'); } } diff --git a/typescript/cli/src/submit/types.ts b/typescript/cli/src/submit/types.ts index d50a63e22..423659c99 100644 --- a/typescript/cli/src/submit/types.ts +++ b/typescript/cli/src/submit/types.ts @@ -1,27 +1,11 @@ +import { z } from 'zod'; + import type { - EV5GnosisSafeTxSubmitterProps, - EV5ImpersonatedAccountTxSubmitterProps, - EV5InterchainAccountTxTransformerProps, MultiProvider, - TxSubmitterType, - TxTransformerType, + SubmissionStrategySchema, } from '@hyperlane-xyz/sdk'; -export interface SubmitterBuilderSettings { - submitterMetadata: SubmitterMetadata; - transformersMetadata: TransformerMetadata[]; +export type SubmitterBuilderSettings = { + submissionStrategy: z.infer; multiProvider: MultiProvider; -} -export interface SubmitterMetadata { - type: TxSubmitterType; - props: SubmitterProps; -} -export interface TransformerMetadata { - type: TxTransformerType; - props: TransformerProps; -} - -type SubmitterProps = - | EV5ImpersonatedAccountTxSubmitterProps - | EV5GnosisSafeTxSubmitterProps; -type TransformerProps = EV5InterchainAccountTxTransformerProps; +}; diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 5e6de6201..2c6c6e5cf 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -308,10 +308,14 @@ export { defaultViemProviderBuilder, protocolToDefaultProviderBuilder, } from './providers/providerBuilders.js'; -export { PopulatedTransactionSchema } from './providers/transactions/schemas.js'; +export { + PopulatedTransactionSchema, + PopulatedTransactionsSchema, +} from './providers/transactions/schemas.js'; export { CallData, PopulatedTransaction, + PopulatedTransactions, } from './providers/transactions/types.js'; export { TxSubmitterType } from './providers/transactions/submitter/TxSubmitterTypes.js'; diff --git a/typescript/sdk/src/providers/transactions/schemas.ts b/typescript/sdk/src/providers/transactions/schemas.ts index 706a96179..e0430375a 100644 --- a/typescript/sdk/src/providers/transactions/schemas.ts +++ b/typescript/sdk/src/providers/transactions/schemas.ts @@ -10,6 +10,8 @@ export const PopulatedTransactionSchema = z.object({ chainId: z.number(), }); +export const PopulatedTransactionsSchema = PopulatedTransactionSchema.array(); + export const CallDataSchema = z.object({ to: ZHash, data: z.string(), diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.ts index 74e1ce53c..9822f0eea 100644 --- a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.ts +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.ts @@ -5,7 +5,7 @@ import { Address, assert, rootLogger } from '@hyperlane-xyz/utils'; // @ts-ignore import { getSafe, getSafeService } from '../../../../utils/gnosisSafe.js'; import { MultiProvider } from '../../../MultiProvider.js'; -import { PopulatedTransaction } from '../../types.js'; +import { PopulatedTransaction, PopulatedTransactions } from '../../types.js'; import { TxSubmitterType } from '../TxSubmitterTypes.js'; import { EV5TxSubmitterInterface } from './EV5TxSubmitterInterface.js'; @@ -24,7 +24,7 @@ export class EV5GnosisSafeTxSubmitter implements EV5TxSubmitterInterface { public readonly props: EV5GnosisSafeTxSubmitterProps, ) {} - public async submit(...txs: PopulatedTransaction[]): Promise { + public async submit(...txs: PopulatedTransactions): Promise { const safe = await getSafe( this.props.chain, this.multiProvider, diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.ts index c184ab17d..9a6866e35 100644 --- a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.ts +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.ts @@ -1,5 +1,4 @@ import { TransactionReceipt } from '@ethersproject/providers'; -import { PopulatedTransaction } from 'ethers'; import { Logger } from 'pino'; import { rootLogger } from '@hyperlane-xyz/utils'; @@ -9,6 +8,7 @@ import { stopImpersonatingAccount, } from '../../../../utils/fork.js'; import { MultiProvider } from '../../../MultiProvider.js'; +import { PopulatedTransactions } from '../../types.js'; import { TxSubmitterType } from '../TxSubmitterTypes.js'; import { EV5JsonRpcTxSubmitter } from './EV5JsonRpcTxSubmitter.js'; @@ -30,7 +30,7 @@ export class EV5ImpersonatedAccountTxSubmitter extends EV5JsonRpcTxSubmitter { } public async submit( - ...txs: PopulatedTransaction[] + ...txs: PopulatedTransactions ): Promise { const impersonatedAccount = await impersonateAccount( this.props.userAddress, diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.ts index f50c7f822..30b60137d 100644 --- a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.ts +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.ts @@ -1,10 +1,11 @@ import { TransactionReceipt } from '@ethersproject/providers'; -import { ContractReceipt, PopulatedTransaction } from 'ethers'; +import { ContractReceipt } from 'ethers'; import { Logger } from 'pino'; import { assert, rootLogger } from '@hyperlane-xyz/utils'; import { MultiProvider } from '../../../MultiProvider.js'; +import { PopulatedTransactions } from '../../types.js'; import { TxSubmitterType } from '../TxSubmitterTypes.js'; import { EV5TxSubmitterInterface } from './EV5TxSubmitterInterface.js'; @@ -19,7 +20,7 @@ export class EV5JsonRpcTxSubmitter implements EV5TxSubmitterInterface { constructor(public readonly multiProvider: MultiProvider) {} public async submit( - ...txs: PopulatedTransaction[] + ...txs: PopulatedTransactions ): Promise { const receipts: TransactionReceipt[] = []; for (const tx of txs) { diff --git a/typescript/sdk/src/providers/transactions/submitter/schemas.ts b/typescript/sdk/src/providers/transactions/submitter/schemas.ts index 0c4185aaf..9ec67f19b 100644 --- a/typescript/sdk/src/providers/transactions/submitter/schemas.ts +++ b/typescript/sdk/src/providers/transactions/submitter/schemas.ts @@ -9,14 +9,13 @@ import { export const SubmitterMetadataSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal(TxSubmitterType.JSON_RPC), - props: z.object({}).optional(), }), z.object({ type: z.literal(TxSubmitterType.IMPERSONATED_ACCOUNT), - props: EV5ImpersonatedAccountTxSubmitterPropsSchema, + ...EV5ImpersonatedAccountTxSubmitterPropsSchema.shape, }), z.object({ type: z.literal(TxSubmitterType.GNOSIS_SAFE), - props: EV5GnosisSafeTxSubmitterPropsSchema, + ...EV5GnosisSafeTxSubmitterPropsSchema.shape, }), ]); diff --git a/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.ts b/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.ts index 9a3e659fb..62aefcf09 100644 --- a/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.ts +++ b/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.ts @@ -1,7 +1,7 @@ import { ethers } from 'ethers'; import { Logger } from 'pino'; -import { assert, objKeys, rootLogger } from '@hyperlane-xyz/utils'; +import { assert, objMap, rootLogger } from '@hyperlane-xyz/utils'; import { InterchainAccount, @@ -9,7 +9,11 @@ import { } from '../../../../middleware/account/InterchainAccount.js'; import { ChainName } from '../../../../types.js'; import { MultiProvider } from '../../../MultiProvider.js'; -import { CallData, PopulatedTransaction } from '../../types.js'; +import { + CallData, + PopulatedTransaction, + PopulatedTransactions, +} from '../../types.js'; import { TxTransformerType } from '../TxTransformerTypes.js'; import { EV5TxTransformerInterface } from './EV5TxTransformerInterface.js'; @@ -35,7 +39,7 @@ export class EV5InterchainAccountTxTransformer } public async transform( - ...txs: PopulatedTransaction[] + ...txs: PopulatedTransactions ): Promise { const txChainsToInnerCalls: Record = txs.reduce( ( @@ -57,17 +61,17 @@ export class EV5InterchainAccountTxTransformer ); const transformedTxs: ethers.PopulatedTransaction[] = []; - for (const txChain of objKeys(txChainsToInnerCalls)) { + objMap(txChainsToInnerCalls, async (destination, innerCalls) => { transformedTxs.push( await interchainAccountApp.getCallRemote({ chain: this.props.chain, - destination: txChain, - innerCalls: txChainsToInnerCalls[txChain], + destination, + innerCalls, config: this.props.config, hookMetadata: this.props.hookMetadata, }), ); - } + }); return transformedTxs; } diff --git a/typescript/sdk/src/providers/transactions/transformer/schemas.ts b/typescript/sdk/src/providers/transactions/transformer/schemas.ts index 14a5bb358..621e5d0c6 100644 --- a/typescript/sdk/src/providers/transactions/transformer/schemas.ts +++ b/typescript/sdk/src/providers/transactions/transformer/schemas.ts @@ -6,6 +6,6 @@ import { EV5InterchainAccountTxTransformerPropsSchema } from './ethersV5/schemas export const TransformerMetadataSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal(TxTransformerType.INTERCHAIN_ACCOUNT), - props: EV5InterchainAccountTxTransformerPropsSchema, + ...EV5InterchainAccountTxTransformerPropsSchema.shape, }), ]); diff --git a/typescript/sdk/src/providers/transactions/types.ts b/typescript/sdk/src/providers/transactions/types.ts index b1b91b1c4..4ed4e2319 100644 --- a/typescript/sdk/src/providers/transactions/types.ts +++ b/typescript/sdk/src/providers/transactions/types.ts @@ -1,9 +1,17 @@ import { ethers } from 'ethers'; import { z } from 'zod'; -import { CallDataSchema, PopulatedTransactionSchema } from './schemas.js'; +import { + CallDataSchema, + PopulatedTransactionSchema, + PopulatedTransactionsSchema, +} from './schemas.js'; export type PopulatedTransaction = z.infer & ethers.PopulatedTransaction; +export type PopulatedTransactions = z.infer< + typeof PopulatedTransactionsSchema +> & + ethers.PopulatedTransaction[]; export type CallData = z.infer;