Create CLI Submitter (#3730)

### Description

* A CLI-side submitter to interact with the SDK's tx submitter builder

### Drive-by changes

* None

### Related issues

* Fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3738

### Backward compatibility

* Yes

### Testing

* None
pull/3752/head
Noah Bayindirli 🥂 6 months ago committed by GitHub
parent f0df1a4cd1
commit eba3936803
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/quiet-cheetahs-own.md
  2. 5
      .changeset/sixty-avocados-double.md
  3. 86
      typescript/cli/src/submit/submit.ts
  4. 27
      typescript/cli/src/submit/types.ts
  5. 5
      typescript/sdk/src/index.ts
  6. 5
      typescript/sdk/src/providers/transactions/submitter/TxSubmitterInterface.ts
  7. 3
      typescript/sdk/src/providers/transactions/submitter/builder/TxSubmitterBuilder.ts
  8. 28
      typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.ts
  9. 19
      typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.ts
  10. 14
      typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.ts
  11. 13
      typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5TxSubmitterTypes.ts
  12. 58
      typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5InterchainAccountTxTransformer.ts
  13. 10
      typescript/sdk/src/providers/transactions/transformer/ethersV5/EV5TxTransformerTypes.ts

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': patch
---
Exports submitter and transformer props types.

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/cli': minor
---
Add CLI-side submitter to use SDK submitter from CRUD and other command modules.

@ -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<TProtocol extends ProtocolType>({
submitterMetadata,
transformersMetadata,
multiProvider,
}: SubmitterBuilderSettings): Promise<TxSubmitterBuilder<TProtocol>> {
const submitter = await getSubmitter<TProtocol>(
multiProvider,
submitterMetadata,
);
const transformers = await getTransformers<TProtocol>(
multiProvider,
transformersMetadata,
);
return new TxSubmitterBuilder<TProtocol>(submitter, transformers);
}
async function getSubmitter<TProtocol extends ProtocolType>(
multiProvider: MultiProvider,
submitterMetadata: SubmitterMetadata,
): Promise<TxSubmitterInterface<TProtocol>> {
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<TProtocol extends ProtocolType>(
multiProvider: MultiProvider,
metadata: TransformerMetadata[],
): Promise<TxTransformerInterface<TProtocol>[]> {
return Promise.all(
metadata.map(({ type, props: settings }) =>
getTransformer<TProtocol>(multiProvider, { type, props: settings }),
),
);
}
async function getTransformer<TProtocol extends ProtocolType>(
multiProvider: MultiProvider,
transformerMetadata: TransformerMetadata,
): Promise<TxTransformerInterface<TProtocol>> {
switch (transformerMetadata.type) {
case TxTransformerType.ICA:
return new EV5InterchainAccountTxTransformer(
multiProvider,
transformerMetadata.props,
);
default:
throw new Error(`Invalid TxTransformerType: ${transformerMetadata.type}`);
}
}

@ -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;

@ -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';

@ -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<TProtocol extends ProtocolType> {
* Defines the type of tx submitter.
*/
txSubmitterType: TxSubmitterType;
/**
* The chain to submit transactions on.
*/
chain: ChainName;
/**
* The provider to use for transaction submission.
*/

@ -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<TProtocol extends ProtocolType>
implements TxSubmitterInterface<TProtocol>
{
public readonly txSubmitterType: TxSubmitterType;
public readonly chain: ChainName;
protected readonly logger: Logger = rootLogger.child({
module: 'submitter-builder',
@ -46,7 +44,6 @@ export class TxSubmitterBuilder<TProtocol extends ProtocolType>
private currentTransformers: TxTransformerInterface<TProtocol>[] = [],
) {
this.txSubmitterType = this.currentSubmitter.txSubmitterType;
this.chain = this.currentSubmitter.chain;
}
/**

@ -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<void> {
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({

@ -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<TransactionReceipt[]> {
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);
}
}

@ -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<TransactionReceipt[]> {
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);
}

@ -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;
}

@ -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<PopulatedTransaction[]> {
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<ChainName, CallData[]> = {};
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<PopulatedTransaction>[] = [];
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);
}
}

@ -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;
}
Loading…
Cancel
Save