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

feat: Modular Tx Submission – Create Transformer + Submitter + Builder Abstraction #3627

Merged
merged 27 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
81c4b8f
migrate fork util to SDK
nbayindirli Apr 18, 2024
99a880b
add changeset
nbayindirli Apr 18, 2024
54bc095
implement initial tx transformers + submitters + builder
nbayindirli Apr 19, 2024
834daa7
fix codespell
nbayindirli Apr 19, 2024
8d657cf
address comments
nbayindirli Apr 19, 2024
7b058c5
Merge branch 'noah/move-fork' into noah/txs
nbayindirli Apr 19, 2024
561aac1
Merge branch 'main' into noah/txs
nbayindirli Apr 22, 2024
335fc71
remove stale artifacts from merge
nbayindirli Apr 22, 2024
a5c5a92
flush out builder & remove DEFAULT tx cases
nbayindirli Apr 22, 2024
a1b723c
add Gnosis safe util to SDK
nbayindirli Apr 22, 2024
ca7c794
add changeset
nbayindirli Apr 23, 2024
e600759
Merge branch 'main' into noah/txs
nbayindirli Apr 23, 2024
38c85e2
complete all but ICA transform support
nbayindirli Apr 23, 2024
c6847ed
complete builder refactor + full ICA transformer integration
nbayindirli Apr 25, 2024
6a92a61
Merge branch 'main' into noah/txs
nbayindirli Apr 25, 2024
f3546d8
update builder jsdocs
nbayindirli Apr 25, 2024
5eb9900
implement multiple transformer support; remove all type-branch logic;…
nbayindirli Apr 29, 2024
a657e57
Merge branch 'main' into noah/txs
nbayindirli Apr 29, 2024
da9151c
update add() builder function to for()
nbayindirli Apr 29, 2024
6c3d62a
Remove direct refernce to EthSafeTransaction
nbayindirli Apr 29, 2024
08f6b66
address @yorhodes comments
nbayindirli Apr 30, 2024
1ba8804
Merge branch 'main' into noah/txs
nbayindirli Apr 30, 2024
e2e4877
address @pbio comments; expand on multi-protocol support + minor cleanup
nbayindirli May 1, 2024
a66d923
Merge branch 'main' into noah/txs
nbayindirli May 1, 2024
87f8be6
refactor transformers list to persist
nbayindirli May 1, 2024
face9b1
Merge branch 'main' into noah/txs
nbayindirli May 1, 2024
a9ecb22
read gnosis urls from multiProvider
nbayindirli May 1, 2024
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/blue-kings-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---

Adds modular transaction submission support for SDK clients, e.g. CLI.
3 changes: 3 additions & 0 deletions typescript/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"@cosmjs/stargate": "^0.31.3",
"@hyperlane-xyz/core": "3.10.0",
"@hyperlane-xyz/utils": "3.10.0",
"@safe-global/api-kit": "1.3.0",
"@safe-global/protocol-kit": "1.3.0",
"@solana/spl-token": "^0.3.8",
"@solana/web3.js": "^1.78.0",
"@types/coingecko-api": "^1.0.10",
Expand All @@ -17,6 +19,7 @@
"cross-fetch": "^3.1.5",
"ethers": "^5.7.2",
"pino": "^8.19.0",
"stack-typescript": "^1.0.4",
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
"viem": "^1.20.0",
"zod": "^3.21.2"
},
Expand Down
2 changes: 1 addition & 1 deletion typescript/sdk/src/providers/MultiProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ export class MultiProvider<MetaExt = {}> extends ChainMetadataManager<MetaExt> {
tx: PopulatedTransaction,
from?: string,
): Promise<providers.TransactionRequest> {
const txFrom = from ? from : await this.getSignerAddress(chainNameOrId);
const txFrom = from ?? (await this.getSignerAddress(chainNameOrId));
const overrides = this.getTransactionOverrides(chainNameOrId);
return {
...tx,
Expand Down
30 changes: 30 additions & 0 deletions typescript/sdk/src/providers/transactions/submitter/TxSubmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ChainName } from '../../../types.js';
import { MultiProvider } from '../../MultiProvider.js';
import {
TypedTransaction,
TypedTransactionReceipt,
} from '../../ProviderType.js';

import { TxSubmitterType } from './TxSubmitterTypes.js';

export interface TxSubmitterInterface<
TX extends TypedTransaction,
TR extends TypedTransactionReceipt,
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
> {
/**
* Defines the type of tx submitter.
*/
txSubmitterType: TxSubmitterType;
multiProvider: MultiProvider;
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
chain: ChainName;
/**
* Should execute all transactions and return their receipts.
* @param txs The array of transactions to execute
*/
submitTxs(txs: TX[]): Promise<TR[] | void>;
/**
* Should execute a transaction and return its receipt.
* @param tx The transaction to execute
*/
submitTx?(tx: TX): Promise<TR | void>;
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum TxSubmitterType {
JSON_RPC = 'Json RPC',
IMPERSONATED_ACCOUNT = 'Impersonated Account',
GNOSIS_SAFE = 'Gnosis Safe',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import assert from 'assert';
import { Logger } from 'pino';
import { Stack } from 'stack-typescript';

import { rootLogger } from '@hyperlane-xyz/utils';

import {
TypedTransaction,
TypedTransactionReceipt,
} from '../../../ProviderType.js';
import { TxTransformerInterface } from '../../transformer/TxTransformer.js';
import { TxSubmitterInterface } from '../TxSubmitter.js';

/**
* Builds a TxSubmitterBuilder for batch transaction submission.
*
* Example use-cases:
* const eV5builder = new TxSubmitterBuilder<EV5Transaction, EV5TransactionReceipt>();
* let txReceipts = eV5builder.for(
* new GnosisSafeTxSubmitter(chainA)
* ).transform(
* InterchainAccountTxTransformer(chainB)
* ).submit(
* txs
* );
* txReceipts = eV5builder.for(
* new ImpersonatedAccountTxSubmitter(chainA)
* ).submit(txs);
* txReceipts = eV5builder.for(
* new JsonRpcTxSubmitter(chainC)
* ).submit(txs);
*/
export class TxSubmitterBuilder<
TX extends TypedTransaction,
TR extends TypedTransactionReceipt,
> {
protected readonly logger: Logger = rootLogger.child({
module: 'submitter-builder',
});

private currentSubmitter?: TxSubmitterInterface<TX, TR>;

constructor(
private readonly currentTransformers: Stack<
TxTransformerInterface<TX>
> = new Stack<TxTransformerInterface<TX>>(),
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
) {}

/**
* Sets the current submitter for the builder.
* @param txSubmitterOrType The submitter to add to the builder
*/
public for(
txSubmitter: TxSubmitterInterface<TX, TR>,
): TxSubmitterBuilder<TX, TR> {
this.currentSubmitter = txSubmitter;
return this;
}

/**
* Adds a transformer for the builder.
* @param txTransformerOrType The transformer to add to the builder
*/
public transform(
txTransformer: TxTransformerInterface<TX>,
): TxSubmitterBuilder<TX, TR> {
assert(
this.currentSubmitter,
'No submitter specified for which to execute the transform.',
);

this.currentTransformers.push(txTransformer);
return this;
}

/**
* Submits a set of transactions to the builder.
* @param txs The transactions to submit
*/
public async submit(txs: TX[]): Promise<TR[]> {
assert(
this.currentSubmitter,
'Must specify submitter to submit transactions.',
);
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved

this.logger.info(
`Submitting ${txs.length} transactions to the ${this.currentSubmitter.txSubmitterType} submitter...`,
);

let transformedTxs = txs;
while (this.currentTransformers.size > 0) {
const currentTransformer: TxTransformerInterface<TX> =
this.currentTransformers.pop();
transformedTxs = await currentTransformer.transformTxs(transformedTxs);
this.logger.info(
`🔄 Transformed ${transformedTxs.length} transactions with the ${currentTransformer.txTransformerType} transformer...`,
);
}
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved

const txReceipts = await this.currentSubmitter.submitTxs(transformedTxs);
this.logger.info(
`✅ Successfully submitted ${transformedTxs.length} transactions to the ${this.currentSubmitter.txSubmitterType} submitter.`,
);

this.currentSubmitter = undefined;
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved

return txReceipts ?? [];
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import SafeApiKit from '@safe-global/api-kit';
import Safe, { EthSafeSignature } from '@safe-global/protocol-kit';
import assert from 'assert';
import { Logger } from 'pino';

import { Address, rootLogger } from '@hyperlane-xyz/utils';

import { ChainName } from '../../../../types.js';
import { getSafe, getSafeService } from '../../../../utils/gnosisSafe.js';
import { MultiProvider } from '../../../MultiProvider.js';
import {
EthersV5Transaction,
EthersV5TransactionReceipt,
} from '../../../ProviderType.js';
import { TxSubmitterInterface } from '../TxSubmitter.js';
import { TxSubmitterType } from '../TxSubmitterTypes.js';

enum OperationType {
Call = 0,
DelegateCall = 1,
}

interface MetaTransactionData {
to: string;
value: string;
data: string;
operation?: OperationType;
}

interface SafeTransactionData extends MetaTransactionData {
operation: OperationType;
safeTxGas: string;
baseGas: string;
gasPrice: string;
gasToken: string;
refundReceiver: string;
nonce: number;
}

interface GnosisSafeTxSubmitterProps {
safeAddress: Address;
signerAddress?: Address;
}

export class GnosisSafeTxSubmitter
implements
TxSubmitterInterface<EthersV5Transaction, EthersV5TransactionReceipt>
{
public readonly txSubmitterType: TxSubmitterType =
TxSubmitterType.GNOSIS_SAFE;

protected readonly logger: Logger = rootLogger.child({
module: 'gnosis-safe-submitter',
});

constructor(
public readonly multiProvider: MultiProvider,
public readonly chain: ChainName,
public readonly props: GnosisSafeTxSubmitterProps,
) {}

public async submitTxs(txs: EthersV5Transaction[]): Promise<void> {
const safe: Safe.default = await getSafe(
this.chain,
this.multiProvider,
this.props.safeAddress,
);
const safeService: SafeApiKit.default = getSafeService(
this.chain,
this.multiProvider,
);
const nextNonce: number = await safeService.getNextNonce(
this.props.safeAddress,
);
const safeTransactionBatch: MetaTransactionData[] = txs.map(
({ transaction }: EthersV5Transaction) => {
const { to, data, value } = transaction;
assert(
to && data,
'Invalid EthersV5Transaction: Missing required metadata.',
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved
);
return { to, data, value: value?.toString() ?? '0' };
},
);
const safeTransaction = await safe.createTransaction({
safeTransactionData: safeTransactionBatch,
options: { nonce: nextNonce },
});
const safeTransactionData: SafeTransactionData = safeTransaction.data;
const safeTxHash: string = await safe.getTransactionHash(safeTransaction);
let senderAddress: Address | undefined = this.props.signerAddress;
if (!senderAddress) {
senderAddress = await this.multiProvider.getSignerAddress(this.chain);
}
const safeSignature: EthSafeSignature = await safe.signTransactionHash(
safeTxHash,
);
const senderSignature: string = safeSignature.data;

this.logger.debug(
`Submitting transaction proposal to ${this.props.safeAddress} on ${this.chain}: ${safeTxHash}`,
);

return safeService.proposeTransaction({
safeAddress: this.props.safeAddress,
safeTransactionData,
safeTxHash,
senderAddress,
senderSignature,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ContractReceipt } 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 {
EthersV5Transaction,
EthersV5TransactionReceipt,
ProviderType,
} from '../../../ProviderType.js';
import { TxSubmitterInterface } from '../TxSubmitter.js';
import { TxSubmitterType } from '../TxSubmitterTypes.js';

interface ImpersonatedAccountTxSubmitterProps {
address: Address;
}

export class ImpersonatedAccountTxSubmitter
implements
TxSubmitterInterface<EthersV5Transaction, EthersV5TransactionReceipt>
{
public readonly txSubmitterType: TxSubmitterType =
TxSubmitterType.IMPERSONATED_ACCOUNT;

protected readonly logger: Logger = rootLogger.child({
module: 'impersonated-account-submitter',
});

constructor(
public readonly multiProvider: MultiProvider,
public readonly chain: ChainName,
public readonly props: ImpersonatedAccountTxSubmitterProps,
) {}

public async submitTxs(
txs: EthersV5Transaction[],
): Promise<EthersV5TransactionReceipt[]> {
const receipts: EthersV5TransactionReceipt[] = [];
for (const tx of txs) {
const receipt = await this.submitTx(tx);
receipts.push(receipt);
}
return receipts;
}

public async submitTx(
tx: EthersV5Transaction,
): Promise<EthersV5TransactionReceipt> {
const signer = await impersonateAccount(this.props.address);
this.multiProvider.setSigner(this.chain, signer);
const receipt: ContractReceipt = await this.multiProvider.sendTransaction(
this.chain,
tx.transaction,
);
nbayindirli marked this conversation as resolved.
Show resolved Hide resolved

this.logger.debug(
`Submitted EthersV5Transaction on ${this.chain}: ${receipt.transactionHash}`,
);

return { type: ProviderType.EthersV5, receipt };
}
}
Loading
Loading