Skip to content

Commit 075e1a5

Browse files
authored
Confirm a transaction conditionally (#301)
1 parent 74b2555 commit 075e1a5

File tree

7 files changed

+895
-18
lines changed

7 files changed

+895
-18
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"test:watch": "jest --watchAll"
2626
},
2727
"dependencies": {
28+
"@babel/runtime": "^7.24.1",
2829
"@ethereumjs/tx": "^5.2.1",
2930
"@ethereumjs/util": "^9.0.2",
3031
"@ethersproject/bytes": "^5.7.0",
@@ -33,6 +34,7 @@
3334
"@metamask/eth-query": "^4.0.0",
3435
"@metamask/network-controller": "^17.2.0",
3536
"@metamask/polling-controller": "^5.0.0",
37+
"@metamask/transaction-controller": "^25.1.0",
3638
"bignumber.js": "^9.0.1",
3739
"events": "^3.3.0",
3840
"fast-json-patch": "^3.1.0",

src/SmartTransactionsController.test.ts

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { NetworkType, convertHexToDecimal } from '@metamask/controller-utils';
22
import type { NetworkState } from '@metamask/network-controller';
33
import { NetworkStatus } from '@metamask/network-controller';
4+
import {
5+
TransactionStatus,
6+
TransactionType,
7+
} from '@metamask/transaction-controller';
48
import nock from 'nock';
59
import * as sinon from 'sinon';
610

@@ -58,6 +62,8 @@ jest.mock('@metamask/eth-query', () => {
5862
});
5963

6064
const addressFrom = '0x268392a24B6b093127E8581eAfbD1DA228bAdAe3';
65+
const txHash =
66+
'0x0302b75dfb9fd9eb34056af031efcaee2a8cbd799ea054a85966165cd82a7356';
6167

6268
const createUnsignedTransaction = (chainId: number) => {
6369
return {
@@ -184,7 +190,7 @@ const createSignedTransaction = () => {
184190

185191
const createTxParams = () => {
186192
return {
187-
from: '0x268392a24B6b093127E8581eAfbD1DA228bAdAe3',
193+
from: addressFrom,
188194
to: '0x0000000000000000000000000000000000000000',
189195
value: 0,
190196
data: '0x',
@@ -268,6 +274,37 @@ const testHistory = [
268274
},
269275
];
270276

277+
const createTransactionMeta = (
278+
status: TransactionStatus = TransactionStatus.signed,
279+
) => {
280+
return {
281+
hash: txHash,
282+
status,
283+
id: '1',
284+
txParams: {
285+
from: addressFrom,
286+
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
287+
gasPrice: '0x77359400',
288+
gas: '0x7b0d',
289+
nonce: '0x4b',
290+
},
291+
type: TransactionType.simpleSend,
292+
chainId: CHAIN_IDS.ETHEREUM,
293+
time: 1624408066355,
294+
defaultGasEstimates: {
295+
gas: '0x7b0d',
296+
gasPrice: '0x77359400',
297+
},
298+
error: {
299+
name: 'Error',
300+
message: 'Details of the error',
301+
},
302+
securityProviderResponse: {
303+
flagAsDangerous: 0,
304+
},
305+
};
306+
};
307+
271308
const ethereumChainIdDec = parseInt(CHAIN_IDS.ETHEREUM, 16);
272309
const goerliChainIdDec = parseInt(CHAIN_IDS.GOERLI, 16);
273310

@@ -352,6 +389,7 @@ describe('SmartTransactionsController', () => {
352389
}),
353390
provider: { sendAsync: jest.fn() },
354391
confirmExternalTransaction: jest.fn(),
392+
getTransactions: jest.fn(),
355393
trackMetaMetricsEvent: trackMetaMetricsEventSpy,
356394
getNetworkClientById: jest.fn().mockImplementation((networkClientId) => {
357395
switch (networkClientId) {
@@ -843,6 +881,12 @@ describe('SmartTransactionsController', () => {
843881
...createStateAfterPending()[0],
844882
history: testHistory,
845883
};
884+
885+
jest
886+
.spyOn(smartTransactionsController, 'getRegularTransactions')
887+
.mockImplementation(() => {
888+
return [createTransactionMeta()];
889+
});
846890
smartTransactionsController.update({
847891
smartTransactionsState: {
848892
...smartTransactionsState,
@@ -853,6 +897,10 @@ describe('SmartTransactionsController', () => {
853897
});
854898
const updateTransaction = {
855899
...pendingStx,
900+
statusMetadata: {
901+
...pendingStx.statusMetadata,
902+
minedHash: txHash,
903+
},
856904
status: SmartTransactionStatuses.SUCCESS,
857905
};
858906

@@ -862,8 +910,108 @@ describe('SmartTransactionsController', () => {
862910
networkClientId: 'mainnet',
863911
},
864912
);
913+
await flushPromises();
914+
expect(
915+
smartTransactionsController.confirmExternalTransaction,
916+
).toHaveBeenCalledTimes(1);
917+
expect(
918+
smartTransactionsController.state.smartTransactionsState
919+
.smartTransactions[CHAIN_IDS.ETHEREUM],
920+
).toStrictEqual([
921+
{
922+
...updateTransaction,
923+
confirmed: true,
924+
},
925+
]);
926+
});
865927

928+
it('does not call the "confirmExternalTransaction" fn if a tx is already confirmed', async () => {
929+
const { smartTransactionsState } = smartTransactionsController.state;
930+
const pendingStx = {
931+
...createStateAfterPending()[0],
932+
history: testHistory,
933+
};
934+
jest
935+
.spyOn(smartTransactionsController, 'getRegularTransactions')
936+
.mockImplementation(() => {
937+
return [createTransactionMeta(TransactionStatus.confirmed)];
938+
});
939+
smartTransactionsController.update({
940+
smartTransactionsState: {
941+
...smartTransactionsState,
942+
smartTransactions: {
943+
[CHAIN_IDS.ETHEREUM]: [pendingStx] as SmartTransaction[],
944+
},
945+
},
946+
});
947+
const updateTransaction = {
948+
...pendingStx,
949+
status: SmartTransactionStatuses.SUCCESS,
950+
statusMetadata: {
951+
...pendingStx.statusMetadata,
952+
minedHash: txHash,
953+
},
954+
};
955+
956+
smartTransactionsController.updateSmartTransaction(
957+
updateTransaction as SmartTransaction,
958+
{
959+
networkClientId: 'mainnet',
960+
},
961+
);
962+
await flushPromises();
963+
expect(
964+
smartTransactionsController.confirmExternalTransaction,
965+
).not.toHaveBeenCalled();
966+
expect(
967+
smartTransactionsController.state.smartTransactionsState
968+
.smartTransactions[CHAIN_IDS.ETHEREUM],
969+
).toStrictEqual([
970+
{
971+
...updateTransaction,
972+
confirmed: true,
973+
},
974+
]);
975+
});
976+
977+
it('does not call the "confirmExternalTransaction" fn if a tx is already submitted', async () => {
978+
const { smartTransactionsState } = smartTransactionsController.state;
979+
const pendingStx = {
980+
...createStateAfterPending()[0],
981+
history: testHistory,
982+
};
983+
jest
984+
.spyOn(smartTransactionsController, 'getRegularTransactions')
985+
.mockImplementation(() => {
986+
return [createTransactionMeta(TransactionStatus.submitted)];
987+
});
988+
smartTransactionsController.update({
989+
smartTransactionsState: {
990+
...smartTransactionsState,
991+
smartTransactions: {
992+
[CHAIN_IDS.ETHEREUM]: [pendingStx] as SmartTransaction[],
993+
},
994+
},
995+
});
996+
const updateTransaction = {
997+
...pendingStx,
998+
status: SmartTransactionStatuses.SUCCESS,
999+
statusMetadata: {
1000+
...pendingStx.statusMetadata,
1001+
minedHash: txHash,
1002+
},
1003+
};
1004+
1005+
smartTransactionsController.updateSmartTransaction(
1006+
updateTransaction as SmartTransaction,
1007+
{
1008+
networkClientId: 'mainnet',
1009+
},
1010+
);
8661011
await flushPromises();
1012+
expect(
1013+
smartTransactionsController.confirmExternalTransaction,
1014+
).not.toHaveBeenCalled();
8671015
expect(
8681016
smartTransactionsController.state.smartTransactionsState
8691017
.smartTransactions[CHAIN_IDS.ETHEREUM],

src/SmartTransactionsController.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import type {
1010
NetworkState,
1111
} from '@metamask/network-controller';
1212
import { StaticIntervalPollingControllerV1 } from '@metamask/polling-controller';
13+
import type { TransactionMeta } from '@metamask/transaction-controller';
14+
import { TransactionStatus } from '@metamask/transaction-controller';
1315
import { BigNumber } from 'bignumber.js';
1416
// eslint-disable-next-line import/no-nodejs-modules
1517
import EventEmitter from 'events';
@@ -29,6 +31,7 @@ import type {
2931
SmartTransaction,
3032
SmartTransactionsStatus,
3133
UnsignedTransaction,
34+
GetTransactionsOptions,
3235
} from './types';
3336
import { APIType, SmartTransactionStatuses } from './types';
3437
import {
@@ -91,6 +94,10 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
9194

9295
public confirmExternalTransaction: any;
9396

97+
public getRegularTransactions: (
98+
options?: GetTransactionsOptions,
99+
) => TransactionMeta[];
100+
94101
private readonly trackMetaMetricsEvent: any;
95102

96103
public eventEmitter: EventEmitter;
@@ -117,6 +124,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
117124
getNonceLock,
118125
provider,
119126
confirmExternalTransaction,
127+
getTransactions,
120128
trackMetaMetricsEvent,
121129
getNetworkClientById,
122130
}: {
@@ -126,6 +134,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
126134
getNonceLock: any;
127135
provider: Provider;
128136
confirmExternalTransaction: any;
137+
getTransactions: (options?: GetTransactionsOptions) => TransactionMeta[];
129138
trackMetaMetricsEvent: any;
130139
getNetworkClientById: NetworkController['getNetworkClientById'];
131140
},
@@ -173,6 +182,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
173182
this.getNonceLock = getNonceLock;
174183
this.ethQuery = undefined;
175184
this.confirmExternalTransaction = confirmExternalTransaction;
185+
this.getRegularTransactions = getTransactions;
176186
this.trackMetaMetricsEvent = trackMetaMetricsEvent;
177187
this.getNetworkClientById = getNetworkClientById;
178188

@@ -447,6 +457,31 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
447457
}
448458
}
449459

460+
#doesTransactionNeedConfirmation(
461+
smartTransaction: SmartTransaction,
462+
): boolean {
463+
const smartTransactionMinedTxHash =
464+
smartTransaction.statusMetadata?.minedHash;
465+
if (!smartTransactionMinedTxHash) {
466+
return false;
467+
}
468+
const transactions = this.getRegularTransactions();
469+
const foundTransaction = transactions?.find((tx) => {
470+
return (
471+
tx.hash?.toLowerCase() === smartTransactionMinedTxHash.toLowerCase()
472+
);
473+
});
474+
if (!foundTransaction) {
475+
return false;
476+
}
477+
// If a found transaction is either confirmed or submitted, it doesn't need confirmation from the STX controller.
478+
// When it's in the submitted state, the TransactionController checks its status and confirms it,
479+
// so no need to confirm it again here.
480+
return ![TransactionStatus.confirmed, TransactionStatus.submitted].includes(
481+
foundTransaction.status,
482+
);
483+
}
484+
450485
async #confirmSmartTransaction(
451486
smartTransaction: SmartTransaction,
452487
{
@@ -490,7 +525,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
490525
const originalTxMeta = {
491526
...smartTransaction,
492527
id: smartTransaction.uuid,
493-
status: 'confirmed',
528+
status: TransactionStatus.confirmed,
494529
hash: txHash,
495530
txParams: updatedTxParams,
496531
};
@@ -512,11 +547,13 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
512547
}
513548
: originalTxMeta;
514549

515-
this.confirmExternalTransaction(
516-
txMeta,
517-
transactionReceipt,
518-
baseFeePerGas,
519-
);
550+
if (this.#doesTransactionNeedConfirmation(smartTransaction)) {
551+
this.confirmExternalTransaction(
552+
txMeta,
553+
transactionReceipt,
554+
baseFeePerGas,
555+
);
556+
}
520557

521558
this.trackMetaMetricsEvent({
522559
event: MetaMetricsEventName.StxConfirmed,

src/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('default export', () => {
99
getNonceLock: null,
1010
provider: { sendAsync: jest.fn() },
1111
confirmExternalTransaction: jest.fn(),
12+
getTransactions: jest.fn(),
1213
trackMetaMetricsEvent: jest.fn(),
1314
getNetworkClientById: jest.fn(),
1415
});

src/types.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
/** API */
1+
import type { TransactionMeta } from '@metamask/transaction-controller';
22

3+
/** API */
34
export enum APIType {
45
'GET_FEES',
56
'ESTIMATE_GAS',
@@ -10,7 +11,6 @@ export enum APIType {
1011
}
1112

1213
/** SmartTransactions */
13-
1414
export enum SmartTransactionMinedTx {
1515
NOT_MINED = 'not_mined',
1616
SUCCESS = 'success',
@@ -64,7 +64,7 @@ export type SmartTransactionsStatus = {
6464
cancellationFeeWei: number;
6565
cancellationReason?: SmartTransactionCancellationReason;
6666
deadlineRatio: number;
67-
minedHash: string | undefined;
67+
minedHash: string;
6868
minedTx: SmartTransactionMinedTx;
6969
isSettled: boolean;
7070
};
@@ -121,3 +121,10 @@ export type SignedTransaction = any;
121121
export type SignedCanceledTransaction = any;
122122

123123
export type Hex = `0x${string}`;
124+
125+
export type GetTransactionsOptions = {
126+
searchCriteria?: any;
127+
initialList?: TransactionMeta[];
128+
filterToCurrentNetwork?: boolean;
129+
limit?: number;
130+
};

src/utils.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('src/utils.js', () => {
2727
minedTx: SmartTransactionMinedTx.NOT_MINED,
2828
cancellationFeeWei: 10000,
2929
deadlineRatio: 10,
30-
minedHash: undefined,
30+
minedHash: '',
3131
isSettled: true,
3232
},
3333
};
@@ -215,7 +215,7 @@ describe('src/utils.js', () => {
215215
cancellationFeeWei: 10000,
216216
cancellationReason: SmartTransactionCancellationReason.NOT_CANCELLED,
217217
deadlineRatio: 10,
218-
minedHash: undefined,
218+
minedHash: '',
219219
isSettled: true,
220220
...customProps,
221221
};

0 commit comments

Comments
 (0)