@metamask/transaction-controller 16.0.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +66 -1
  2. package/dist/TransactionController.d.ts +184 -21
  3. package/dist/TransactionController.d.ts.map +1 -1
  4. package/dist/TransactionController.js +635 -121
  5. package/dist/TransactionController.js.map +1 -1
  6. package/dist/constants.d.ts +2 -3
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +3 -15
  9. package/dist/constants.js.map +1 -1
  10. package/dist/helpers/PendingTransactionTracker.d.ts +23 -5
  11. package/dist/helpers/PendingTransactionTracker.d.ts.map +1 -1
  12. package/dist/helpers/PendingTransactionTracker.js +263 -108
  13. package/dist/helpers/PendingTransactionTracker.js.map +1 -1
  14. package/dist/logger.d.ts +0 -1
  15. package/dist/logger.d.ts.map +1 -1
  16. package/dist/logger.js +1 -2
  17. package/dist/logger.js.map +1 -1
  18. package/dist/types.d.ts +225 -8
  19. package/dist/types.d.ts.map +1 -1
  20. package/dist/types.js +1 -0
  21. package/dist/types.js.map +1 -1
  22. package/dist/utils/etherscan.d.ts.map +1 -1
  23. package/dist/utils/etherscan.js.map +1 -1
  24. package/dist/utils/gas-fees.d.ts +3 -2
  25. package/dist/utils/gas-fees.d.ts.map +1 -1
  26. package/dist/utils/gas-fees.js +22 -4
  27. package/dist/utils/gas-fees.js.map +1 -1
  28. package/dist/utils/gas.d.ts +2 -0
  29. package/dist/utils/gas.d.ts.map +1 -1
  30. package/dist/utils/gas.js +26 -17
  31. package/dist/utils/gas.js.map +1 -1
  32. package/dist/utils/swaps.d.ts +81 -0
  33. package/dist/utils/swaps.d.ts.map +1 -0
  34. package/dist/utils/swaps.js +253 -0
  35. package/dist/utils/swaps.js.map +1 -0
  36. package/dist/utils/utils.d.ts +21 -3
  37. package/dist/utils/utils.d.ts.map +1 -1
  38. package/dist/utils/utils.js +45 -4
  39. package/dist/utils/utils.js.map +1 -1
  40. package/dist/utils/validation.d.ts +1 -1
  41. package/dist/utils/validation.d.ts.map +1 -1
  42. package/dist/utils/validation.js +90 -7
  43. package/dist/utils/validation.js.map +1 -1
  44. package/package.json +16 -15
@@ -24,7 +24,7 @@ const eth_method_registry_1 = __importDefault(require("eth-method-registry"));
24
24
  const ethereumjs_util_1 = require("ethereumjs-util");
25
25
  const events_1 = require("events");
26
26
  const lodash_1 = require("lodash");
27
- const nonce_tracker_1 = __importDefault(require("nonce-tracker"));
27
+ const nonce_tracker_1 = require("nonce-tracker");
28
28
  const uuid_1 = require("uuid");
29
29
  const EtherscanRemoteTransactionSource_1 = require("./helpers/EtherscanRemoteTransactionSource");
30
30
  const IncomingTransactionHelper_1 = require("./helpers/IncomingTransactionHelper");
@@ -35,6 +35,7 @@ const external_transactions_1 = require("./utils/external-transactions");
35
35
  const gas_1 = require("./utils/gas");
36
36
  const gas_fees_1 = require("./utils/gas-fees");
37
37
  const history_1 = require("./utils/history");
38
+ const swaps_1 = require("./utils/swaps");
38
39
  const transaction_type_1 = require("./utils/transaction-type");
39
40
  const utils_1 = require("./utils/utils");
40
41
  const validation_1 = require("./utils/validation");
@@ -54,7 +55,7 @@ const controllerName = 'TransactionController';
54
55
  /**
55
56
  * Controller responsible for submitting and managing transactions.
56
57
  */
57
- class TransactionController extends base_controller_1.BaseController {
58
+ class TransactionController extends base_controller_1.BaseControllerV1 {
58
59
  /**
59
60
  * Creates a TransactionController instance.
60
61
  *
@@ -62,6 +63,8 @@ class TransactionController extends base_controller_1.BaseController {
62
63
  * @param options.blockTracker - The block tracker used to poll for new blocks data.
63
64
  * @param options.disableHistory - Whether to disable storing history in transaction metadata.
64
65
  * @param options.disableSendFlowHistory - Explicitly disable transaction metadata history.
66
+ * @param options.disableSwaps - Whether to disable additional processing on swaps transactions.
67
+ * @param options.getSavedGasFees - Gets the saved gas fee config.
65
68
  * @param options.getCurrentAccountEIP1559Compatibility - Whether or not the account supports EIP-1559.
66
69
  * @param options.getCurrentNetworkEIP1559Compatibility - Whether or not the network supports EIP-1559.
67
70
  * @param options.getGasFeeEstimates - Callback to retrieve gas fee estimates.
@@ -75,13 +78,23 @@ class TransactionController extends base_controller_1.BaseController {
75
78
  * @param options.incomingTransactions.updateTransactions - Whether to update local transactions using remote transaction data.
76
79
  * @param options.messenger - The controller messenger.
77
80
  * @param options.onNetworkStateChange - Allows subscribing to network controller state changes.
81
+ * @param options.pendingTransactions - Configuration options for pending transaction support.
82
+ * @param options.pendingTransactions.isResubmitEnabled - Whether transaction publishing is automatically retried.
78
83
  * @param options.provider - The provider used to create the underlying EthQuery instance.
79
84
  * @param options.securityProviderRequest - A function for verifying a transaction, whether it is malicious or not.
85
+ * @param options.hooks - The controller hooks.
86
+ * @param options.hooks.afterSign - Additional logic to execute after signing a transaction. Return false to not change the status to signed.
87
+ * @param options.hooks.beforeApproveOnInit - Additional logic to execute before starting an approval flow for a transaction during initialization. Return false to skip the transaction.
88
+ * @param options.hooks.beforeCheckPendingTransaction - Additional logic to execute before checking pending transactions. Return false to prevent the broadcast of the transaction.
89
+ * @param options.hooks.beforePublish - Additional logic to execute before publishing a transaction. Return false to prevent the broadcast of the transaction.
90
+ * @param options.hooks.getAdditionalSignArguments - Returns additional arguments required to sign a transaction.
80
91
  * @param config - Initial options used to configure this controller.
81
92
  * @param state - Initial state to set on this controller.
82
93
  */
83
- constructor({ blockTracker, disableHistory, disableSendFlowHistory, getCurrentAccountEIP1559Compatibility, getCurrentNetworkEIP1559Compatibility, getGasFeeEstimates, getNetworkState, getPermittedAccounts, getSelectedAddress, incomingTransactions = {}, messenger, onNetworkStateChange, provider, securityProviderRequest, }, config, state) {
94
+ constructor({ blockTracker, disableHistory, disableSendFlowHistory, disableSwaps, getSavedGasFees, getCurrentAccountEIP1559Compatibility, getCurrentNetworkEIP1559Compatibility, getGasFeeEstimates, getNetworkState, getPermittedAccounts, getSelectedAddress, incomingTransactions = {}, messenger, onNetworkStateChange, pendingTransactions = {}, provider, securityProviderRequest, hooks = {}, }, config, state) {
95
+ var _a, _b, _c, _d, _e;
84
96
  super(config, state);
97
+ this.inProcessOfSigning = new Set();
85
98
  this.mutex = new async_mutex_1.Mutex();
86
99
  /**
87
100
  * EventEmitter instance used to listen to specific transactional events
@@ -103,13 +116,14 @@ class TransactionController extends base_controller_1.BaseController {
103
116
  this.provider = provider;
104
117
  this.messagingSystem = messenger;
105
118
  this.getNetworkState = getNetworkState;
106
- // @ts-expect-error TODO: Provider type alignment
107
119
  this.ethQuery = new eth_query_1.default(provider);
108
120
  this.isSendFlowHistoryDisabled = disableSendFlowHistory !== null && disableSendFlowHistory !== void 0 ? disableSendFlowHistory : false;
109
121
  this.isHistoryDisabled = disableHistory !== null && disableHistory !== void 0 ? disableHistory : false;
122
+ this.isSwapsDisabled = disableSwaps !== null && disableSwaps !== void 0 ? disableSwaps : false;
110
123
  this.registry = new eth_method_registry_1.default({ provider });
124
+ this.getSavedGasFees = getSavedGasFees !== null && getSavedGasFees !== void 0 ? getSavedGasFees : ((_chainId) => undefined);
111
125
  this.getCurrentAccountEIP1559Compatibility =
112
- getCurrentAccountEIP1559Compatibility;
126
+ getCurrentAccountEIP1559Compatibility !== null && getCurrentAccountEIP1559Compatibility !== void 0 ? getCurrentAccountEIP1559Compatibility : (() => Promise.resolve(true));
113
127
  this.getCurrentNetworkEIP1559Compatibility =
114
128
  getCurrentNetworkEIP1559Compatibility;
115
129
  this.getGasFeeEstimates =
@@ -117,11 +131,21 @@ class TransactionController extends base_controller_1.BaseController {
117
131
  this.getPermittedAccounts = getPermittedAccounts;
118
132
  this.getSelectedAddress = getSelectedAddress;
119
133
  this.securityProviderRequest = securityProviderRequest;
120
- this.nonceTracker = new nonce_tracker_1.default({
134
+ this.afterSign = (_a = hooks === null || hooks === void 0 ? void 0 : hooks.afterSign) !== null && _a !== void 0 ? _a : (() => true);
135
+ this.beforeApproveOnInit = (_b = hooks === null || hooks === void 0 ? void 0 : hooks.beforeApproveOnInit) !== null && _b !== void 0 ? _b : (() => true);
136
+ this.beforeCheckPendingTransaction =
137
+ (_c = hooks === null || hooks === void 0 ? void 0 : hooks.beforeCheckPendingTransaction) !== null && _c !== void 0 ? _c :
138
+ /* istanbul ignore next */
139
+ (() => true);
140
+ this.beforePublish = (_d = hooks === null || hooks === void 0 ? void 0 : hooks.beforePublish) !== null && _d !== void 0 ? _d : (() => true);
141
+ this.getAdditionalSignArguments =
142
+ (_e = hooks === null || hooks === void 0 ? void 0 : hooks.getAdditionalSignArguments) !== null && _e !== void 0 ? _e : (() => []);
143
+ this.nonceTracker = new nonce_tracker_1.NonceTracker({
144
+ // @ts-expect-error provider types misaligned: SafeEventEmitterProvider vs Record<string,string>
121
145
  provider,
122
146
  blockTracker,
123
- getPendingTransactions: (address) => (0, utils_1.getAndFormatTransactionsForNonceTracker)(address, types_1.TransactionStatus.submitted, this.state.transactions),
124
- getConfirmedTransactions: (address) => (0, utils_1.getAndFormatTransactionsForNonceTracker)(address, types_1.TransactionStatus.confirmed, this.state.transactions),
147
+ getPendingTransactions: this.getNonceTrackerTransactions.bind(this, types_1.TransactionStatus.submitted),
148
+ getConfirmedTransactions: this.getNonceTrackerTransactions.bind(this, types_1.TransactionStatus.confirmed),
125
149
  });
126
150
  this.incomingTransactionHelper = new IncomingTransactionHelper_1.IncomingTransactionHelper({
127
151
  blockTracker,
@@ -139,25 +163,37 @@ class TransactionController extends base_controller_1.BaseController {
139
163
  this.incomingTransactionHelper.hub.on('transactions', this.onIncomingTransactions.bind(this));
140
164
  this.incomingTransactionHelper.hub.on('updatedLastFetchedBlockNumbers', this.onUpdatedLastFetchedBlockNumbers.bind(this));
141
165
  this.pendingTransactionTracker = new PendingTransactionTracker_1.PendingTransactionTracker({
166
+ approveTransaction: this.approveTransaction.bind(this),
142
167
  blockTracker,
143
- failTransaction: this.failTransaction.bind(this),
144
168
  getChainId: this.getChainId.bind(this),
145
169
  getEthQuery: () => this.ethQuery,
146
170
  getTransactions: () => this.state.transactions,
171
+ isResubmitEnabled: pendingTransactions.isResubmitEnabled,
147
172
  nonceTracker: this.nonceTracker,
173
+ onStateChange: this.subscribe.bind(this),
174
+ publishTransaction: this.publishTransaction.bind(this),
175
+ hooks: {
176
+ beforeCheckPendingTransaction: this.beforeCheckPendingTransaction.bind(this),
177
+ beforePublish: this.beforePublish.bind(this),
178
+ },
148
179
  });
149
- this.pendingTransactionTracker.hub.on('transactions', this.onPendingTransactionsUpdate.bind(this));
150
- this.pendingTransactionTracker.hub.on('transaction-confirmed', (transactionMeta) => this.hub.emit(`${transactionMeta.id}:confirmed`, transactionMeta));
180
+ this.addPendingTransactionTrackerListeners();
151
181
  onNetworkStateChange(() => {
152
- // @ts-expect-error TODO: Provider type alignment
153
182
  this.ethQuery = new eth_query_1.default(this.provider);
154
183
  this.registry = new eth_method_registry_1.default({ provider: this.provider });
184
+ this.onBootCleanup();
155
185
  });
156
- this.pendingTransactionTracker.start();
186
+ this.onBootCleanup();
157
187
  }
158
- failTransaction(transactionMeta, error) {
159
- const newTransactionMeta = Object.assign(Object.assign({}, transactionMeta), { error, status: types_1.TransactionStatus.failed });
188
+ failTransaction(transactionMeta, error, actionId) {
189
+ const newTransactionMeta = Object.assign(Object.assign({}, transactionMeta), { error: (0, utils_1.normalizeTxError)(error), status: types_1.TransactionStatus.failed });
190
+ this.hub.emit('transaction-failed', {
191
+ actionId,
192
+ error: error.message,
193
+ transactionMeta: newTransactionMeta,
194
+ });
160
195
  this.updateTransaction(newTransactionMeta, 'TransactionController#failTransaction - Add error message and set status to failed');
196
+ this.onTransactionStatusChange(newTransactionMeta);
161
197
  this.hub.emit(`${transactionMeta.id}:finished`, newTransactionMeta);
162
198
  }
163
199
  registryLookup(fourBytePrefix) {
@@ -208,12 +244,14 @@ class TransactionController extends base_controller_1.BaseController {
208
244
  * @param opts.securityAlertResponse - Response from security validator.
209
245
  * @param opts.sendFlowHistory - The sendFlowHistory entries to add.
210
246
  * @param opts.type - Type of transaction to add, such as 'cancel' or 'swap'.
247
+ * @param opts.swaps - Options for swaps transactions.
248
+ * @param opts.swaps.hasApproveTx - Whether the transaction has an approval transaction.
249
+ * @param opts.swaps.meta - Metadata for swap transaction.
211
250
  * @returns Object containing a promise resolving to the transaction hash if approved.
212
251
  */
213
- addTransaction(txParams, { actionId, deviceConfirmedOn, method, origin, requireApproval, securityAlertResponse, sendFlowHistory, type, } = {}) {
252
+ addTransaction(txParams, { actionId, deviceConfirmedOn, method, origin, requireApproval, securityAlertResponse, sendFlowHistory, swaps = {}, type, } = {}) {
214
253
  return __awaiter(this, void 0, void 0, function* () {
215
- const chainId = this.getChainId();
216
- const { transactions } = this.state;
254
+ (0, logger_1.projectLogger)('Adding transaction', txParams);
217
255
  txParams = (0, utils_1.normalizeTxParams)(txParams);
218
256
  const isEIP1559Compatible = yield this.getEIP1559Compatibility();
219
257
  (0, validation_1.validateTxParams)(txParams, isEIP1559Compatible);
@@ -223,6 +261,7 @@ class TransactionController extends base_controller_1.BaseController {
223
261
  const dappSuggestedGasFees = this.generateDappSuggestedGasFees(txParams, origin);
224
262
  const transactionType = type !== null && type !== void 0 ? type : (yield (0, transaction_type_1.determineTransactionType)(txParams, this.ethQuery)).type;
225
263
  const existingTransactionMeta = this.getTransactionWithActionId(actionId);
264
+ const chainId = this.getChainId();
226
265
  // If a request to add a transaction with the same actionId is submitted again, a new transaction will not be created for it.
227
266
  const transactionMeta = existingTransactionMeta || {
228
267
  // Add actionId to txMeta to check if same actionId is seen again
@@ -240,17 +279,7 @@ class TransactionController extends base_controller_1.BaseController {
240
279
  verifiedOnBlockchain: false,
241
280
  type: transactionType,
242
281
  };
243
- yield (0, gas_1.updateGas)({
244
- ethQuery: this.ethQuery,
245
- providerConfig: this.getNetworkState().providerConfig,
246
- txMeta: transactionMeta,
247
- });
248
- yield (0, gas_fees_1.updateGasFees)({
249
- eip1559: isEIP1559Compatible,
250
- ethQuery: this.ethQuery,
251
- getGasFeeEstimates: this.getGasFeeEstimates.bind(this),
252
- txMeta: transactionMeta,
253
- });
282
+ yield this.updateGasProperties(transactionMeta);
254
283
  // Checks if a transaction already exists with a given actionId
255
284
  if (!existingTransactionMeta) {
256
285
  // Set security provider response
@@ -265,16 +294,19 @@ class TransactionController extends base_controller_1.BaseController {
265
294
  if (!this.isHistoryDisabled) {
266
295
  (0, history_1.addInitialHistorySnapshot)(transactionMeta);
267
296
  }
268
- transactions.push(transactionMeta);
269
- this.update({
270
- transactions: this.trimTransactionsForState(transactions),
297
+ yield (0, swaps_1.updateSwapsTransaction)(transactionMeta, transactionType, swaps, {
298
+ isSwapsDisabled: this.isSwapsDisabled,
299
+ cancelTransaction: this.cancelTransaction.bind(this),
300
+ controllerHubEmitter: this.hub.emit.bind(this.hub),
271
301
  });
302
+ this.addMetadata(transactionMeta);
272
303
  this.hub.emit(`unapprovedTransaction`, transactionMeta);
273
304
  }
274
305
  return {
275
306
  result: this.processApproval(transactionMeta, {
276
307
  isExisting: Boolean(existingTransactionMeta),
277
308
  requireApproval,
309
+ actionId,
278
310
  }),
279
311
  transactionMeta,
280
312
  };
@@ -291,22 +323,6 @@ class TransactionController extends base_controller_1.BaseController {
291
323
  yield this.incomingTransactionHelper.update();
292
324
  });
293
325
  }
294
- /**
295
- * Creates approvals for all unapproved transactions persisted.
296
- */
297
- initApprovals() {
298
- const chainId = this.getChainId();
299
- const unapprovedTxs = this.state.transactions.filter((transaction) => transaction.status === types_1.TransactionStatus.unapproved &&
300
- transaction.chainId === chainId);
301
- for (const txMeta of unapprovedTxs) {
302
- this.processApproval(txMeta, {
303
- shouldShowRequest: false,
304
- }).catch((error) => {
305
- /* istanbul ignore next */
306
- console.error('Error during persisted transaction approval', error);
307
- });
308
- }
309
- }
310
326
  /**
311
327
  * Attempts to cancel a transaction based on its ID by setting its status to "rejected"
312
328
  * and emitting a `<tx.id>:finished` hub event.
@@ -314,15 +330,22 @@ class TransactionController extends base_controller_1.BaseController {
314
330
  * @param transactionId - The ID of the transaction to cancel.
315
331
  * @param gasValues - The gas values to use for the cancellation transaction.
316
332
  * @param options - The options for the cancellation transaction.
333
+ * @param options.actionId - Unique ID to prevent duplicate requests.
317
334
  * @param options.estimatedBaseFee - The estimated base fee of the transaction.
318
335
  */
319
- stopTransaction(transactionId, gasValues, { estimatedBaseFee } = {}) {
336
+ stopTransaction(transactionId, gasValues, { estimatedBaseFee, actionId, } = {}) {
320
337
  var _a, _b;
321
338
  return __awaiter(this, void 0, void 0, function* () {
339
+ // If transaction is found for same action id, do not create a cancel transaction.
340
+ if (this.getTransactionWithActionId(actionId)) {
341
+ return;
342
+ }
322
343
  if (gasValues) {
344
+ // Not good practice to reassign a parameter but temporarily avoiding a larger refactor.
345
+ gasValues = (0, utils_1.normalizeGasFeeValues)(gasValues);
323
346
  (0, utils_1.validateGasValues)(gasValues);
324
347
  }
325
- const transactionMeta = this.state.transactions.find(({ id }) => id === transactionId);
348
+ const transactionMeta = this.getTransaction(transactionId);
326
349
  if (!transactionMeta) {
327
350
  return;
328
351
  }
@@ -349,13 +372,13 @@ class TransactionController extends base_controller_1.BaseController {
349
372
  const newMaxPriorityFeePerGas = (maxPriorityFeePerGasValues &&
350
373
  (0, utils_1.validateMinimumIncrease)(maxPriorityFeePerGasValues, minMaxPriorityFeePerGas)) ||
351
374
  (existingMaxPriorityFeePerGas && minMaxPriorityFeePerGas);
352
- const txParams = newMaxFeePerGas && newMaxPriorityFeePerGas
375
+ const newTxParams = newMaxFeePerGas && newMaxPriorityFeePerGas
353
376
  ? {
354
377
  from: transactionMeta.txParams.from,
355
378
  gasLimit: transactionMeta.txParams.gas,
356
379
  maxFeePerGas: newMaxFeePerGas,
357
380
  maxPriorityFeePerGas: newMaxPriorityFeePerGas,
358
- type: 2,
381
+ type: types_1.TransactionEnvelopeType.feeMarket,
359
382
  nonce: transactionMeta.txParams.nonce,
360
383
  to: transactionMeta.txParams.from,
361
384
  value: '0x0',
@@ -368,14 +391,33 @@ class TransactionController extends base_controller_1.BaseController {
368
391
  to: transactionMeta.txParams.from,
369
392
  value: '0x0',
370
393
  };
371
- const unsignedEthTx = this.prepareUnsignedEthTx(txParams);
394
+ const unsignedEthTx = this.prepareUnsignedEthTx(newTxParams);
372
395
  const signedTx = yield this.sign(unsignedEthTx, transactionMeta.txParams.from);
373
- yield this.updateTransactionMetaRSV(transactionMeta, signedTx);
374
396
  const rawTx = (0, ethereumjs_util_1.bufferToHex)(signedTx.serialize());
375
- yield (0, controller_utils_1.query)(this.ethQuery, 'sendRawTransaction', [rawTx]);
376
- transactionMeta.estimatedBaseFee = estimatedBaseFee;
377
- transactionMeta.status = types_1.TransactionStatus.cancelled;
378
- this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
397
+ const hash = yield this.publishTransaction(rawTx);
398
+ const cancelTransactionMeta = {
399
+ actionId,
400
+ chainId: transactionMeta.chainId,
401
+ estimatedBaseFee,
402
+ hash,
403
+ id: (0, uuid_1.v1)(),
404
+ originalGasEstimate: transactionMeta.txParams.gas,
405
+ status: types_1.TransactionStatus.submitted,
406
+ time: Date.now(),
407
+ type: types_1.TransactionType.cancel,
408
+ txParams: newTxParams,
409
+ };
410
+ this.addMetadata(cancelTransactionMeta);
411
+ // stopTransaction has no approval request, so we assume the user has already approved the transaction
412
+ this.hub.emit('transaction-approved', {
413
+ transactionMeta: cancelTransactionMeta,
414
+ actionId,
415
+ });
416
+ this.hub.emit('transaction-submitted', {
417
+ transactionMeta: cancelTransactionMeta,
418
+ actionId,
419
+ });
420
+ this.hub.emit(`${cancelTransactionMeta.id}:finished`, cancelTransactionMeta);
379
421
  });
380
422
  }
381
423
  /**
@@ -395,6 +437,8 @@ class TransactionController extends base_controller_1.BaseController {
395
437
  return;
396
438
  }
397
439
  if (gasValues) {
440
+ // Not good practice to reassign a parameter but temporarily avoiding a larger refactor.
441
+ gasValues = (0, utils_1.normalizeGasFeeValues)(gasValues);
398
442
  (0, utils_1.validateGasValues)(gasValues);
399
443
  }
400
444
  const transactionMeta = this.state.transactions.find(({ id }) => id === transactionId);
@@ -406,7 +450,6 @@ class TransactionController extends base_controller_1.BaseController {
406
450
  if (!this.sign) {
407
451
  throw new Error('No sign method defined.');
408
452
  }
409
- const { transactions } = this.state;
410
453
  // gasPrice (legacy non EIP1559)
411
454
  const minGasPrice = (0, utils_1.getIncreasedPriceFromExisting)(transactionMeta.txParams.gasPrice, exports.SPEED_UP_RATE);
412
455
  const gasPriceFromValues = (0, utils_1.isGasPriceValue)(gasValues) && gasValues.gasPrice;
@@ -428,18 +471,26 @@ class TransactionController extends base_controller_1.BaseController {
428
471
  (0, utils_1.validateMinimumIncrease)(maxPriorityFeePerGasValues, minMaxPriorityFeePerGas)) ||
429
472
  (existingMaxPriorityFeePerGas && minMaxPriorityFeePerGas);
430
473
  const txParams = newMaxFeePerGas && newMaxPriorityFeePerGas
431
- ? Object.assign(Object.assign({}, transactionMeta.txParams), { gasLimit: transactionMeta.txParams.gas, maxFeePerGas: newMaxFeePerGas, maxPriorityFeePerGas: newMaxPriorityFeePerGas, type: 2 }) : Object.assign(Object.assign({}, transactionMeta.txParams), { gasLimit: transactionMeta.txParams.gas, gasPrice: newGasPrice });
474
+ ? Object.assign(Object.assign({}, transactionMeta.txParams), { gasLimit: transactionMeta.txParams.gas, maxFeePerGas: newMaxFeePerGas, maxPriorityFeePerGas: newMaxPriorityFeePerGas, type: types_1.TransactionEnvelopeType.feeMarket }) : Object.assign(Object.assign({}, transactionMeta.txParams), { gasLimit: transactionMeta.txParams.gas, gasPrice: newGasPrice });
432
475
  const unsignedEthTx = this.prepareUnsignedEthTx(txParams);
433
476
  const signedTx = yield this.sign(unsignedEthTx, transactionMeta.txParams.from);
434
477
  yield this.updateTransactionMetaRSV(transactionMeta, signedTx);
435
478
  const rawTx = (0, ethereumjs_util_1.bufferToHex)(signedTx.serialize());
436
479
  const hash = yield (0, controller_utils_1.query)(this.ethQuery, 'sendRawTransaction', [rawTx]);
437
480
  const baseTransactionMeta = Object.assign(Object.assign({}, transactionMeta), { estimatedBaseFee, id: (0, uuid_1.v1)(), time: Date.now(), hash,
438
- actionId, originalGasEstimate: transactionMeta.txParams.gas, type: types_1.TransactionType.retry });
481
+ actionId, originalGasEstimate: transactionMeta.txParams.gas, type: types_1.TransactionType.retry, originalType: transactionMeta.type });
439
482
  const newTransactionMeta = newMaxFeePerGas && newMaxPriorityFeePerGas
440
483
  ? Object.assign(Object.assign({}, baseTransactionMeta), { txParams: Object.assign(Object.assign({}, transactionMeta.txParams), { maxFeePerGas: newMaxFeePerGas, maxPriorityFeePerGas: newMaxPriorityFeePerGas }) }) : Object.assign(Object.assign({}, baseTransactionMeta), { txParams: Object.assign(Object.assign({}, transactionMeta.txParams), { gasPrice: newGasPrice }) });
441
- transactions.push(newTransactionMeta);
442
- this.update({ transactions: this.trimTransactionsForState(transactions) });
484
+ this.addMetadata(newTransactionMeta);
485
+ // speedUpTransaction has no approval request, so we assume the user has already approved the transaction
486
+ this.hub.emit('transaction-approved', {
487
+ transactionMeta: newTransactionMeta,
488
+ actionId,
489
+ });
490
+ this.hub.emit('transaction-submitted', {
491
+ transactionMeta: newTransactionMeta,
492
+ actionId,
493
+ });
443
494
  this.hub.emit(`${transactionMeta.id}:speedup`, newTransactionMeta);
444
495
  });
445
496
  }
@@ -455,6 +506,22 @@ class TransactionController extends base_controller_1.BaseController {
455
506
  return { gas: estimatedGas, simulationFails };
456
507
  });
457
508
  }
509
+ /**
510
+ * Estimates required gas for a given transaction and add additional gas buffer with the given multiplier.
511
+ *
512
+ * @param transaction - The transaction params to estimate gas for.
513
+ * @param multiplier - The multiplier to use for the gas buffer.
514
+ */
515
+ estimateGasBuffered(transaction, multiplier) {
516
+ return __awaiter(this, void 0, void 0, function* () {
517
+ const { blockGasLimit, estimatedGas, simulationFails } = yield (0, gas_1.estimateGas)(transaction, this.ethQuery);
518
+ const gas = (0, gas_1.addGasBuffer)(estimatedGas, blockGasLimit, multiplier);
519
+ return {
520
+ gas,
521
+ simulationFails,
522
+ };
523
+ });
524
+ }
458
525
  /**
459
526
  * Updates an existing transaction in state.
460
527
  *
@@ -472,6 +539,23 @@ class TransactionController extends base_controller_1.BaseController {
472
539
  transactions[index] = transactionMeta;
473
540
  this.update({ transactions: this.trimTransactionsForState(transactions) });
474
541
  }
542
+ /**
543
+ * Update the security alert response for a transaction.
544
+ *
545
+ * @param transactionId - ID of the transaction.
546
+ * @param securityAlertResponse - The new security alert response for the transaction.
547
+ */
548
+ updateSecurityAlertResponse(transactionId, securityAlertResponse) {
549
+ if (!securityAlertResponse) {
550
+ throw new Error('updateSecurityAlertResponse: securityAlertResponse should not be null');
551
+ }
552
+ const transactionMeta = this.getTransaction(transactionId);
553
+ if (!transactionMeta) {
554
+ throw new Error(`Cannot update security alert response as no transaction metadata found`);
555
+ }
556
+ const updatedMeta = (0, lodash_1.merge)(transactionMeta, { securityAlertResponse });
557
+ this.updateTransaction(updatedMeta, 'TransactionController:updatesecurityAlertResponse - securityAlertResponse updated');
558
+ }
475
559
  /**
476
560
  * Removes all transactions from state, optionally based on the current network.
477
561
  *
@@ -529,9 +613,16 @@ class TransactionController extends base_controller_1.BaseController {
529
613
  this.markNonceDuplicatesDropped(transactionId);
530
614
  // Update external provided transaction with updated gas values and confirmed status.
531
615
  this.updateTransaction(transactionMeta, 'TransactionController:confirmExternalTransaction - Add external transaction');
616
+ this.onTransactionStatusChange(transactionMeta);
617
+ // Intentional given potential duration of process.
618
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
619
+ this.updatePostBalance(transactionMeta);
620
+ this.hub.emit('transaction-confirmed', {
621
+ transactionMeta,
622
+ });
532
623
  }
533
624
  catch (error) {
534
- console.error(error);
625
+ console.error('Failed to confirm external transaction', error);
535
626
  }
536
627
  });
537
628
  }
@@ -610,6 +701,36 @@ class TransactionController extends base_controller_1.BaseController {
610
701
  this.updateTransaction(updatedMeta, 'TransactionController:updateTransactionGasFees - gas values updated');
611
702
  return this.getTransaction(transactionId);
612
703
  }
704
+ /**
705
+ * Update the previous gas values of a transaction.
706
+ *
707
+ * @param transactionId - The ID of the transaction to update.
708
+ * @param previousGas - Previous gas values to update.
709
+ * @param previousGas.gasLimit - Maxmimum number of units of gas to use for this transaction.
710
+ * @param previousGas.maxFeePerGas - Maximum amount per gas to pay for the transaction, including the priority fee.
711
+ * @param previousGas.maxPriorityFeePerGas - Maximum amount per gas to give to validator as incentive.
712
+ * @returns The updated transactionMeta.
713
+ */
714
+ updatePreviousGasParams(transactionId, { gasLimit, maxFeePerGas, maxPriorityFeePerGas, }) {
715
+ const transactionMeta = this.getTransaction(transactionId);
716
+ if (!transactionMeta) {
717
+ throw new Error(`Cannot update transaction as no transaction metadata found`);
718
+ }
719
+ (0, utils_1.validateIfTransactionUnapproved)(transactionMeta, 'updatePreviousGasParams');
720
+ const transactionPreviousGas = {
721
+ previousGas: {
722
+ gasLimit,
723
+ maxFeePerGas,
724
+ maxPriorityFeePerGas,
725
+ },
726
+ };
727
+ // only update what is defined
728
+ transactionPreviousGas.previousGas = (0, lodash_1.pickBy)(transactionPreviousGas.previousGas);
729
+ // merge updated previous gas values with existing transaction meta
730
+ const updatedMeta = (0, lodash_1.merge)(transactionMeta, transactionPreviousGas);
731
+ this.updateTransaction(updatedMeta, 'TransactionController:updatePreviousGasParams - Previous gas values updated');
732
+ return this.getTransaction(transactionId);
733
+ }
613
734
  /**
614
735
  * Gets the next nonce according to the nonce-tracker.
615
736
  * Ensure `releaseLock` is called once processing of the `nonce` value is complete.
@@ -622,7 +743,312 @@ class TransactionController extends base_controller_1.BaseController {
622
743
  return this.nonceTracker.getNonceLock(address);
623
744
  });
624
745
  }
625
- processApproval(transactionMeta, { isExisting = false, requireApproval, shouldShowRequest = true, }) {
746
+ /**
747
+ * Updates the editable parameters of a transaction.
748
+ *
749
+ * @param txId - The ID of the transaction to update.
750
+ * @param params - The editable parameters to update.
751
+ * @param params.data - Data to pass with the transaction.
752
+ * @param params.gas - Maximum number of units of gas to use for the transaction.
753
+ * @param params.gasPrice - Price per gas for legacy transactions.
754
+ * @param params.from - Address to send the transaction from.
755
+ * @param params.to - Address to send the transaction to.
756
+ * @param params.value - Value associated with the transaction.
757
+ * @returns The updated transaction metadata.
758
+ */
759
+ updateEditableParams(txId, { data, gas, gasPrice, from, to, value, }) {
760
+ return __awaiter(this, void 0, void 0, function* () {
761
+ const transactionMeta = this.getTransaction(txId);
762
+ if (!transactionMeta) {
763
+ throw new Error(`Cannot update editable params as no transaction metadata found`);
764
+ }
765
+ (0, utils_1.validateIfTransactionUnapproved)(transactionMeta, 'updateEditableParams');
766
+ const editableParams = {
767
+ txParams: {
768
+ data,
769
+ from,
770
+ to,
771
+ value,
772
+ gas,
773
+ gasPrice,
774
+ },
775
+ };
776
+ editableParams.txParams = (0, lodash_1.pickBy)(editableParams.txParams);
777
+ const updatedTransaction = (0, lodash_1.merge)(transactionMeta, editableParams);
778
+ const { type } = yield (0, transaction_type_1.determineTransactionType)(updatedTransaction.txParams, this.ethQuery);
779
+ updatedTransaction.type = type;
780
+ this.updateTransaction(updatedTransaction, `Update Editable Params for ${txId}`);
781
+ return this.getTransaction(txId);
782
+ });
783
+ }
784
+ /**
785
+ * Signs and returns the raw transaction data for provided transaction params list.
786
+ *
787
+ * @param listOfTxParams - The list of transaction params to approve.
788
+ * @returns The raw transactions.
789
+ */
790
+ approveTransactionsWithSameNonce(listOfTxParams = []) {
791
+ return __awaiter(this, void 0, void 0, function* () {
792
+ if (listOfTxParams.length === 0) {
793
+ return '';
794
+ }
795
+ const initialTx = listOfTxParams[0];
796
+ const common = this.getCommonConfiguration();
797
+ const initialTxAsEthTx = tx_1.TransactionFactory.fromTxData(initialTx, {
798
+ common,
799
+ });
800
+ const initialTxAsSerializedHex = (0, ethereumjs_util_1.bufferToHex)(initialTxAsEthTx.serialize());
801
+ if (this.inProcessOfSigning.has(initialTxAsSerializedHex)) {
802
+ return '';
803
+ }
804
+ this.inProcessOfSigning.add(initialTxAsSerializedHex);
805
+ let rawTransactions, nonceLock;
806
+ try {
807
+ // TODO: we should add a check to verify that all transactions have the same from address
808
+ const fromAddress = initialTx.from;
809
+ nonceLock = yield this.nonceTracker.getNonceLock(fromAddress);
810
+ const nonce = nonceLock.nextNonce;
811
+ rawTransactions = yield Promise.all(listOfTxParams.map((txParams) => {
812
+ txParams.nonce = (0, ethereumjs_util_1.addHexPrefix)(nonce.toString(16));
813
+ return this.signExternalTransaction(txParams);
814
+ }));
815
+ }
816
+ catch (err) {
817
+ (0, logger_1.projectLogger)('Error while signing transactions with same nonce', err);
818
+ // Must set transaction to submitted/failed before releasing lock
819
+ // continue with error chain
820
+ throw err;
821
+ }
822
+ finally {
823
+ if (nonceLock) {
824
+ nonceLock.releaseLock();
825
+ }
826
+ this.inProcessOfSigning.delete(initialTxAsSerializedHex);
827
+ }
828
+ return rawTransactions;
829
+ });
830
+ }
831
+ /**
832
+ * Update a custodial transaction.
833
+ *
834
+ * @param transactionId - The ID of the transaction to update.
835
+ * @param options - The custodial transaction options to update.
836
+ * @param options.errorMessage - The error message to be assigned in case transaction status update to failed.
837
+ * @param options.hash - The new hash value to be assigned.
838
+ * @param options.status - The new status value to be assigned.
839
+ */
840
+ updateCustodialTransaction(transactionId, { errorMessage, hash, status, }) {
841
+ const transactionMeta = this.getTransaction(transactionId);
842
+ if (!transactionMeta) {
843
+ throw new Error(`Cannot update custodial transaction as no transaction metadata found`);
844
+ }
845
+ if (!transactionMeta.custodyId) {
846
+ throw new Error('Transaction must be a custodian transaction');
847
+ }
848
+ if (status &&
849
+ ![
850
+ types_1.TransactionStatus.submitted,
851
+ types_1.TransactionStatus.signed,
852
+ types_1.TransactionStatus.failed,
853
+ ].includes(status)) {
854
+ throw new Error(`Cannot update custodial transaction with status: ${status}`);
855
+ }
856
+ const updatedTransactionMeta = (0, lodash_1.merge)(transactionMeta, (0, lodash_1.pickBy)({ hash, status }));
857
+ if (status === types_1.TransactionStatus.submitted) {
858
+ updatedTransactionMeta.submittedTime = new Date().getTime();
859
+ }
860
+ if (status === types_1.TransactionStatus.failed) {
861
+ updatedTransactionMeta.error = (0, utils_1.normalizeTxError)(new Error(errorMessage));
862
+ }
863
+ this.updateTransaction(updatedTransactionMeta, `TransactionController:updateCustodialTransaction - Custodial transaction updated`);
864
+ }
865
+ /**
866
+ * Creates approvals for all unapproved transactions persisted.
867
+ */
868
+ initApprovals() {
869
+ const chainId = this.getChainId();
870
+ const unapprovedTxs = this.state.transactions.filter((transaction) => transaction.status === types_1.TransactionStatus.unapproved &&
871
+ transaction.chainId === chainId);
872
+ for (const txMeta of unapprovedTxs) {
873
+ this.processApproval(txMeta, {
874
+ shouldShowRequest: false,
875
+ }).catch((error) => {
876
+ if ((error === null || error === void 0 ? void 0 : error.code) === rpc_errors_1.errorCodes.provider.userRejectedRequest) {
877
+ return;
878
+ }
879
+ console.error('Error during persisted transaction approval', error);
880
+ });
881
+ }
882
+ }
883
+ /**
884
+ * Search transaction metadata for matching entries.
885
+ *
886
+ * @param opts - Options bag.
887
+ * @param opts.searchCriteria - An object containing values or functions for transaction properties to filter transactions with.
888
+ * @param opts.initialList - The transactions to search. Defaults to the current state.
889
+ * @param opts.filterToCurrentNetwork - Whether to filter the results to the current network. Defaults to true.
890
+ * @param opts.limit - The maximum number of transactions to return. No limit by default.
891
+ * @returns An array of transactions matching the provided options.
892
+ */
893
+ getTransactions({ searchCriteria = {}, initialList, filterToCurrentNetwork = true, limit, } = {}) {
894
+ const chainId = this.getChainId();
895
+ // searchCriteria is an object that might have values that aren't predicate
896
+ // methods. When providing any other value type (string, number, etc), we
897
+ // consider this shorthand for "check the value at key for strict equality
898
+ // with the provided value". To conform this object to be only methods, we
899
+ // mapValues (lodash) such that every value on the object is a method that
900
+ // returns a boolean.
901
+ const predicateMethods = (0, lodash_1.mapValues)(searchCriteria, (predicate) => {
902
+ return typeof predicate === 'function'
903
+ ? predicate
904
+ : (v) => v === predicate;
905
+ });
906
+ const transactionsToFilter = initialList !== null && initialList !== void 0 ? initialList : this.state.transactions;
907
+ // Combine sortBy and pickBy to transform our state object into an array of
908
+ // matching transactions that are sorted by time.
909
+ const filteredTransactions = (0, lodash_1.sortBy)((0, lodash_1.pickBy)(transactionsToFilter, (transaction) => {
910
+ if (filterToCurrentNetwork && transaction.chainId !== chainId) {
911
+ return false;
912
+ }
913
+ // iterate over the predicateMethods keys to check if the transaction
914
+ // matches the searchCriteria
915
+ for (const [key, predicate] of Object.entries(predicateMethods)) {
916
+ // We return false early as soon as we know that one of the specified
917
+ // search criteria do not match the transaction. This prevents
918
+ // needlessly checking all criteria when we already know the criteria
919
+ // are not fully satisfied. We check both txParams and the base
920
+ // object as predicate keys can be either.
921
+ if (key in transaction.txParams) {
922
+ if (predicate(transaction.txParams[key]) === false) {
923
+ return false;
924
+ }
925
+ }
926
+ else if (predicate(transaction[key]) === false) {
927
+ return false;
928
+ }
929
+ }
930
+ return true;
931
+ }), 'time');
932
+ if (limit !== undefined) {
933
+ // We need to have all transactions of a given nonce in order to display
934
+ // necessary details in the UI. We use the size of this set to determine
935
+ // whether we have reached the limit provided, thus ensuring that all
936
+ // transactions of nonces we include will be sent to the UI.
937
+ const nonces = new Set();
938
+ const txs = [];
939
+ // By default, the transaction list we filter from is sorted by time ASC.
940
+ // To ensure that filtered results prefers the newest transactions we
941
+ // iterate from right to left, inserting transactions into front of a new
942
+ // array. The original order is preserved, but we ensure that newest txs
943
+ // are preferred.
944
+ for (let i = filteredTransactions.length - 1; i > -1; i--) {
945
+ const txMeta = filteredTransactions[i];
946
+ const { nonce } = txMeta.txParams;
947
+ if (!nonces.has(nonce)) {
948
+ if (nonces.size < limit) {
949
+ nonces.add(nonce);
950
+ }
951
+ else {
952
+ continue;
953
+ }
954
+ }
955
+ // Push transaction into the beginning of our array to ensure the
956
+ // original order is preserved.
957
+ txs.unshift(txMeta);
958
+ }
959
+ return txs;
960
+ }
961
+ return filteredTransactions;
962
+ }
963
+ signExternalTransaction(transactionParams) {
964
+ return __awaiter(this, void 0, void 0, function* () {
965
+ if (!this.sign) {
966
+ throw new Error('No sign method defined.');
967
+ }
968
+ const normalizedTransactionParams = (0, utils_1.normalizeTxParams)(transactionParams);
969
+ const chainId = this.getChainId();
970
+ const type = (0, utils_1.isEIP1559Transaction)(normalizedTransactionParams)
971
+ ? types_1.TransactionEnvelopeType.feeMarket
972
+ : types_1.TransactionEnvelopeType.legacy;
973
+ const updatedTransactionParams = Object.assign(Object.assign({}, normalizedTransactionParams), { type, gasLimit: normalizedTransactionParams.gas, chainId });
974
+ const { from } = updatedTransactionParams;
975
+ const common = this.getCommonConfiguration();
976
+ const unsignedTransaction = tx_1.TransactionFactory.fromTxData(updatedTransactionParams, { common });
977
+ const signedTransaction = yield this.sign(unsignedTransaction, from);
978
+ const rawTransaction = (0, ethereumjs_util_1.bufferToHex)(signedTransaction.serialize());
979
+ return rawTransaction;
980
+ });
981
+ }
982
+ /**
983
+ * Removes unapproved transactions from state.
984
+ */
985
+ clearUnapprovedTransactions() {
986
+ const transactions = this.state.transactions.filter(({ status }) => status !== types_1.TransactionStatus.unapproved);
987
+ this.update({ transactions: this.trimTransactionsForState(transactions) });
988
+ }
989
+ addMetadata(transactionMeta) {
990
+ const { transactions } = this.state;
991
+ transactions.push(transactionMeta);
992
+ this.update({ transactions: this.trimTransactionsForState(transactions) });
993
+ }
994
+ updateGasProperties(transactionMeta) {
995
+ return __awaiter(this, void 0, void 0, function* () {
996
+ const isEIP1559Compatible = (yield this.getEIP1559Compatibility()) &&
997
+ transactionMeta.txParams.type !== types_1.TransactionEnvelopeType.legacy;
998
+ const chainId = this.getChainId();
999
+ yield (0, gas_1.updateGas)({
1000
+ ethQuery: this.ethQuery,
1001
+ providerConfig: this.getNetworkState().providerConfig,
1002
+ txMeta: transactionMeta,
1003
+ });
1004
+ yield (0, gas_fees_1.updateGasFees)({
1005
+ eip1559: isEIP1559Compatible,
1006
+ ethQuery: this.ethQuery,
1007
+ getSavedGasFees: this.getSavedGasFees.bind(this, chainId),
1008
+ getGasFeeEstimates: this.getGasFeeEstimates.bind(this),
1009
+ txMeta: transactionMeta,
1010
+ });
1011
+ });
1012
+ }
1013
+ getCurrentChainTransactionsByStatus(status) {
1014
+ const chainId = this.getChainId();
1015
+ return this.state.transactions.filter((transaction) => transaction.status === status && transaction.chainId === chainId);
1016
+ }
1017
+ onBootCleanup() {
1018
+ this.submitApprovedTransactions();
1019
+ }
1020
+ /**
1021
+ * Create approvals for all unapproved transactions on current chain.
1022
+ */
1023
+ createApprovalsForUnapprovedTransactions() {
1024
+ const unapprovedTransactions = this.getCurrentChainTransactionsByStatus(types_1.TransactionStatus.unapproved);
1025
+ for (const transactionMeta of unapprovedTransactions) {
1026
+ this.processApproval(transactionMeta, {
1027
+ shouldShowRequest: false,
1028
+ }).catch((error) => {
1029
+ if ((error === null || error === void 0 ? void 0 : error.code) === rpc_errors_1.errorCodes.provider.userRejectedRequest) {
1030
+ return;
1031
+ }
1032
+ /* istanbul ignore next */
1033
+ console.error('Error during persisted transaction approval', error);
1034
+ });
1035
+ }
1036
+ }
1037
+ /**
1038
+ * Force to submit approved transactions on current chain.
1039
+ */
1040
+ submitApprovedTransactions() {
1041
+ const approvedTransactions = this.getCurrentChainTransactionsByStatus(types_1.TransactionStatus.approved);
1042
+ for (const transactionMeta of approvedTransactions) {
1043
+ if (this.beforeApproveOnInit(transactionMeta)) {
1044
+ this.approveTransaction(transactionMeta.id).catch((error) => {
1045
+ /* istanbul ignore next */
1046
+ console.error('Error while submitting persisted transaction', error);
1047
+ });
1048
+ }
1049
+ }
1050
+ }
1051
+ processApproval(transactionMeta, { isExisting = false, requireApproval, shouldShowRequest = true, actionId, }) {
626
1052
  return __awaiter(this, void 0, void 0, function* () {
627
1053
  const transactionId = transactionMeta.id;
628
1054
  let resultCallbacks;
@@ -637,21 +1063,33 @@ class TransactionController extends base_controller_1.BaseController {
637
1063
  shouldShowRequest,
638
1064
  });
639
1065
  resultCallbacks = acceptResult.resultCallbacks;
1066
+ if (resultCallbacks) {
1067
+ this.hub.once(`${transactionId}:publish-skip`, () => {
1068
+ resultCallbacks === null || resultCallbacks === void 0 ? void 0 : resultCallbacks.success();
1069
+ // Remove the reference to prevent additional reports once submitted.
1070
+ resultCallbacks = undefined;
1071
+ });
1072
+ }
640
1073
  }
641
1074
  const { isCompleted: isTxCompleted } = this.isTransactionCompleted(transactionId);
642
1075
  if (!isTxCompleted) {
643
1076
  yield this.approveTransaction(transactionId);
1077
+ const updatedTransactionMeta = this.getTransaction(transactionId);
1078
+ this.hub.emit('transaction-approved', {
1079
+ transactionMeta: updatedTransactionMeta,
1080
+ actionId,
1081
+ });
644
1082
  }
645
1083
  }
646
1084
  catch (error) {
647
1085
  const { isCompleted: isTxCompleted } = this.isTransactionCompleted(transactionId);
648
1086
  if (!isTxCompleted) {
649
- if (error.code === rpc_errors_1.errorCodes.provider.userRejectedRequest) {
650
- this.cancelTransaction(transactionId);
651
- throw rpc_errors_1.providerErrors.userRejectedRequest('User rejected the transaction');
1087
+ if ((error === null || error === void 0 ? void 0 : error.code) === rpc_errors_1.errorCodes.provider.userRejectedRequest) {
1088
+ this.cancelTransaction(transactionId, actionId);
1089
+ throw rpc_errors_1.providerErrors.userRejectedRequest('MetaMask Tx Signature: User denied transaction signature.');
652
1090
  }
653
1091
  else {
654
- this.failTransaction(meta, error);
1092
+ this.failTransaction(meta, error, actionId);
655
1093
  }
656
1094
  }
657
1095
  }
@@ -661,10 +1099,6 @@ class TransactionController extends base_controller_1.BaseController {
661
1099
  case types_1.TransactionStatus.failed:
662
1100
  resultCallbacks === null || resultCallbacks === void 0 ? void 0 : resultCallbacks.error(finalMeta.error);
663
1101
  throw rpc_errors_1.rpcErrors.internal(finalMeta.error.message);
664
- case types_1.TransactionStatus.cancelled:
665
- const cancelError = rpc_errors_1.rpcErrors.internal('User cancelled the transaction');
666
- resultCallbacks === null || resultCallbacks === void 0 ? void 0 : resultCallbacks.error(cancelError);
667
- throw cancelError;
668
1102
  case types_1.TransactionStatus.submitted:
669
1103
  resultCallbacks === null || resultCallbacks === void 0 ? void 0 : resultCallbacks.success();
670
1104
  return finalMeta.hash;
@@ -703,7 +1137,10 @@ class TransactionController extends base_controller_1.BaseController {
703
1137
  this.failTransaction(transactionMeta, new Error('No chainId defined.'));
704
1138
  return;
705
1139
  }
706
- const { approved: status } = types_1.TransactionStatus;
1140
+ if (this.inProcessOfSigning.has(transactionId)) {
1141
+ (0, logger_1.projectLogger)('Skipping approval as signing in progress', transactionId);
1142
+ return;
1143
+ }
707
1144
  let nonceToUse = nonce;
708
1145
  // if a nonce already exists on the transactionMeta it means this is a speedup or cancel transaction
709
1146
  // so we want to reuse that nonce and hope that it beats the previous attempt to chain. Otherwise use a new locked nonce
@@ -711,38 +1148,40 @@ class TransactionController extends base_controller_1.BaseController {
711
1148
  nonceLock = yield this.nonceTracker.getNonceLock(from);
712
1149
  nonceToUse = (0, ethereumjs_util_1.addHexPrefix)(nonceLock.nextNonce.toString(16));
713
1150
  }
714
- transactionMeta.status = status;
1151
+ transactionMeta.status = types_1.TransactionStatus.approved;
715
1152
  transactionMeta.txParams.nonce = nonceToUse;
716
1153
  transactionMeta.txParams.chainId = chainId;
717
1154
  const baseTxParams = Object.assign(Object.assign({}, transactionMeta.txParams), { gasLimit: transactionMeta.txParams.gas });
1155
+ this.updateTransaction(transactionMeta, 'TransactionController#approveTransaction - Transaction approved');
1156
+ this.onTransactionStatusChange(transactionMeta);
718
1157
  const isEIP1559 = (0, utils_1.isEIP1559Transaction)(transactionMeta.txParams);
719
1158
  const txParams = isEIP1559
720
- ? Object.assign(Object.assign({}, baseTxParams), { maxFeePerGas: transactionMeta.txParams.maxFeePerGas, maxPriorityFeePerGas: transactionMeta.txParams.maxPriorityFeePerGas, estimatedBaseFee: transactionMeta.txParams.estimatedBaseFee,
721
- // specify type 2 if maxFeePerGas and maxPriorityFeePerGas are set
722
- type: 2 }) : baseTxParams;
723
- // delete gasPrice if maxFeePerGas and maxPriorityFeePerGas are set
724
- if (isEIP1559) {
725
- delete txParams.gasPrice;
1159
+ ? Object.assign(Object.assign({}, baseTxParams), { estimatedBaseFee: transactionMeta.txParams.estimatedBaseFee, type: types_1.TransactionEnvelopeType.feeMarket }) : baseTxParams;
1160
+ const rawTx = yield this.signTransaction(transactionMeta, txParams);
1161
+ if (!this.beforePublish(transactionMeta)) {
1162
+ (0, logger_1.projectLogger)('Skipping publishing transaction based on hook');
1163
+ this.hub.emit(`${transactionMeta.id}:publish-skip`, transactionMeta);
1164
+ return;
1165
+ }
1166
+ if (!rawTx) {
1167
+ return;
726
1168
  }
727
- const unsignedEthTx = this.prepareUnsignedEthTx(txParams);
728
- const signedTx = yield this.sign(unsignedEthTx, from);
729
- yield this.updateTransactionMetaRSV(transactionMeta, signedTx);
730
- transactionMeta.status = types_1.TransactionStatus.signed;
731
- this.updateTransaction(transactionMeta, 'TransactionController#approveTransaction - Transaction signed');
732
- const rawTx = (0, ethereumjs_util_1.bufferToHex)(signedTx.serialize());
733
- transactionMeta.rawTx = rawTx;
734
- this.updateTransaction(transactionMeta, 'TransactionController#approveTransaction - RawTransaction added');
735
- const hash = yield (0, controller_utils_1.query)(this.ethQuery, 'sendRawTransaction', [rawTx]);
1169
+ const hash = yield this.publishTransaction(rawTx);
736
1170
  transactionMeta.hash = hash;
737
1171
  transactionMeta.status = types_1.TransactionStatus.submitted;
738
1172
  transactionMeta.submittedTime = new Date().getTime();
739
1173
  this.updateTransaction(transactionMeta, 'TransactionController#approveTransaction - Transaction submitted');
1174
+ this.hub.emit('transaction-submitted', {
1175
+ transactionMeta,
1176
+ });
740
1177
  this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
1178
+ this.onTransactionStatusChange(transactionMeta);
741
1179
  }
742
1180
  catch (error) {
743
1181
  this.failTransaction(transactionMeta, error);
744
1182
  }
745
1183
  finally {
1184
+ this.inProcessOfSigning.delete(transactionId);
746
1185
  // must set transaction to submitted/failed before releasing lock
747
1186
  if (nonceLock) {
748
1187
  nonceLock.releaseLock();
@@ -751,21 +1190,32 @@ class TransactionController extends base_controller_1.BaseController {
751
1190
  }
752
1191
  });
753
1192
  }
1193
+ publishTransaction(rawTransaction) {
1194
+ return __awaiter(this, void 0, void 0, function* () {
1195
+ return yield (0, controller_utils_1.query)(this.ethQuery, 'sendRawTransaction', [rawTransaction]);
1196
+ });
1197
+ }
754
1198
  /**
755
1199
  * Cancels a transaction based on its ID by setting its status to "rejected"
756
1200
  * and emitting a `<tx.id>:finished` hub event.
757
1201
  *
758
1202
  * @param transactionId - The ID of the transaction to cancel.
1203
+ * @param actionId - The actionId passed from UI
759
1204
  */
760
- cancelTransaction(transactionId) {
1205
+ cancelTransaction(transactionId, actionId) {
761
1206
  const transactionMeta = this.state.transactions.find(({ id }) => id === transactionId);
762
1207
  if (!transactionMeta) {
763
1208
  return;
764
1209
  }
765
1210
  transactionMeta.status = types_1.TransactionStatus.rejected;
766
- this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
767
1211
  const transactions = this.state.transactions.filter(({ id }) => id !== transactionId);
768
1212
  this.update({ transactions: this.trimTransactionsForState(transactions) });
1213
+ this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
1214
+ this.hub.emit('transaction-rejected', {
1215
+ transactionMeta,
1216
+ actionId,
1217
+ });
1218
+ this.onTransactionStatusChange(transactionMeta);
769
1219
  }
770
1220
  /**
771
1221
  * Trim the amount of transactions that are set on the state. Checks
@@ -776,7 +1226,7 @@ class TransactionController extends base_controller_1.BaseController {
776
1226
  * representation, this function will not break apart transactions with the
777
1227
  * same nonce, created on the same day, per network. Not accounting for transactions of the same
778
1228
  * nonce, same day and network combo can result in confusing or broken experiences
779
- * in the UI. The transactions are then updated using the BaseController update.
1229
+ * in the UI. The transactions are then updated using the BaseControllerV1 update.
780
1230
  *
781
1231
  * @param transactions - The transactions to be applied to the state.
782
1232
  * @returns The trimmed list of transactions.
@@ -812,8 +1262,7 @@ class TransactionController extends base_controller_1.BaseController {
812
1262
  isFinalState(status) {
813
1263
  return (status === types_1.TransactionStatus.rejected ||
814
1264
  status === types_1.TransactionStatus.confirmed ||
815
- status === types_1.TransactionStatus.failed ||
816
- status === types_1.TransactionStatus.cancelled);
1265
+ status === types_1.TransactionStatus.failed);
817
1266
  }
818
1267
  /**
819
1268
  * Whether the transaction has at least completed all local processing.
@@ -823,7 +1272,6 @@ class TransactionController extends base_controller_1.BaseController {
823
1272
  */
824
1273
  isLocalFinalState(status) {
825
1274
  return [
826
- types_1.TransactionStatus.cancelled,
827
1275
  types_1.TransactionStatus.confirmed,
828
1276
  types_1.TransactionStatus.failed,
829
1277
  types_1.TransactionStatus.rejected,
@@ -910,10 +1358,6 @@ class TransactionController extends base_controller_1.BaseController {
910
1358
  this.update({ lastFetchedBlockNumbers });
911
1359
  this.hub.emit('incomingTransactionBlock', blockNumber);
912
1360
  }
913
- onPendingTransactionsUpdate(transactions) {
914
- (0, logger_1.pendingTransactionsLogger)('Updated pending transactions');
915
- this.update({ transactions: this.trimTransactionsForState(transactions) });
916
- }
917
1361
  generateDappSuggestedGasFees(txParams, origin) {
918
1362
  if (!origin || origin === controller_utils_1.ORIGIN_METAMASK) {
919
1363
  return undefined;
@@ -946,25 +1390,23 @@ class TransactionController extends base_controller_1.BaseController {
946
1390
  */
947
1391
  addExternalTransaction(transactionMeta) {
948
1392
  var _a, _b;
949
- return __awaiter(this, void 0, void 0, function* () {
950
- const chainId = this.getChainId();
951
- const { transactions } = this.state;
952
- const fromAddress = (_a = transactionMeta === null || transactionMeta === void 0 ? void 0 : transactionMeta.txParams) === null || _a === void 0 ? void 0 : _a.from;
953
- const sameFromAndNetworkTransactions = transactions.filter((transaction) => transaction.txParams.from === fromAddress &&
954
- transaction.chainId === chainId);
955
- const confirmedTxs = sameFromAndNetworkTransactions.filter((transaction) => transaction.status === types_1.TransactionStatus.confirmed);
956
- const pendingTxs = sameFromAndNetworkTransactions.filter((transaction) => transaction.status === types_1.TransactionStatus.submitted);
957
- (0, external_transactions_1.validateConfirmedExternalTransaction)(transactionMeta, confirmedTxs, pendingTxs);
958
- // Make sure provided external transaction has non empty history array
959
- if (!((_b = transactionMeta.history) !== null && _b !== void 0 ? _b : []).length) {
960
- if (!this.isHistoryDisabled) {
961
- (0, history_1.addInitialHistorySnapshot)(transactionMeta);
962
- }
1393
+ const chainId = this.getChainId();
1394
+ const { transactions } = this.state;
1395
+ const fromAddress = (_a = transactionMeta === null || transactionMeta === void 0 ? void 0 : transactionMeta.txParams) === null || _a === void 0 ? void 0 : _a.from;
1396
+ const sameFromAndNetworkTransactions = transactions.filter((transaction) => transaction.txParams.from === fromAddress &&
1397
+ transaction.chainId === chainId);
1398
+ const confirmedTxs = sameFromAndNetworkTransactions.filter((transaction) => transaction.status === types_1.TransactionStatus.confirmed);
1399
+ const pendingTxs = sameFromAndNetworkTransactions.filter((transaction) => transaction.status === types_1.TransactionStatus.submitted);
1400
+ (0, external_transactions_1.validateConfirmedExternalTransaction)(transactionMeta, confirmedTxs, pendingTxs);
1401
+ // Make sure provided external transaction has non empty history array
1402
+ if (!((_b = transactionMeta.history) !== null && _b !== void 0 ? _b : []).length) {
1403
+ if (!this.isHistoryDisabled) {
1404
+ (0, history_1.addInitialHistorySnapshot)(transactionMeta);
963
1405
  }
964
- const updatedTransactions = [...transactions, transactionMeta];
965
- this.update({
966
- transactions: this.trimTransactionsForState(updatedTransactions),
967
- });
1406
+ }
1407
+ const updatedTransactions = [...transactions, transactionMeta];
1408
+ this.update({
1409
+ transactions: this.trimTransactionsForState(updatedTransactions),
968
1410
  });
969
1411
  }
970
1412
  /**
@@ -1005,7 +1447,11 @@ class TransactionController extends base_controller_1.BaseController {
1005
1447
  */
1006
1448
  setTransactionStatusDropped(transactionMeta) {
1007
1449
  transactionMeta.status = types_1.TransactionStatus.dropped;
1450
+ this.hub.emit('transaction-dropped', {
1451
+ transactionMeta,
1452
+ });
1008
1453
  this.updateTransaction(transactionMeta, 'TransactionController#setTransactionStatusDropped - Transaction dropped');
1454
+ this.onTransactionStatusChange(transactionMeta);
1009
1455
  }
1010
1456
  /**
1011
1457
  * Get transaction with provided actionId.
@@ -1046,14 +1492,82 @@ class TransactionController extends base_controller_1.BaseController {
1046
1492
  });
1047
1493
  }
1048
1494
  getEIP1559Compatibility() {
1049
- var _a, _b;
1050
1495
  return __awaiter(this, void 0, void 0, function* () {
1051
1496
  const currentNetworkIsEIP1559Compatible = yield this.getCurrentNetworkEIP1559Compatibility();
1052
- const currentAccountIsEIP1559Compatible = (_b = (_a = this.getCurrentAccountEIP1559Compatibility) === null || _a === void 0 ? void 0 : _a.call(this)) !== null && _b !== void 0 ? _b : true;
1497
+ const currentAccountIsEIP1559Compatible = yield this.getCurrentAccountEIP1559Compatibility();
1053
1498
  return (currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible);
1054
1499
  });
1055
1500
  }
1501
+ addPendingTransactionTrackerListeners() {
1502
+ this.pendingTransactionTracker.hub.on('transaction-confirmed', this.onConfirmedTransaction.bind(this));
1503
+ this.pendingTransactionTracker.hub.on('transaction-dropped', this.setTransactionStatusDropped.bind(this));
1504
+ this.pendingTransactionTracker.hub.on('transaction-failed', this.failTransaction.bind(this));
1505
+ this.pendingTransactionTracker.hub.on('transaction-updated', this.updateTransaction.bind(this));
1506
+ }
1507
+ signTransaction(transactionMeta, txParams) {
1508
+ var _a;
1509
+ return __awaiter(this, void 0, void 0, function* () {
1510
+ (0, logger_1.projectLogger)('Signing transaction', txParams);
1511
+ const unsignedEthTx = this.prepareUnsignedEthTx(txParams);
1512
+ this.inProcessOfSigning.add(transactionMeta.id);
1513
+ const signedTx = yield ((_a = this.sign) === null || _a === void 0 ? void 0 : _a.call(this, unsignedEthTx, txParams.from, ...this.getAdditionalSignArguments(transactionMeta)));
1514
+ if (!signedTx) {
1515
+ (0, logger_1.projectLogger)('Skipping signed status as no signed transaction');
1516
+ return undefined;
1517
+ }
1518
+ if (!this.afterSign(transactionMeta, signedTx)) {
1519
+ this.updateTransaction(transactionMeta, 'TransactionController#signTransaction - Update after sign');
1520
+ (0, logger_1.projectLogger)('Skipping signed status based on hook');
1521
+ return undefined;
1522
+ }
1523
+ yield this.updateTransactionMetaRSV(transactionMeta, signedTx);
1524
+ transactionMeta.status = types_1.TransactionStatus.signed;
1525
+ this.updateTransaction(transactionMeta, 'TransactionController#approveTransaction - Transaction signed');
1526
+ this.onTransactionStatusChange(transactionMeta);
1527
+ const rawTx = (0, ethereumjs_util_1.bufferToHex)(signedTx.serialize());
1528
+ transactionMeta.rawTx = rawTx;
1529
+ this.updateTransaction(transactionMeta, 'TransactionController#approveTransaction - RawTransaction added');
1530
+ return rawTx;
1531
+ });
1532
+ }
1533
+ onTransactionStatusChange(transactionMeta) {
1534
+ this.hub.emit('transaction-status-update', { transactionMeta });
1535
+ }
1536
+ getNonceTrackerTransactions(status, address) {
1537
+ const currentChainId = this.getChainId();
1538
+ return (0, utils_1.getAndFormatTransactionsForNonceTracker)(currentChainId, address, status, this.state.transactions);
1539
+ }
1540
+ onConfirmedTransaction(transactionMeta) {
1541
+ (0, logger_1.projectLogger)('Processing confirmed transaction', transactionMeta.id);
1542
+ this.hub.emit('transaction-confirmed', { transactionMeta });
1543
+ this.hub.emit(`${transactionMeta.id}:confirmed`, transactionMeta);
1544
+ this.onTransactionStatusChange(transactionMeta);
1545
+ // Intentional given potential duration of process.
1546
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1547
+ this.updatePostBalance(transactionMeta);
1548
+ }
1549
+ updatePostBalance(transactionMeta) {
1550
+ return __awaiter(this, void 0, void 0, function* () {
1551
+ try {
1552
+ if (transactionMeta.type !== types_1.TransactionType.swap) {
1553
+ return;
1554
+ }
1555
+ const { updatedTransactionMeta, approvalTransactionMeta } = yield (0, swaps_1.updatePostTransactionBalance)(transactionMeta, {
1556
+ ethQuery: this.ethQuery,
1557
+ getTransaction: this.getTransaction.bind(this),
1558
+ updateTransaction: this.updateTransaction.bind(this),
1559
+ });
1560
+ this.hub.emit('post-transaction-balance-updated', {
1561
+ transactionMeta: updatedTransactionMeta,
1562
+ approvalTransactionMeta,
1563
+ });
1564
+ }
1565
+ catch (error) {
1566
+ /* istanbul ignore next */
1567
+ (0, logger_1.projectLogger)('Error while updating post transaction balance', error);
1568
+ }
1569
+ });
1570
+ }
1056
1571
  }
1057
1572
  exports.TransactionController = TransactionController;
1058
- exports.default = TransactionController;
1059
1573
  //# sourceMappingURL=TransactionController.js.map