Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create CLI Submitter #3730

Merged
merged 14 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-cheetahs-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': patch
---

Exports submitter and transformer props types.
5 changes: 5 additions & 0 deletions .changeset/sixty-avocados-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separate changesets for the CLI and SDK, very nice thank you.

'@hyperlane-xyz/cli': minor
---

Add CLI-side submitter to use SDK submitter from CRUD and other command modules.
105 changes: 105 additions & 0 deletions typescript/cli/src/submit/submit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {
EV5GnosisSafeTxSubmitter,
EV5ImpersonatedAccountTxSubmitter,
EV5InterchainAccountTxTransformer,
EV5JsonRpcTxSubmitter,
MultiProvider,
TxSubmitterBuilder,
TxSubmitterInterface,
TxSubmitterType,
TxTransformerInterface,
TxTransformerType,
} from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';

import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js';

import {
SubmitterBuilderSettings,
SubmitterMetadata,
TransformerMetadata,
} from './submitTypes.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,
{ type, settings }: SubmitterMetadata,
): Promise<TxSubmitterInterface<TProtocol>> {
switch (type) {
case TxSubmitterType.JSON_RPC:
return new EV5JsonRpcTxSubmitter(multiProvider);
case TxSubmitterType.IMPERSONATED_ACCOUNT:
if (!settings)
throw new Error(
'Must provide EV5ImpersonatedAccountTxSubmitterProps for impersonated account submitter.',
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the little assert function in our utils package.
assert(settings, 'EV5ImpersonatedAccountTxSubmitterProps required for impersonated account submitter')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For whatever reason it seems like we were using this convention more CLI-side, and assert SDK-side. In either case, now using the utils assert!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the one downside is its slightly less readable to the average TS dev but I like the brevity :)


await verifyAnvil();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this switch case ever get invoked for a situation other than --dry-run? If not, do we need this call when we already do it in context.ts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right– once utilized by any of the command modules this will sit behind the context so there's no need to verify or fork here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think so

await forkNetworkToMultiProvider(
multiProvider,
settings.eV5ImpersonatedAccountProps.chain,
);

return new EV5ImpersonatedAccountTxSubmitter(
multiProvider,
settings.eV5ImpersonatedAccountProps,
);
case TxSubmitterType.GNOSIS_SAFE:
if (!settings)
throw new Error(
'Must provide EV5GnosisSafeTxSubmitterProps for Gnosis safe submitter.',
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we have the required settings somehow be derived from the discriminated union?
ie the JSON_RPC submitter has an empty/undefined settings object etc

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could also use zod schemas here to validate the input

return new EV5GnosisSafeTxSubmitter(
multiProvider,
settings.eV5GnosisSafeProps,
);
default:
throw new Error(`Invalid TxSubmitterType: ${type}`);
}
}

async function getTransformers<TProtocol extends ProtocolType>(
multiProvider: MultiProvider,
metadata: TransformerMetadata[],
): Promise<TxTransformerInterface<TProtocol>[]> {
return Promise.all(
metadata.map(({ type, settings }) =>
getTransformer<TProtocol>(multiProvider, { type, settings }),
),
);
}

async function getTransformer<TProtocol extends ProtocolType>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these do not need to be async

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't look resolved?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't look resolved?

We chatted about this in the office– since getTransformer must call getCallRemote (which in turn might make a call to network), we're ok with keeping this async

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the async transform doesnt happen in this function iiuc

multiProvider: MultiProvider,
{ type, settings }: TransformerMetadata,
): Promise<TxTransformerInterface<TProtocol>> {
switch (type) {
case TxTransformerType.ICA:
if (!settings)
throw new Error(
'Must provide EV5InterchainAccountTxTransformerProps for ICA transformer.',
);
return new EV5InterchainAccountTxTransformer(
multiProvider,
settings.eV5InterchainAccountProps,
);
default:
throw new Error(`Invalid TxTransformerType: ${type}`);
}
}
30 changes: 30 additions & 0 deletions typescript/cli/src/submit/submitTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convention is to call this file just types and let it's location provide the context

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah– I thought I refactored this back to types. Updated

EV5GnosisSafeTxSubmitterProps,
EV5ImpersonatedAccountTxSubmitterProps,
EV5InterchainAccountTxTransformerProps,
MultiProvider,
TxSubmitterType,
TxTransformerType,
} from '@hyperlane-xyz/sdk';

export interface SubmitterBuilderSettings {
submitterMetadata: SubmitterMetadata;
transformersMetadata: TransformerMetadata[];
multiProvider: MultiProvider;
}
export interface SubmitterMetadata {
type: TxSubmitterType;
settings?: SubmitterSettings;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can the settings type be derived from the TxSubmitterType?

Suggested change
export interface SubmitterMetadata {
type: TxSubmitterType;
settings?: SubmitterSettings;
}
export interface SubmitterMetadata<T extends TxSubmitterType> {
type: T;
settings?: SubmitterSettings<T>;
}

something like this

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and at that point submitter metadata seems superfluous with the submitter settings interface itself

export interface TransformerMetadata {
type: TxTransformerType;
settings?: TransformerSettings;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto here
seems as though we can just use TransformerSettings without this intermediate interface?


interface SubmitterSettings {
eV5GnosisSafeProps: EV5GnosisSafeTxSubmitterProps;
eV5ImpersonatedAccountProps: EV5ImpersonatedAccountTxSubmitterProps;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this would be a union type
in what situation would the user define both of these fields? each invocation of the submit command has one submitter afaict

interface TransformerSettings {
eV5InterchainAccountProps: EV5InterchainAccountTxTransformerProps;
}
11 changes: 9 additions & 2 deletions typescript/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,21 @@ export {
protocolToDefaultProviderBuilder,
} from './providers/providerBuilders.js';
export { TxSubmitterInterface } from './providers/transactions/submitter/TxSubmitterInterface.js';
export { TxSubmitterType } from './providers/transactions/submitter/TxSubmitterTypes.js';
export {
TxSubmitterType,
EV5GnosisSafeTxSubmitterProps,
EV5ImpersonatedAccountTxSubmitterProps,
} from './providers/transactions/submitter/TxSubmitterTypes.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';
export { EV5JsonRpcTxSubmitter } from './providers/transactions/submitter/ethersV5/EV5JsonRpcTxSubmitter.js';
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 {
TxTransformerType,
EV5InterchainAccountTxTransformerProps,
} from './providers/transactions/transformer/TxTransformerTypes.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';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ProtocolType } from '@hyperlane-xyz/utils';

import { ChainName } from '../../../types.js';
import {
ProtocolTypedProvider,
ProtocolTypedReceipt,
Expand All @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import { Address } from '@hyperlane-xyz/utils';

import { ChainName } from '../../../types.js';

export enum TxSubmitterType {
JSON_RPC = 'JSON RPC',
IMPERSONATED_ACCOUNT = 'Impersonated Account',
GNOSIS_SAFE = 'Gnosis Safe',
}

export interface EV5GnosisSafeTxSubmitterProps {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for my own understanding, is this actually specific to ethers v5?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes– on Solana, for example, we may use Snowflake.

All the submitter+transformer implementations are protocol-specific (organized by protocol dir, i.e. /ethersV5)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reread and I see you may just be referring to this props interface (and not the submitter)– IMO it might be a little early to try and assume an all-inclusive interface, but if you believe something like SafeTxSubmitterProps would be useful I can make the update

FWIW we chatted offline on this, and @tkporter mentioned "We use Squads for our Solana deployment (just v2 for now, but v3 soonish). For the transaction submitter rn imo we should just worry about implementing EVM transaction submitters for now and we can deal with Solana if/when we move over to using the CLI for managing Solana deployments" (src)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know gnosis is ethereum specific
ethers v5 is a specific ethereum library and I was just wondering if this needs to be labeled with that

chain: ChainName;
safeAddress: Address;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be just address for consistency?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think so, since safeAddress is definitely only used to set the safeAddress for proposals

With that said, I updated the below field on EV5ImpersonatedAccountTxSubmitterProps to be userAddress for symmetry (also more accurate)

}

export interface EV5ImpersonatedAccountTxSubmitterProps {
chain: ChainName;
address: Address;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',
Expand All @@ -46,7 +44,6 @@ export class TxSubmitterBuilder<TProtocol extends ProtocolType>
private currentTransformers: TxTransformerInterface<TProtocol>[] = [],
) {
this.txSubmitterType = this.currentSubmitter.txSubmitterType;
this.chain = this.currentSubmitter.chain;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@ 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 {
EV5GnosisSafeTxSubmitterProps,
TxSubmitterType,
} from '../TxSubmitterTypes.js';

import { EV5TxSubmitterInterface } from './EV5TxSubmitterInterface.js';

interface EV5GnosisSafeTxSubmitterProps {
safeAddress: Address;
}

export class EV5GnosisSafeTxSubmitter implements EV5TxSubmitterInterface {
public readonly txSubmitterType: TxSubmitterType =
TxSubmitterType.GNOSIS_SAFE;
Expand All @@ -25,25 +23,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' };
},
Expand All @@ -55,13 +59,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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,16 @@ 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 {
EV5ImpersonatedAccountTxSubmitterProps,
TxSubmitterType,
} from '../TxSubmitterTypes.js';

import { EV5JsonRpcTxSubmitter } from './EV5JsonRpcTxSubmitter.js';

interface EV5ImpersonatedAccountTxSubmitterProps {
address: Address;
}

export class EV5ImpersonatedAccountTxSubmitter extends EV5JsonRpcTxSubmitter {
public readonly txSubmitterType: TxSubmitterType =
TxSubmitterType.IMPERSONATED_ACCOUNT;
Expand All @@ -26,18 +23,17 @@ export class EV5ImpersonatedAccountTxSubmitter extends EV5JsonRpcTxSubmitter {

constructor(
public readonly multiProvider: MultiProvider,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not reuse the multiprovider on the EV5JsonRpcTxSubmitter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason not to– updated to use parent's

public readonly chain: ChainName,
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);
this.multiProvider.setSharedSigner(impersonatedAccount);
super.multiProvider.setSharedSigner(impersonatedAccount);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we just reuse supers?

Suggested change
this.multiProvider.setSharedSigner(impersonatedAccount);
super.multiProvider.setSharedSigner(impersonatedAccount);
super.multiProvider.setSharedSigner(impersonatedAccount);

return await super.submit(...txs);
Comment on lines +35 to 36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk what the implication is of not doing this but should we
stopImpersonating after submitting?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting- I think we should do this in order to keep the submission atomic, and I don't think this should have any downstream effects since we are also impersonating during this call

will address as a part of #3786

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);
}
Expand Down
Loading
Loading