Skip to content

Commit ac4c0bd

Browse files
committed
refactor(transaction-controller): address PR review feedback
Remove instant option from addTransaction in favour of dedicated startTransaction method. Extract validation into reusable functions, move pipeline types to lifecycle/types.ts, deduplicate shared setup into createTransaction, alphabetise TransactionContext, and remove premature ready check in approveTransaction.
1 parent 87a9b18 commit ac4c0bd

8 files changed

Lines changed: 331 additions & 381 deletions

File tree

packages/transaction-controller/CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- Add `instant` option to `addTransaction` that adds the transaction to state immediately with default values, deferring gas estimation, fee calculation, and type resolution to the background ([#XXXX](https://github.com/MetaMask/core/pull/XXXX))
13-
- Add `ready` property to `TransactionMeta` that indicates whether essential async data has been resolved for instant transactions ([#XXXX](https://github.com/MetaMask/core/pull/XXXX))
14-
- Add safety guards that prevent approving, speeding up, or cancelling transactions when `ready` is `false` ([#XXXX](https://github.com/MetaMask/core/pull/XXXX))
12+
- Add `startTransaction` method for synchronous transaction creation with deferred async data resolution ([#8248](https://github.com/MetaMask/core/pull/8248))
13+
- Transaction is immediately available in state with `ready: false`
14+
- Gas estimation, fee calculation, and type resolution run in the background
15+
- `ready` flips to `true` once essential data resolves
1516

1617
## [62.22.0]
1718

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

Lines changed: 36 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3499,7 +3499,7 @@ describe('TransactionController', () => {
34993499
});
35003500

35013501
describe('ready gate', () => {
3502-
it('resolves data before approval when using instant via addTransaction', async () => {
3502+
it('resolves data before approval when using startTransaction', async () => {
35033503
const { controller } = setupController({
35043504
messengerOptions: {
35053505
addTransactionApprovalRequest: {
@@ -3508,7 +3508,7 @@ describe('TransactionController', () => {
35083508
},
35093509
});
35103510

3511-
const { result } = await controller.addTransaction(
3511+
const { result } = controller.startTransaction(
35123512
{
35133513
from: ACCOUNT_MOCK,
35143514
gas: '0x0',
@@ -3518,7 +3518,6 @@ describe('TransactionController', () => {
35183518
},
35193519
{
35203520
networkClientId: NETWORK_CLIENT_ID_MOCK,
3521-
instant: true,
35223521
},
35233522
);
35243523

@@ -3920,36 +3919,34 @@ describe('TransactionController', () => {
39203919
});
39213920
});
39223921

3923-
describe('when instant option is true', () => {
3924-
it('returns immediately with ready false', async () => {
3922+
describe('startTransaction', () => {
3923+
it('returns immediately with ready false', () => {
39253924
const { controller } = setupController();
39263925

3927-
const { transactionMeta } = await controller.addTransaction(
3926+
const { transactionMeta } = controller.startTransaction(
39283927
{
39293928
from: ACCOUNT_MOCK,
39303929
to: ACCOUNT_MOCK,
39313930
},
39323931
{
39333932
networkClientId: NETWORK_CLIENT_ID_MOCK,
3934-
instant: true,
39353933
},
39363934
);
39373935

39383936
expect(transactionMeta.ready).toBe(false);
39393937
expect(transactionMeta.status).toBe(TransactionStatus.unapproved);
39403938
});
39413939

3942-
it('adds transaction to state immediately', async () => {
3940+
it('adds transaction to state immediately', () => {
39433941
const { controller } = setupController();
39443942

3945-
const { transactionMeta } = await controller.addTransaction(
3943+
const { transactionMeta } = controller.startTransaction(
39463944
{
39473945
from: ACCOUNT_MOCK,
39483946
to: ACCOUNT_MOCK,
39493947
},
39503948
{
39513949
networkClientId: NETWORK_CLIENT_ID_MOCK,
3952-
instant: true,
39533950
},
39543951
);
39553952

@@ -3964,14 +3961,13 @@ describe('TransactionController', () => {
39643961
it('sets ready to true after background resolution', async () => {
39653962
const { controller } = setupController();
39663963

3967-
const { transactionMeta } = await controller.addTransaction(
3964+
const { transactionMeta } = controller.startTransaction(
39683965
{
39693966
from: ACCOUNT_MOCK,
39703967
to: ACCOUNT_MOCK,
39713968
},
39723969
{
39733970
networkClientId: NETWORK_CLIENT_ID_MOCK,
3974-
instant: true,
39753971
},
39763972
);
39773973

@@ -3984,47 +3980,45 @@ describe('TransactionController', () => {
39843980
expect(txInState?.ready).toBe(true);
39853981
});
39863982

3987-
it('throws if instant is true with an external origin', async () => {
3983+
it('throws with an external origin', () => {
39883984
const { controller } = setupController();
39893985

3990-
await expect(
3991-
controller.addTransaction(
3986+
expect(() =>
3987+
controller.startTransaction(
39923988
{
39933989
from: ACCOUNT_MOCK,
39943990
to: ACCOUNT_MOCK,
39953991
},
39963992
{
39973993
networkClientId: NETWORK_CLIENT_ID_MOCK,
3998-
instant: true,
39993994
origin: 'https://metamask.github.io/test-dapp/',
40003995
},
40013996
),
4002-
).rejects.toThrow(
4003-
'The instant option is not supported for external transactions.',
3997+
).toThrow(
3998+
'startTransaction is not supported for external transactions.',
40043999
);
40054000
});
40064001

4007-
it('does not throw if instant is true with ORIGIN_METAMASK', async () => {
4002+
it('does not throw with ORIGIN_METAMASK', () => {
40084003
const { controller } = setupController();
40094004

4010-
const addResult = await controller.addTransaction(
4005+
const startResult = controller.startTransaction(
40114006
{
40124007
from: ACCOUNT_MOCK,
40134008
to: ACCOUNT_MOCK,
40144009
},
40154010
{
40164011
networkClientId: NETWORK_CLIENT_ID_MOCK,
4017-
instant: true,
40184012
origin: ORIGIN_METAMASK,
40194013
},
40204014
);
40214015

4022-
expect(addResult).toMatchObject({
4016+
expect(startResult).toMatchObject({
40234017
transactionMeta: expect.objectContaining({ ready: false }),
40244018
});
40254019
});
40264020

4027-
it('publishes unapprovedTransactionAdded event immediately', async () => {
4021+
it('publishes unapprovedTransactionAdded event immediately', () => {
40284022
const { controller, messenger } = setupController();
40294023
const listener = jest.fn();
40304024

@@ -4033,22 +4027,21 @@ describe('TransactionController', () => {
40334027
listener,
40344028
);
40354029

4036-
await controller.addTransaction(
4030+
controller.startTransaction(
40374031
{
40384032
from: ACCOUNT_MOCK,
40394033
to: ACCOUNT_MOCK,
40404034
},
40414035
{
40424036
networkClientId: NETWORK_CLIENT_ID_MOCK,
4043-
instant: true,
40444037
},
40454038
);
40464039

40474040
expect(listener).toHaveBeenCalledTimes(1);
40484041
expect(listener.mock.calls[0][0]).toMatchObject({ ready: false });
40494042
});
40504043

4051-
it('does not set ready when instant is not true', async () => {
4044+
it('does not set ready when using addTransaction', async () => {
40524045
const { controller } = setupController();
40534046

40544047
const { transactionMeta } = await controller.addTransaction(
@@ -4064,42 +4057,38 @@ describe('TransactionController', () => {
40644057
expect(transactionMeta.ready).toBeUndefined();
40654058
});
40664059

4067-
it('uses caller-provided type without waiting for background', async () => {
4060+
it('uses caller-provided type without waiting for background', () => {
40684061
const { controller } = setupController();
40694062

4070-
const { transactionMeta } = await controller.addTransaction(
4063+
const { transactionMeta } = controller.startTransaction(
40714064
{
40724065
from: ACCOUNT_MOCK,
40734066
to: ACCOUNT_MOCK,
40744067
},
40754068
{
40764069
networkClientId: NETWORK_CLIENT_ID_MOCK,
4077-
instant: true,
40784070
type: TransactionType.simpleSend,
40794071
},
40804072
);
40814073

40824074
expect(transactionMeta.type).toBe(TransactionType.simpleSend);
40834075
});
40844076

4085-
it('works with requireApproval false', async () => {
4077+
it('works with requireApproval false', () => {
40864078
const { controller } = setupController();
40874079

4088-
const { transactionMeta, result } = await controller.addTransaction(
4080+
const { transactionMeta, result } = controller.startTransaction(
40894081
{
40904082
from: ACCOUNT_MOCK,
40914083
to: ACCOUNT_MOCK,
40924084
},
40934085
{
40944086
networkClientId: NETWORK_CLIENT_ID_MOCK,
4095-
instant: true,
40964087
requireApproval: false,
40974088
},
40984089
);
40994090

4100-
result.catch(() => {
4101-
// Expected: ready gate rejects because ready is false
4102-
});
4091+
result.catch(() => undefined);
41034092

41044093
expect(transactionMeta.ready).toBe(false);
41054094
expect(
@@ -4109,28 +4098,28 @@ describe('TransactionController', () => {
41094098
).toBe(true);
41104099
});
41114100

4112-
it('adds multiple instant transactions in quick succession', async () => {
4101+
it('adds multiple transactions in quick succession', () => {
41134102
uuidModuleMock.v1
41144103
.mockImplementationOnce(() => 'aaa-instant-1')
41154104
.mockImplementationOnce(() => 'bbb-instant-2')
41164105
.mockImplementationOnce(() => 'ccc-instant-3');
41174106

41184107
const { controller } = setupController();
41194108

4120-
const results = await Promise.all([
4121-
controller.addTransaction(
4109+
const results = [
4110+
controller.startTransaction(
41224111
{ from: ACCOUNT_MOCK, to: ACCOUNT_MOCK },
4123-
{ networkClientId: NETWORK_CLIENT_ID_MOCK, instant: true },
4112+
{ networkClientId: NETWORK_CLIENT_ID_MOCK },
41244113
),
4125-
controller.addTransaction(
4114+
controller.startTransaction(
41264115
{ from: ACCOUNT_MOCK, to: ACCOUNT_MOCK },
4127-
{ networkClientId: NETWORK_CLIENT_ID_MOCK, instant: true },
4116+
{ networkClientId: NETWORK_CLIENT_ID_MOCK },
41284117
),
4129-
controller.addTransaction(
4118+
controller.startTransaction(
41304119
{ from: ACCOUNT_MOCK, to: ACCOUNT_MOCK },
4131-
{ networkClientId: NETWORK_CLIENT_ID_MOCK, instant: true },
4120+
{ networkClientId: NETWORK_CLIENT_ID_MOCK },
41324121
),
4133-
]);
4122+
];
41344123

41354124
const ids = results.map((res) => res.transactionMeta.id);
41364125

@@ -4147,14 +4136,13 @@ describe('TransactionController', () => {
41474136

41484137
const { controller } = setupController();
41494138

4150-
const { transactionMeta, result } = await controller.addTransaction(
4139+
const { transactionMeta, result } = controller.startTransaction(
41514140
{
41524141
from: ACCOUNT_MOCK,
41534142
to: ACCOUNT_MOCK,
41544143
},
41554144
{
41564145
networkClientId: NETWORK_CLIENT_ID_MOCK,
4157-
instant: true,
41584146
},
41594147
);
41604148

@@ -4654,14 +4642,13 @@ describe('TransactionController', () => {
46544642
it('throws when transaction has ready false', async () => {
46554643
const { controller } = setupController();
46564644

4657-
const { transactionMeta } = await controller.addTransaction(
4645+
const { transactionMeta } = controller.startTransaction(
46584646
{
46594647
from: ACCOUNT_MOCK,
46604648
to: ACCOUNT_MOCK,
46614649
},
46624650
{
46634651
networkClientId: NETWORK_CLIENT_ID_MOCK,
4664-
instant: true,
46654652
},
46664653
);
46674654

@@ -5132,14 +5119,13 @@ describe('TransactionController', () => {
51325119
it('throws when transaction has ready false', async () => {
51335120
const { controller } = setupController();
51345121

5135-
const { transactionMeta } = await controller.addTransaction(
5122+
const { transactionMeta } = controller.startTransaction(
51365123
{
51375124
from: ACCOUNT_MOCK,
51385125
to: ACCOUNT_MOCK,
51395126
},
51405127
{
51415128
networkClientId: NETWORK_CLIENT_ID_MOCK,
5142-
instant: true,
51435129
},
51445130
);
51455131

packages/transaction-controller/src/TransactionController.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ import {
8989
startTransaction as startTransactionPipeline,
9090
addTransaction as addTransactionPipeline,
9191
} from './lifecycle/pipeline';
92+
import type {
93+
StartTransactionResult,
94+
TransactionContext,
95+
} from './lifecycle/types';
9296
import { projectLogger as log } from './logger';
9397
import type {
9498
DappSuggestedGasFees,
@@ -127,8 +131,6 @@ import type {
127131
PublishHookResult,
128132
GetGasFeeTokensRequest,
129133
InternalAccount,
130-
StartTransactionResult,
131-
TransactionContext,
132134
} from './types';
133135
import {
134136
GasFeeEstimateLevel,
@@ -3053,16 +3055,6 @@ export class TransactionController extends BaseController<
30533055

30543056
log('Approving transaction', transactionMeta);
30553057

3056-
if (transactionMeta.ready === false) {
3057-
this.#failTransaction(
3058-
transactionMeta,
3059-
new Error(
3060-
'Transaction is not ready. Essential async data has not resolved yet.',
3061-
),
3062-
);
3063-
return ApprovalState.NotApproved;
3064-
}
3065-
30663058
try {
30673059
if (!this.#sign) {
30683060
this.#failTransaction(

packages/transaction-controller/src/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,6 @@ export type {
9595
TransactionMeta,
9696
TransactionParams,
9797
TransactionReceipt,
98-
PipelineCallbacks,
99-
StartTransactionResult,
100-
TransactionContext,
101-
TransactionStage,
10298
ValidateSecurityRequest,
10399
} from './types';
104100
export {
@@ -124,3 +120,9 @@ export { SUPPORTED_CHAIN_IDS as INCOMING_TRANSACTIONS_SUPPORTED_CHAIN_IDS } from
124120
export { HARDFORK } from './utils/prepare';
125121
export { addTransaction, startTransaction } from './lifecycle/pipeline';
126122
export { data } from './lifecycle/stages/data';
123+
export type {
124+
PipelineCallbacks,
125+
StartTransactionResult,
126+
TransactionContext,
127+
TransactionStage,
128+
} from './lifecycle/types';

0 commit comments

Comments
 (0)