Skip to content

Commit 9852a3c

Browse files
feat: Add afterAdd hook to transaction controller (#5692)
## Explanation Add optional `afterAdd` hook to mutate transactions added via `addTransaction` method. Persist original transaction params in new `txParamsOriginal` property. ## References Fixes [#4688](MetaMask/MetaMask-planning#4688) ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
1 parent ccfd470 commit 9852a3c

File tree

5 files changed

+135
-0
lines changed

5 files changed

+135
-0
lines changed

packages/transaction-controller/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add optional `afterAdd` hook to constructor ([#5692](https://github.com/MetaMask/core/pull/5692))
13+
- Add optional `txParamsOriginal` property to `TransactionMeta`.
14+
- Add `AfterAddHook` type.
15+
1016
## [54.1.0]
1117

1218
### Changed

packages/transaction-controller/src/TransactionController.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2072,6 +2072,98 @@ describe('TransactionController', () => {
20722072
);
20732073
});
20742074

2075+
describe('with afterAdd hook', () => {
2076+
it('calls afterAdd hook', async () => {
2077+
const afterAddHook = jest.fn().mockResolvedValueOnce({});
2078+
2079+
const { controller } = setupController({
2080+
options: {
2081+
hooks: {
2082+
afterAdd: afterAddHook,
2083+
},
2084+
},
2085+
});
2086+
2087+
await controller.addTransaction(
2088+
{
2089+
from: ACCOUNT_MOCK,
2090+
to: ACCOUNT_MOCK,
2091+
},
2092+
{
2093+
networkClientId: NETWORK_CLIENT_ID_MOCK,
2094+
},
2095+
);
2096+
2097+
expect(afterAddHook).toHaveBeenCalledTimes(1);
2098+
});
2099+
2100+
it('updates transaction if update callback returned', async () => {
2101+
const updateTransactionMock = jest.fn();
2102+
2103+
const afterAddHook = jest
2104+
.fn()
2105+
.mockResolvedValueOnce({ updateTransaction: updateTransactionMock });
2106+
2107+
const { controller } = setupController({
2108+
options: {
2109+
hooks: {
2110+
afterAdd: afterAddHook,
2111+
},
2112+
},
2113+
});
2114+
2115+
await controller.addTransaction(
2116+
{
2117+
from: ACCOUNT_MOCK,
2118+
to: ACCOUNT_MOCK,
2119+
},
2120+
{
2121+
networkClientId: NETWORK_CLIENT_ID_MOCK,
2122+
},
2123+
);
2124+
2125+
expect(updateTransactionMock).toHaveBeenCalledTimes(1);
2126+
expect(updateTransactionMock).toHaveBeenCalledWith(
2127+
expect.objectContaining({
2128+
id: expect.any(String),
2129+
}),
2130+
);
2131+
});
2132+
2133+
it('saves original transaction params if update callback returned', async () => {
2134+
const updateTransactionMock = jest.fn();
2135+
2136+
const afterAddHook = jest
2137+
.fn()
2138+
.mockResolvedValueOnce({ updateTransaction: updateTransactionMock });
2139+
2140+
const { controller } = setupController({
2141+
options: {
2142+
hooks: {
2143+
afterAdd: afterAddHook,
2144+
},
2145+
},
2146+
});
2147+
2148+
await controller.addTransaction(
2149+
{
2150+
from: ACCOUNT_MOCK,
2151+
to: ACCOUNT_MOCK,
2152+
},
2153+
{
2154+
networkClientId: NETWORK_CLIENT_ID_MOCK,
2155+
},
2156+
);
2157+
2158+
expect(controller.state.transactions[0].txParamsOriginal).toStrictEqual(
2159+
expect.objectContaining({
2160+
from: ACCOUNT_MOCK,
2161+
to: ACCOUNT_MOCK,
2162+
}),
2163+
);
2164+
});
2165+
});
2166+
20752167
describe('updates simulation data', () => {
20762168
it('by default', async () => {
20772169
getSimulationDataMock.mockResolvedValueOnce(

packages/transaction-controller/src/TransactionController.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ import type {
110110
GasFeeToken,
111111
IsAtomicBatchSupportedResult,
112112
IsAtomicBatchSupportedRequest,
113+
AfterAddHook,
113114
} from './types';
114115
import {
115116
TransactionEnvelopeType,
@@ -377,6 +378,9 @@ export type TransactionControllerOptions = {
377378

378379
/** The controller hooks. */
379380
hooks: {
381+
/** Additional logic to execute after adding a transaction. */
382+
afterAdd?: AfterAddHook;
383+
380384
/** Additional logic to execute after signing a transaction. Return false to not change the status to signed. */
381385
afterSign?: (
382386
transactionMeta: TransactionMeta,
@@ -662,6 +666,8 @@ export class TransactionController extends BaseController<
662666
TransactionControllerState,
663667
TransactionControllerMessenger
664668
> {
669+
readonly #afterAdd: AfterAddHook;
670+
665671
readonly #internalEvents = new EventEmitter();
666672

667673
private readonly isHistoryDisabled: boolean;
@@ -891,6 +897,7 @@ export class TransactionController extends BaseController<
891897
this.#testGasFeeFlows = testGasFeeFlows === true;
892898
this.#trace = trace ?? (((_request, fn) => fn?.()) as TraceCallback);
893899

900+
this.#afterAdd = hooks?.afterAdd ?? (() => Promise.resolve({}));
894901
this.afterSign = hooks?.afterSign ?? (() => true);
895902
this.beforeCheckPendingTransaction =
896903
/* istanbul ignore next */
@@ -1247,6 +1254,20 @@ export class TransactionController extends BaseController<
12471254
verifiedOnBlockchain: false,
12481255
};
12491256

1257+
const { updateTransaction } = await this.#afterAdd({
1258+
transactionMeta: addedTransactionMeta,
1259+
});
1260+
1261+
if (updateTransaction) {
1262+
log('Updating transaction using afterAdd hook');
1263+
1264+
addedTransactionMeta.txParamsOriginal = cloneDeep(
1265+
addedTransactionMeta.txParams,
1266+
);
1267+
1268+
updateTransaction(addedTransactionMeta);
1269+
}
1270+
12501271
await this.#trace(
12511272
{ name: 'Estimate Gas Properties', parentContext: traceContext },
12521273
(context) =>

packages/transaction-controller/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export {
3131
TransactionController,
3232
} from './TransactionController';
3333
export type {
34+
AfterAddHook,
3435
Authorization,
3536
AuthorizationList,
3637
BatchTransactionParams,

packages/transaction-controller/src/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,11 @@ export type TransactionMeta = {
446446
*/
447447
txParams: TransactionParams;
448448

449+
/**
450+
* Initial transaction parameters before `afterAdd` hook was invoked.
451+
*/
452+
txParamsOriginal?: TransactionParams;
453+
449454
/**
450455
* Transaction receipt.
451456
*/
@@ -1756,3 +1761,13 @@ export type IsAtomicBatchSupportedResultEntry = {
17561761
/** Address of the contract that the account would be upgraded to. */
17571762
upgradeContractAddress?: Hex;
17581763
};
1764+
1765+
/**
1766+
* Custom logic to be executed after a transaction is added.
1767+
* Can optionally update the transaction by returning the `updateTransaction` callback.
1768+
*/
1769+
export type AfterAddHook = (request: {
1770+
transactionMeta: TransactionMeta;
1771+
}) => Promise<{
1772+
updateTransaction?: (transaction: TransactionMeta) => void;
1773+
}>;

0 commit comments

Comments
 (0)