diff --git a/.changeset/quiet-cheetahs-own.md b/.changeset/quiet-cheetahs-own.md new file mode 100644 index 0000000000..df89570596 --- /dev/null +++ b/.changeset/quiet-cheetahs-own.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/sdk': patch +--- + +Exports submitter and transformer props types. diff --git a/.changeset/sixty-avocados-double.md b/.changeset/sixty-avocados-double.md new file mode 100644 index 0000000000..821e105c03 --- /dev/null +++ b/.changeset/sixty-avocados-double.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': minor +--- + +Add CLI-side submitter to use SDK submitter from CRUD and other command modules. diff --git a/typescript/cli/src/submit/submit.ts b/typescript/cli/src/submit/submit.ts new file mode 100644 index 0000000000..cc344c2f0b --- /dev/null +++ b/typescript/cli/src/submit/submit.ts @@ -0,0 +1,86 @@ +import { + EV5GnosisSafeTxSubmitter, + EV5GnosisSafeTxSubmitterProps, + EV5ImpersonatedAccountTxSubmitter, + EV5ImpersonatedAccountTxSubmitterProps, + EV5InterchainAccountTxTransformer, + EV5JsonRpcTxSubmitter, + MultiProvider, + TxSubmitterBuilder, + TxSubmitterInterface, + TxSubmitterType, + TxTransformerInterface, + TxTransformerType, +} from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { + SubmitterBuilderSettings, + SubmitterMetadata, + TransformerMetadata, +} from './types.js'; + +export async function getSubmitterBuilder({ + submitterMetadata, + transformersMetadata, + multiProvider, +}: SubmitterBuilderSettings): Promise> { + const submitter = await getSubmitter( + multiProvider, + submitterMetadata, + ); + const transformers = await getTransformers( + multiProvider, + transformersMetadata, + ); + + return new TxSubmitterBuilder(submitter, transformers); +} + +async function getSubmitter( + multiProvider: MultiProvider, + submitterMetadata: SubmitterMetadata, +): Promise> { + switch (submitterMetadata.type) { + case TxSubmitterType.JSON_RPC: + return new EV5JsonRpcTxSubmitter(multiProvider); + case TxSubmitterType.IMPERSONATED_ACCOUNT: + return new EV5ImpersonatedAccountTxSubmitter( + multiProvider, + submitterMetadata.props as EV5ImpersonatedAccountTxSubmitterProps, + ); + case TxSubmitterType.GNOSIS_SAFE: + return new EV5GnosisSafeTxSubmitter( + multiProvider, + submitterMetadata.props as EV5GnosisSafeTxSubmitterProps, + ); + default: + throw new Error(`Invalid TxSubmitterType: ${submitterMetadata.type}`); + } +} + +async function getTransformers( + multiProvider: MultiProvider, + metadata: TransformerMetadata[], +): Promise[]> { + return Promise.all( + metadata.map(({ type, props: settings }) => + getTransformer(multiProvider, { type, props: settings }), + ), + ); +} + +async function getTransformer( + multiProvider: MultiProvider, + transformerMetadata: TransformerMetadata, +): Promise> { + switch (transformerMetadata.type) { + case TxTransformerType.ICA: + return new EV5InterchainAccountTxTransformer( + multiProvider, + transformerMetadata.props, + ); + default: + throw new Error(`Invalid TxTransformerType: ${transformerMetadata.type}`); + } +} diff --git a/typescript/cli/src/submit/types.ts b/typescript/cli/src/submit/types.ts new file mode 100644 index 0000000000..d50a63e22c --- /dev/null +++ b/typescript/cli/src/submit/types.ts @@ -0,0 +1,27 @@ +import type { + EV5GnosisSafeTxSubmitterProps, + EV5ImpersonatedAccountTxSubmitterProps, + EV5InterchainAccountTxTransformerProps, + MultiProvider, + TxSubmitterType, + TxTransformerType, +} from '@hyperlane-xyz/sdk'; + +export interface SubmitterBuilderSettings { + submitterMetadata: SubmitterMetadata; + transformersMetadata: TransformerMetadata[]; + 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 27c936509c..fd338351e7 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -305,6 +305,10 @@ export { } from './providers/providerBuilders.js'; export { TxSubmitterInterface } from './providers/transactions/submitter/TxSubmitterInterface.js'; export { TxSubmitterType } from './providers/transactions/submitter/TxSubmitterTypes.js'; +export { + EV5GnosisSafeTxSubmitterProps, + EV5ImpersonatedAccountTxSubmitterProps, +} from './providers/transactions/submitter/ethersV5/EV5TxSubmitterTypes.js'; export { TxSubmitterBuilder } from './providers/transactions/submitter/builder/TxSubmitterBuilder.js'; export { EV5GnosisSafeTxSubmitter } from './providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.js'; export { EV5ImpersonatedAccountTxSubmitter } from './providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.js'; @@ -312,6 +316,7 @@ export { EV5JsonRpcTxSubmitter } from './providers/transactions/submitter/ethers export { EV5TxSubmitterInterface } from './providers/transactions/submitter/ethersV5/EV5TxSubmitterInterface.js'; export { TxTransformerInterface } from './providers/transactions/transformer/TxTransformerInterface.js'; export { TxTransformerType } from './providers/transactions/transformer/TxTransformerTypes.js'; +export { EV5InterchainAccountTxTransformerProps } from './providers/transactions/transformer/ethersV5/EV5TxTransformerTypes.js'; export { EV5InterchainAccountTxTransformer } from './providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.js'; export { EV5TxTransformerInterface } from './providers/transactions/transformer/ethersV5/EV5TxTransformerInterface.js'; export { GasRouterDeployer } from './router/GasRouterDeployer.js'; diff --git a/typescript/sdk/src/providers/transactions/submitter/TxSubmitterInterface.ts b/typescript/sdk/src/providers/transactions/submitter/TxSubmitterInterface.ts index b857bd990d..69f8718e2b 100644 --- a/typescript/sdk/src/providers/transactions/submitter/TxSubmitterInterface.ts +++ b/typescript/sdk/src/providers/transactions/submitter/TxSubmitterInterface.ts @@ -1,6 +1,5 @@ import { ProtocolType } from '@hyperlane-xyz/utils'; -import { ChainName } from '../../../types.js'; import { ProtocolTypedProvider, ProtocolTypedReceipt, @@ -14,10 +13,6 @@ export interface TxSubmitterInterface { * Defines the type of tx submitter. */ txSubmitterType: TxSubmitterType; - /** - * The chain to submit transactions on. - */ - chain: ChainName; /** * The provider to use for transaction submission. */ diff --git a/typescript/sdk/src/providers/transactions/submitter/builder/TxSubmitterBuilder.ts b/typescript/sdk/src/providers/transactions/submitter/builder/TxSubmitterBuilder.ts index 628bda0352..797f3ea73f 100644 --- a/typescript/sdk/src/providers/transactions/submitter/builder/TxSubmitterBuilder.ts +++ b/typescript/sdk/src/providers/transactions/submitter/builder/TxSubmitterBuilder.ts @@ -3,7 +3,6 @@ import { Logger } from 'pino'; import { rootLogger } from '@hyperlane-xyz/utils'; import { ProtocolType } from '@hyperlane-xyz/utils'; -import { ChainName } from '../../../../types.js'; import { ProtocolTypedReceipt, ProtocolTypedTransaction, @@ -35,7 +34,6 @@ export class TxSubmitterBuilder implements TxSubmitterInterface { public readonly txSubmitterType: TxSubmitterType; - public readonly chain: ChainName; protected readonly logger: Logger = rootLogger.child({ module: 'submitter-builder', @@ -46,7 +44,6 @@ export class TxSubmitterBuilder private currentTransformers: TxTransformerInterface[] = [], ) { this.txSubmitterType = this.currentSubmitter.txSubmitterType; - this.chain = this.currentSubmitter.chain; } /** diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.ts index 9975f560d3..5fb760e327 100644 --- a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.ts +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.ts @@ -3,17 +3,13 @@ import { Logger } from 'pino'; import { Address, assert, rootLogger } from '@hyperlane-xyz/utils'; -import { ChainName } from '../../../../types.js'; // @ts-ignore import { getSafe, getSafeService } from '../../../../utils/gnosisSafe.js'; import { MultiProvider } from '../../../MultiProvider.js'; import { TxSubmitterType } from '../TxSubmitterTypes.js'; import { EV5TxSubmitterInterface } from './EV5TxSubmitterInterface.js'; - -interface EV5GnosisSafeTxSubmitterProps { - safeAddress: Address; -} +import { EV5GnosisSafeTxSubmitterProps } from './EV5TxSubmitterTypes.js'; export class EV5GnosisSafeTxSubmitter implements EV5TxSubmitterInterface { public readonly txSubmitterType: TxSubmitterType = @@ -25,25 +21,31 @@ export class EV5GnosisSafeTxSubmitter implements EV5TxSubmitterInterface { constructor( public readonly multiProvider: MultiProvider, - public readonly chain: ChainName, public readonly props: EV5GnosisSafeTxSubmitterProps, ) {} public async submit(...txs: PopulatedTransaction[]): Promise { const safe = await getSafe( - this.chain, + this.props.chain, this.multiProvider, this.props.safeAddress, ); - const safeService = await getSafeService(this.chain, this.multiProvider); + const safeService = await getSafeService( + this.props.chain, + this.multiProvider, + ); const nextNonce: number = await safeService.getNextNonce( this.props.safeAddress, ); const safeTransactionBatch: any[] = txs.map( - ({ to, data, value }: PopulatedTransaction) => { + ({ to, data, value, chainId }: PopulatedTransaction) => { + assert(to, 'Invalid PopulatedTransaction: Missing to field'); + assert(data, 'Invalid PopulatedTransaction: Missing data field'); + assert(chainId, 'Invalid PopulatedTransaction: Missing chainId field'); + const txChain = this.multiProvider.getChainName(chainId); assert( - to && data, - 'Invalid PopulatedTransaction: Missing required field to or data.', + txChain === this.props.chain, + `Invalid PopulatedTransaction: Cannot submit ${txChain} tx to ${this.props.chain} submitter.`, ); return { to, data, value: value?.toString() ?? '0' }; }, @@ -55,13 +57,13 @@ export class EV5GnosisSafeTxSubmitter implements EV5TxSubmitterInterface { const safeTransactionData: any = safeTransaction.data; const safeTxHash: string = await safe.getTransactionHash(safeTransaction); const senderAddress: Address = await this.multiProvider.getSignerAddress( - this.chain, + this.props.chain, ); const safeSignature: any = await safe.signTransactionHash(safeTxHash); const senderSignature: string = safeSignature.data; this.logger.debug( - `Submitting transaction proposal to ${this.props.safeAddress} on ${this.chain}: ${safeTxHash}`, + `Submitting transaction proposal to ${this.props.safeAddress} on ${this.props.chain}: ${safeTxHash}`, ); return safeService.proposeTransaction({ diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.ts index 3511d00b16..816baf0535 100644 --- a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.ts +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.ts @@ -3,18 +3,13 @@ import { PopulatedTransaction } from 'ethers'; import { Logger } from 'pino'; import { rootLogger } from '@hyperlane-xyz/utils'; -import { Address } from '@hyperlane-xyz/utils'; -import { ChainName } from '../../../../types.js'; import { impersonateAccount } from '../../../../utils/fork.js'; import { MultiProvider } from '../../../MultiProvider.js'; import { TxSubmitterType } from '../TxSubmitterTypes.js'; import { EV5JsonRpcTxSubmitter } from './EV5JsonRpcTxSubmitter.js'; - -interface EV5ImpersonatedAccountTxSubmitterProps { - address: Address; -} +import { EV5ImpersonatedAccountTxSubmitterProps } from './EV5TxSubmitterTypes.js'; export class EV5ImpersonatedAccountTxSubmitter extends EV5JsonRpcTxSubmitter { public readonly txSubmitterType: TxSubmitterType = @@ -25,19 +20,19 @@ export class EV5ImpersonatedAccountTxSubmitter extends EV5JsonRpcTxSubmitter { }); constructor( - public readonly multiProvider: MultiProvider, - public readonly chain: ChainName, + multiProvider: MultiProvider, public readonly props: EV5ImpersonatedAccountTxSubmitterProps, ) { - super(multiProvider, chain); + super(multiProvider); } public async submit( ...txs: PopulatedTransaction[] ): Promise { - const impersonatedAccount = await impersonateAccount(this.props.address); - this.multiProvider.setSigner(this.chain, impersonatedAccount); - super.multiProvider.setSigner(this.chain, impersonatedAccount); + const impersonatedAccount = await impersonateAccount( + this.props.userAddress, + ); + super.multiProvider.setSharedSigner(impersonatedAccount); return await super.submit(...txs); } } diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.ts index e1077d7c5c..f50c7f8228 100644 --- a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.ts +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.ts @@ -2,9 +2,8 @@ import { TransactionReceipt } from '@ethersproject/providers'; import { ContractReceipt, PopulatedTransaction } from 'ethers'; import { Logger } from 'pino'; -import { rootLogger } from '@hyperlane-xyz/utils'; +import { assert, rootLogger } from '@hyperlane-xyz/utils'; -import { ChainName } from '../../../../types.js'; import { MultiProvider } from '../../../MultiProvider.js'; import { TxSubmitterType } from '../TxSubmitterTypes.js'; @@ -17,22 +16,21 @@ export class EV5JsonRpcTxSubmitter implements EV5TxSubmitterInterface { module: 'json-rpc-submitter', }); - constructor( - public readonly multiProvider: MultiProvider, - public readonly chain: ChainName, - ) {} + constructor(public readonly multiProvider: MultiProvider) {} public async submit( ...txs: PopulatedTransaction[] ): Promise { const receipts: TransactionReceipt[] = []; for (const tx of txs) { + assert(tx.chainId, 'Invalid PopulatedTransaction: Missing chainId field'); + const txChain = this.multiProvider.getChainName(tx.chainId); const receipt: ContractReceipt = await this.multiProvider.sendTransaction( - this.chain, + txChain, tx, ); this.logger.debug( - `Submitted PopulatedTransaction on ${this.chain}: ${receipt.transactionHash}`, + `Submitted PopulatedTransaction on ${txChain}: ${receipt.transactionHash}`, ); receipts.push(receipt); } diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5TxSubmitterTypes.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5TxSubmitterTypes.ts new file mode 100644 index 0000000000..cf6f7f164c --- /dev/null +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5TxSubmitterTypes.ts @@ -0,0 +1,13 @@ +import { Address } from '@hyperlane-xyz/utils'; + +import { ChainName } from '../../../../types.js'; + +export interface EV5GnosisSafeTxSubmitterProps { + chain: ChainName; + safeAddress: Address; +} + +export interface EV5ImpersonatedAccountTxSubmitterProps { + chain: ChainName; + userAddress: Address; +} diff --git a/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.ts b/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.ts index 2e2ddfb520..25ae331e76 100644 --- a/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.ts +++ b/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.ts @@ -3,19 +3,12 @@ import { Logger } from 'pino'; import { CallData, assert, rootLogger } from '@hyperlane-xyz/utils'; -import { InterchainAccount } from '../../../../middleware/account/InterchainAccount.js'; -import { AccountConfig } from '../../../../middleware/account/types.js'; import { ChainName } from '../../../../types.js'; import { MultiProvider } from '../../../MultiProvider.js'; import { TxTransformerType } from '../TxTransformerTypes.js'; import { EV5TxTransformerInterface } from './EV5TxTransformerInterface.js'; - -interface EV5InterchainAccountTxTransformerProps { - interchainAccount: InterchainAccount; - accountConfig: AccountConfig; - hookMetadata?: string; -} +import { EV5InterchainAccountTxTransformerProps } from './EV5TxTransformerTypes.js'; export class EV5InterchainAccountTxTransformer implements EV5TxTransformerInterface @@ -27,35 +20,36 @@ export class EV5InterchainAccountTxTransformer constructor( public readonly multiProvider: MultiProvider, - public readonly chain: ChainName, public readonly props: EV5InterchainAccountTxTransformerProps, ) {} public async transform( ...txs: PopulatedTransaction[] ): Promise { - const destinationChainId = txs[0].chainId; - assert( - destinationChainId, - 'Missing destination chainId in PopulatedTransaction.', - ); - - const innerCalls: CallData[] = txs.map( - ({ to, data, value }: PopulatedTransaction) => { - assert(to, 'Invalid PopulatedTransaction: Missing to field'); - assert(data, 'Invalid PopulatedTransaction: Missing data field'); - return { to, data, value }; - }, - ); - - return [ - await this.props.interchainAccount.getCallRemote( - this.chain, - this.multiProvider.getChainName(this.chain), //chainIdToMetadata[destinationChainId].name, - innerCalls, - this.props.accountConfig, - this.props.hookMetadata, - ), - ]; + const txChainsToInnerCalls: Record = {}; + + txs.map(({ to, data, value, chainId }: PopulatedTransaction) => { + assert(to, 'Invalid PopulatedTransaction: Missing to field'); + assert(data, 'Invalid PopulatedTransaction: Missing data field'); + assert(chainId, 'Invalid PopulatedTransaction: Missing chainId field'); + const txChain = this.multiProvider.getChainName(chainId); + if (!txChainsToInnerCalls[txChain]) txChainsToInnerCalls[txChain] = []; + txChainsToInnerCalls[txChain].push({ to, data, value }); + }); + + const transformedTxs: Promise[] = []; + Object.keys(txChainsToInnerCalls).map((txChain: ChainName) => { + transformedTxs.push( + this.props.interchainAccount.getCallRemote( + this.props.chain, + txChain, + txChainsToInnerCalls[txChain], + this.props.accountConfig, + this.props.hookMetadata, + ), + ); + }); + + return Promise.all(transformedTxs); } } diff --git a/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5TxTransformerTypes.ts b/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5TxTransformerTypes.ts new file mode 100644 index 0000000000..e8c7eb06aa --- /dev/null +++ b/typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5TxTransformerTypes.ts @@ -0,0 +1,10 @@ +import { InterchainAccount } from '../../../../middleware/account/InterchainAccount.js'; +import { AccountConfig } from '../../../../middleware/account/types.js'; +import { ChainName } from '../../../../types.js'; + +export interface EV5InterchainAccountTxTransformerProps { + chain: ChainName; + interchainAccount: InterchainAccount; + accountConfig: AccountConfig; + hookMetadata?: string; +}