Skip to content

Commit 2b708c2

Browse files
committed
feat: add sequential batch support
1 parent b53f7d7 commit 2b708c2

File tree

3 files changed

+181
-5
lines changed

3 files changed

+181
-5
lines changed

packages/transaction-controller/src/TransactionController.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,10 @@ export class TransactionController extends BaseController<
10081008
publicKeyEIP7702: this.#publicKeyEIP7702,
10091009
request,
10101010
updateTransaction: this.#updateTransactionInternal.bind(this),
1011+
publishTransaction: (
1012+
ethQuery: EthQuery,
1013+
transactionMeta: TransactionMeta,
1014+
) => this.#publishTransaction(ethQuery, transactionMeta) as Promise<Hex>,
10111015
});
10121016
}
10131017

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { query } from '@metamask/controller-utils';
2+
import type EthQuery from '@metamask/eth-query';
3+
import { rpcErrors } from '@metamask/rpc-errors';
4+
import { createModuleLogger } from '@metamask/utils';
5+
import type { Hex } from '@metamask/utils';
6+
7+
import { projectLogger } from '../logger';
8+
import type {
9+
PublishBatchHookTransaction,
10+
TransactionMeta,
11+
TransactionReceipt,
12+
} from '../types';
13+
14+
const TRANSACTION_CHECK_INTERVAL = 5000; // 5 seconds
15+
const MAX_TRANSACTION_CHECK_ATTEMPTS = 60; // 5 minutes
16+
const RECEIPT_STATUS_SUCCESS = '0x1';
17+
const RECEIPT_STATUS_FAILURE = '0x0';
18+
19+
const log = createModuleLogger(projectLogger, 'sequential-publish-batch-hook');
20+
21+
type SequentialPublishBatchHookParams = {
22+
publishTransaction: (
23+
_ethQuery: EthQuery,
24+
transactionMeta: TransactionMeta,
25+
) => Promise<Hex>;
26+
getTransaction: (id: string) => TransactionMeta;
27+
getEthQuery: (networkClientId: string) => EthQuery;
28+
};
29+
/**
30+
* Custom publish logic that also publishes additional sequential transactions in an batch.
31+
* Requires the batch to be successful to resolve.
32+
*/
33+
export class SequentialPublishBatchHook {
34+
readonly #publishTransaction: (
35+
_ethQuery: EthQuery,
36+
transactionMeta: TransactionMeta,
37+
) => Promise<Hex>;
38+
39+
readonly #getTransaction: (id: string) => TransactionMeta;
40+
41+
readonly #getEthQuery: (networkClientId: string) => EthQuery;
42+
43+
constructor({
44+
publishTransaction,
45+
getTransaction,
46+
getEthQuery,
47+
}: SequentialPublishBatchHookParams) {
48+
this.#publishTransaction = publishTransaction;
49+
this.#getTransaction = getTransaction;
50+
this.#getEthQuery = getEthQuery;
51+
}
52+
53+
/**
54+
* Get the hook function for sequential publishing.
55+
*
56+
* @returns The hook function.
57+
*/
58+
getHook() {
59+
return async ({
60+
from,
61+
networkClientId,
62+
transactions,
63+
}: {
64+
from: string;
65+
networkClientId: string;
66+
transactions: PublishBatchHookTransaction[];
67+
}) => {
68+
log('Starting sequential publish batch hook', { from, networkClientId });
69+
70+
const results = [];
71+
72+
for (const transaction of transactions) {
73+
try {
74+
const transactionMeta = this.#getTransaction(String(transaction.id));
75+
const transactionHash = await this.#publishTransaction(
76+
this.#getEthQuery(networkClientId),
77+
transactionMeta,
78+
);
79+
log('Transaction published', { transactionHash });
80+
81+
const isConfirmed = await this.#waitForTransactionConfirmation(
82+
transactionHash,
83+
networkClientId,
84+
);
85+
86+
if (!isConfirmed) {
87+
throw new Error(
88+
`Transaction ${transactionHash} failed or was not confirmed.`,
89+
);
90+
}
91+
92+
results.push({ transactionHash });
93+
} catch (error) {
94+
log('Transaction failed', { transaction, error });
95+
throw rpcErrors.internal(
96+
`Failed to publish sequential batch transaction`,
97+
);
98+
}
99+
}
100+
101+
log('Sequential publish batch hook completed', { results });
102+
103+
return { results };
104+
};
105+
}
106+
107+
async #waitForTransactionConfirmation(
108+
transactionHash: string,
109+
networkClientId: string,
110+
): Promise<boolean> {
111+
let attempts = 0;
112+
113+
while (attempts < MAX_TRANSACTION_CHECK_ATTEMPTS) {
114+
const isConfirmed = await this.#isTransactionConfirmed(
115+
transactionHash,
116+
networkClientId,
117+
);
118+
119+
if (isConfirmed) {
120+
return true;
121+
}
122+
123+
await new Promise((resolve) =>
124+
setTimeout(resolve, TRANSACTION_CHECK_INTERVAL),
125+
);
126+
127+
attempts += 1;
128+
}
129+
130+
return false;
131+
}
132+
133+
async #getTransactionReceipt(
134+
txHash: string,
135+
networkClientId: string,
136+
): Promise<TransactionReceipt | undefined> {
137+
return await query(
138+
this.#getEthQuery(networkClientId),
139+
'getTransactionReceipt',
140+
[txHash],
141+
);
142+
}
143+
144+
async #isTransactionConfirmed(
145+
transactionHash: string,
146+
networkClientId: string,
147+
): Promise<boolean> {
148+
try {
149+
const receipt = await this.#getTransactionReceipt(
150+
transactionHash,
151+
networkClientId,
152+
);
153+
const isSuccess = receipt?.status === RECEIPT_STATUS_SUCCESS;
154+
const isFailure = receipt?.status === RECEIPT_STATUS_FAILURE;
155+
156+
return isSuccess || isFailure;
157+
} catch (error) {
158+
log('Error checking transaction status', { transactionHash, error });
159+
return false;
160+
}
161+
}
162+
}

packages/transaction-controller/src/utils/batch.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
type TransactionParams,
4343
TransactionType,
4444
} from '../types';
45+
import { SequentialPublishBatchHook } from 'src/hooks/SequentialPublishBatchHook';
4546

4647
type AddTransactionBatchRequest = {
4748
addTransaction: TransactionController['addTransaction'];
@@ -57,6 +58,10 @@ type AddTransactionBatchRequest = {
5758
options: { transactionId: string },
5859
callback: (transactionMeta: TransactionMeta) => void,
5960
) => void;
61+
publishTransaction: (
62+
_ethQuery: EthQuery,
63+
transactionMeta: TransactionMeta,
64+
) => Promise<Hex>;
6065
};
6166

6267
type IsAtomicBatchSupportedRequestInternal = {
@@ -332,7 +337,8 @@ async function getNestedTransactionMeta(
332337
async function addTransactionBatchWithHook(
333338
request: AddTransactionBatchRequest,
334339
): Promise<TransactionBatchResult> {
335-
const { publishBatchHook, request: userRequest } = request;
340+
const { publishBatchHook: initialPublishBatchHook, request: userRequest } =
341+
request;
336342

337343
const {
338344
from,
@@ -342,10 +348,14 @@ async function addTransactionBatchWithHook(
342348

343349
log('Adding transaction batch using hook', userRequest);
344350

345-
if (!publishBatchHook) {
346-
log('No publish batch hook provided');
347-
throw new Error('No publish batch hook provided');
348-
}
351+
const sequentialPublishBatchHook = new SequentialPublishBatchHook({
352+
publishTransaction: request.publishTransaction,
353+
getTransaction: request.getTransaction,
354+
getEthQuery: request.getEthQuery,
355+
});
356+
357+
const publishBatchHook =
358+
initialPublishBatchHook ?? sequentialPublishBatchHook.getHook();
349359

350360
const batchId = generateBatchId();
351361
const transactionCount = nestedTransactions.length;

0 commit comments

Comments
 (0)