Skip to content

Commit e1605d9

Browse files
authored
feat: add support for returning a txHash asap (#467)
1 parent 42e161c commit e1605d9

6 files changed

+281
-14
lines changed

src/SmartTransactionsController.test.ts

+169-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import type {
3131
SmartTransactionsControllerEvents,
3232
} from './SmartTransactionsController';
3333
import type { SmartTransaction, UnsignedTransaction, Hex } from './types';
34-
import { SmartTransactionStatuses } from './types';
34+
import { SmartTransactionStatuses, ClientId } from './types';
3535
import * as utils from './utils';
3636

3737
jest.mock('@ethersproject/bytes', () => ({
@@ -1214,6 +1214,170 @@ describe('SmartTransactionsController', () => {
12141214
},
12151215
);
12161216
});
1217+
1218+
it('calls updateTransaction when smart transaction is cancelled and returnTxHashAsap is true', async () => {
1219+
const mockUpdateTransaction = jest.fn();
1220+
const defaultState = getDefaultSmartTransactionsControllerState();
1221+
const pendingStx = createStateAfterPending();
1222+
await withController(
1223+
{
1224+
options: {
1225+
updateTransaction: mockUpdateTransaction,
1226+
getFeatureFlags: () => ({
1227+
smartTransactions: {
1228+
mobileReturnTxHashAsap: true,
1229+
},
1230+
}),
1231+
getTransactions: () => [
1232+
{
1233+
id: 'test-tx-id',
1234+
status: TransactionStatus.submitted,
1235+
chainId: '0x1',
1236+
time: 123,
1237+
txParams: {
1238+
from: '0x123',
1239+
},
1240+
},
1241+
],
1242+
state: {
1243+
smartTransactionsState: {
1244+
...defaultState.smartTransactionsState,
1245+
smartTransactions: {
1246+
[ChainId.mainnet]: pendingStx as SmartTransaction[],
1247+
},
1248+
},
1249+
},
1250+
},
1251+
},
1252+
async ({ controller }) => {
1253+
const smartTransaction = {
1254+
uuid: 'uuid1',
1255+
status: SmartTransactionStatuses.CANCELLED,
1256+
transactionId: 'test-tx-id',
1257+
};
1258+
1259+
controller.updateSmartTransaction(smartTransaction);
1260+
1261+
expect(mockUpdateTransaction).toHaveBeenCalledWith(
1262+
{
1263+
id: 'test-tx-id',
1264+
status: TransactionStatus.failed,
1265+
chainId: '0x1',
1266+
time: 123,
1267+
txParams: {
1268+
from: '0x123',
1269+
},
1270+
},
1271+
'Smart transaction cancelled',
1272+
);
1273+
},
1274+
);
1275+
});
1276+
1277+
it('does not call updateTransaction when smart transaction is cancelled but returnTxHashAsap is false', async () => {
1278+
const mockUpdateTransaction = jest.fn();
1279+
await withController(
1280+
{
1281+
options: {
1282+
updateTransaction: mockUpdateTransaction,
1283+
getFeatureFlags: () => ({
1284+
smartTransactions: {
1285+
mobileReturnTxHashAsap: false,
1286+
},
1287+
}),
1288+
getTransactions: () => [
1289+
{
1290+
id: 'test-tx-id',
1291+
status: TransactionStatus.submitted,
1292+
chainId: '0x1',
1293+
time: 123,
1294+
txParams: {
1295+
from: '0x123',
1296+
},
1297+
},
1298+
],
1299+
},
1300+
},
1301+
async ({ controller }) => {
1302+
const smartTransaction = {
1303+
uuid: 'test-uuid',
1304+
status: SmartTransactionStatuses.CANCELLED,
1305+
transactionId: 'test-tx-id',
1306+
};
1307+
1308+
controller.updateSmartTransaction(smartTransaction);
1309+
1310+
expect(mockUpdateTransaction).not.toHaveBeenCalled();
1311+
},
1312+
);
1313+
});
1314+
1315+
it('does not call updateTransaction when transaction is not found in regular transactions', async () => {
1316+
const mockUpdateTransaction = jest.fn();
1317+
1318+
await withController(
1319+
{
1320+
options: {
1321+
updateTransaction: mockUpdateTransaction,
1322+
getFeatureFlags: () => ({
1323+
smartTransactions: {
1324+
mobileReturnTxHashAsap: true,
1325+
},
1326+
}),
1327+
getTransactions: () => [],
1328+
},
1329+
},
1330+
async ({ controller }) => {
1331+
const smartTransaction = {
1332+
uuid: 'test-uuid',
1333+
status: SmartTransactionStatuses.CANCELLED,
1334+
transactionId: 'test-tx-id',
1335+
};
1336+
1337+
controller.updateSmartTransaction(smartTransaction);
1338+
1339+
expect(mockUpdateTransaction).not.toHaveBeenCalled();
1340+
},
1341+
);
1342+
});
1343+
1344+
it('does not call updateTransaction for non-cancelled transactions', async () => {
1345+
const mockUpdateTransaction = jest.fn();
1346+
await withController(
1347+
{
1348+
options: {
1349+
updateTransaction: mockUpdateTransaction,
1350+
getFeatureFlags: () => ({
1351+
smartTransactions: {
1352+
mobileReturnTxHashAsap: true,
1353+
},
1354+
}),
1355+
getTransactions: () => [
1356+
{
1357+
id: 'test-tx-id',
1358+
status: TransactionStatus.submitted,
1359+
chainId: '0x1',
1360+
time: 123,
1361+
txParams: {
1362+
from: '0x123',
1363+
},
1364+
},
1365+
],
1366+
},
1367+
},
1368+
async ({ controller }) => {
1369+
const smartTransaction = {
1370+
uuid: 'test-uuid',
1371+
status: SmartTransactionStatuses.PENDING,
1372+
transactionId: 'test-tx-id',
1373+
};
1374+
1375+
controller.updateSmartTransaction(smartTransaction);
1376+
1377+
expect(mockUpdateTransaction).not.toHaveBeenCalled();
1378+
},
1379+
);
1380+
});
12171381
});
12181382

12191383
describe('cancelSmartTransaction', () => {
@@ -1438,7 +1602,7 @@ describe('SmartTransactionsController', () => {
14381602
const fetchHeaders = {
14391603
headers: {
14401604
'Content-Type': 'application/json',
1441-
'X-Client-Id': 'default',
1605+
'X-Client-Id': ClientId.Mobile,
14421606
},
14431607
};
14441608

@@ -1813,6 +1977,7 @@ async function withController<ReturnValue>(
18131977

18141978
const controller = new SmartTransactionsController({
18151979
messenger,
1980+
clientId: ClientId.Mobile,
18161981
getNonceLock: jest.fn().mockResolvedValue({
18171982
nextNonce: 'nextNonce',
18181983
releaseLock: jest.fn(),
@@ -1827,6 +1992,8 @@ async function withController<ReturnValue>(
18271992
deviceModel: 'ledger',
18281993
});
18291994
}),
1995+
getFeatureFlags: jest.fn(),
1996+
updateTransaction: jest.fn(),
18301997
...options,
18311998
});
18321999

src/SmartTransactionsController.ts

+47-11
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import type {
3838
UnsignedTransaction,
3939
GetTransactionsOptions,
4040
MetaMetricsProps,
41+
FeatureFlags,
42+
ClientId,
4143
} from './types';
4244
import { APIType, SmartTransactionStatuses } from './types';
4345
import {
@@ -53,11 +55,11 @@ import {
5355
getTxHash,
5456
getSmartTransactionMetricsProperties,
5557
getSmartTransactionMetricsSensitiveProperties,
58+
getReturnTxHashAsap,
5659
} from './utils';
5760

5861
const SECOND = 1000;
5962
export const DEFAULT_INTERVAL = SECOND * 5;
60-
const DEFAULT_CLIENT_ID = 'default';
6163
const ETH_QUERY_ERROR_MSG =
6264
'`ethQuery` is not defined on SmartTransactionsController';
6365

@@ -178,7 +180,7 @@ export type SmartTransactionsControllerMessenger =
178180

179181
type SmartTransactionsControllerOptions = {
180182
interval?: number;
181-
clientId?: string;
183+
clientId: ClientId;
182184
chainId?: Hex;
183185
supportedChainIds?: Hex[];
184186
getNonceLock: TransactionController['getNonceLock'];
@@ -198,6 +200,8 @@ type SmartTransactionsControllerOptions = {
198200
messenger: SmartTransactionsControllerMessenger;
199201
getTransactions: (options?: GetTransactionsOptions) => TransactionMeta[];
200202
getMetaMetricsProps: () => Promise<MetaMetricsProps>;
203+
getFeatureFlags: () => FeatureFlags;
204+
updateTransaction: (transaction: TransactionMeta, note: string) => void;
201205
};
202206

203207
export type SmartTransactionsControllerPollingInput = {
@@ -211,7 +215,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
211215
> {
212216
#interval: number;
213217

214-
#clientId: string;
218+
#clientId: ClientId;
215219

216220
#chainId: Hex;
217221

@@ -233,6 +237,10 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
233237

234238
readonly #getMetaMetricsProps: () => Promise<MetaMetricsProps>;
235239

240+
#getFeatureFlags: SmartTransactionsControllerOptions['getFeatureFlags'];
241+
242+
#updateTransaction: SmartTransactionsControllerOptions['updateTransaction'];
243+
236244
/* istanbul ignore next */
237245
async #fetch(request: string, options?: RequestInit) {
238246
const fetchOptions = {
@@ -248,7 +256,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
248256

249257
constructor({
250258
interval = DEFAULT_INTERVAL,
251-
clientId = DEFAULT_CLIENT_ID,
259+
clientId,
252260
chainId: InitialChainId = ChainId.mainnet,
253261
supportedChainIds = [ChainId.mainnet, ChainId.sepolia],
254262
getNonceLock,
@@ -258,6 +266,8 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
258266
messenger,
259267
getTransactions,
260268
getMetaMetricsProps,
269+
getFeatureFlags,
270+
updateTransaction,
261271
}: SmartTransactionsControllerOptions) {
262272
super({
263273
name: controllerName,
@@ -279,6 +289,8 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
279289
this.#getRegularTransactions = getTransactions;
280290
this.#trackMetaMetricsEvent = trackMetaMetricsEvent;
281291
this.#getMetaMetricsProps = getMetaMetricsProps;
292+
this.#getFeatureFlags = getFeatureFlags;
293+
this.#updateTransaction = updateTransaction;
282294

283295
this.initializeSmartTransactionsForChainId();
284296

@@ -530,24 +542,47 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
530542
return;
531543
}
532544

545+
const currentSmartTransaction = currentSmartTransactions[currentIndex];
546+
const nextSmartTransaction = {
547+
...currentSmartTransaction,
548+
...smartTransaction,
549+
};
550+
533551
// We have to emit this event here, because then a txHash is returned to the TransactionController once it's available
534552
// and the #doesTransactionNeedConfirmation function will work properly, since it will find the txHash in the regular transactions list.
535553
this.messagingSystem.publish(
536554
`SmartTransactionsController:smartTransaction`,
537-
smartTransaction,
555+
nextSmartTransaction,
538556
);
539557

558+
if (nextSmartTransaction.status === SmartTransactionStatuses.CANCELLED) {
559+
const returnTxHashAsap = getReturnTxHashAsap(
560+
this.#clientId,
561+
this.#getFeatureFlags()?.smartTransactions,
562+
);
563+
if (returnTxHashAsap && nextSmartTransaction.transactionId) {
564+
const foundTransaction = this.#getRegularTransactions().find(
565+
(transaction) =>
566+
transaction.id === nextSmartTransaction.transactionId,
567+
);
568+
if (foundTransaction) {
569+
const updatedTransaction = {
570+
...foundTransaction,
571+
status: TransactionStatus.failed,
572+
};
573+
this.#updateTransaction(
574+
updatedTransaction as TransactionMeta,
575+
'Smart transaction cancelled',
576+
);
577+
}
578+
}
579+
}
580+
540581
if (
541582
(smartTransaction.status === SmartTransactionStatuses.SUCCESS ||
542583
smartTransaction.status === SmartTransactionStatuses.REVERTED) &&
543584
!smartTransaction.confirmed
544585
) {
545-
// confirm smart transaction
546-
const currentSmartTransaction = currentSmartTransactions[currentIndex];
547-
const nextSmartTransaction = {
548-
...currentSmartTransaction,
549-
...smartTransaction,
550-
};
551586
await this.#confirmSmartTransaction(nextSmartTransaction, {
552587
chainId,
553588
ethQuery,
@@ -892,6 +927,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
892927
txHash: submitTransactionResponse.txHash,
893928
cancellable: true,
894929
type: transactionMeta?.type ?? 'swap',
930+
transactionId: transactionMeta?.id,
895931
},
896932
{ chainId, ethQuery },
897933
);

src/index.test.ts

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import SmartTransactionsController, {
88
type AllowedActions,
99
type AllowedEvents,
1010
} from './SmartTransactionsController';
11+
import { ClientId } from './types';
1112

1213
describe('default export', () => {
1314
it('exports SmartTransactionsController', () => {
@@ -30,6 +31,9 @@ describe('default export', () => {
3031
getMetaMetricsProps: jest.fn(async () => {
3132
return Promise.resolve({});
3233
}),
34+
getFeatureFlags: jest.fn(),
35+
updateTransaction: jest.fn(),
36+
clientId: ClientId.Extension,
3337
});
3438
expect(controller).toBeInstanceOf(SmartTransactionsController);
3539
jest.clearAllTimers();

0 commit comments

Comments
 (0)