@metamask-previews/transaction-controller 16.0.0-preview.08312b7 → 16.0.0-preview.4ae7d84

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.
@@ -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");
@@ -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
@@ -107,7 +120,9 @@ class TransactionController extends base_controller_1.BaseController {
107
120
  this.ethQuery = new eth_query_1.default(provider);
108
121
  this.isSendFlowHistoryDisabled = disableSendFlowHistory !== null && disableSendFlowHistory !== void 0 ? disableSendFlowHistory : false;
109
122
  this.isHistoryDisabled = disableHistory !== null && disableHistory !== void 0 ? disableHistory : false;
123
+ this.isSwapsDisabled = disableSwaps !== null && disableSwaps !== void 0 ? disableSwaps : false;
110
124
  this.registry = new eth_method_registry_1.default({ provider });
125
+ this.getSavedGasFees = getSavedGasFees !== null && getSavedGasFees !== void 0 ? getSavedGasFees : ((_chainId) => undefined);
111
126
  this.getCurrentAccountEIP1559Compatibility =
112
127
  getCurrentAccountEIP1559Compatibility;
113
128
  this.getCurrentNetworkEIP1559Compatibility =
@@ -117,6 +132,15 @@ class TransactionController extends base_controller_1.BaseController {
117
132
  this.getPermittedAccounts = getPermittedAccounts;
118
133
  this.getSelectedAddress = getSelectedAddress;
119
134
  this.securityProviderRequest = securityProviderRequest;
135
+ this.afterSign = (_a = hooks === null || hooks === void 0 ? void 0 : hooks.afterSign) !== null && _a !== void 0 ? _a : (() => true);
136
+ this.beforeApproveOnInit = (_b = hooks === null || hooks === void 0 ? void 0 : hooks.beforeApproveOnInit) !== null && _b !== void 0 ? _b : (() => true);
137
+ this.beforeCheckPendingTransaction =
138
+ (_c = hooks === null || hooks === void 0 ? void 0 : hooks.beforeCheckPendingTransaction) !== null && _c !== void 0 ? _c :
139
+ /* istanbul ignore next */
140
+ (() => true);
141
+ this.beforePublish = (_d = hooks === null || hooks === void 0 ? void 0 : hooks.beforePublish) !== null && _d !== void 0 ? _d : (() => true);
142
+ this.getAdditionalSignArguments =
143
+ (_e = hooks === null || hooks === void 0 ? void 0 : hooks.getAdditionalSignArguments) !== null && _e !== void 0 ? _e : (() => []);
120
144
  this.nonceTracker = new nonce_tracker_1.default({
121
145
  provider,
122
146
  blockTracker,
@@ -139,24 +163,36 @@ 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
182
  // @ts-expect-error TODO: Provider type alignment
153
183
  this.ethQuery = new eth_query_1.default(this.provider);
154
184
  this.registry = new eth_method_registry_1.default({ provider: this.provider });
185
+ this.onBootCleanup();
155
186
  });
156
- this.pendingTransactionTracker.start();
187
+ this.onBootCleanup();
157
188
  }
158
- failTransaction(transactionMeta, error) {
159
- const newTransactionMeta = Object.assign(Object.assign({}, transactionMeta), { error, status: types_1.TransactionStatus.failed });
189
+ failTransaction(transactionMeta, error, actionId) {
190
+ const newTransactionMeta = Object.assign(Object.assign({}, transactionMeta), { error: (0, utils_1.normalizeTxError)(error), status: types_1.TransactionStatus.failed });
191
+ this.hub.emit('transaction-failed', {
192
+ actionId,
193
+ error: error.message,
194
+ transactionMeta: newTransactionMeta,
195
+ });
160
196
  this.updateTransaction(newTransactionMeta, 'TransactionController#failTransaction - Add error message and set status to failed');
161
197
  this.hub.emit(`${transactionMeta.id}:finished`, newTransactionMeta);
162
198
  }
@@ -208,9 +244,12 @@ 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
254
  const chainId = this.getChainId();
216
255
  const { transactions } = this.state;
@@ -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,6 +294,11 @@ class TransactionController extends base_controller_1.BaseController {
265
294
  if (!this.isHistoryDisabled) {
266
295
  (0, history_1.addInitialHistorySnapshot)(transactionMeta);
267
296
  }
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),
301
+ });
268
302
  transactions.push(transactionMeta);
269
303
  this.update({
270
304
  transactions: this.trimTransactionsForState(transactions),
@@ -275,6 +309,7 @@ class TransactionController extends base_controller_1.BaseController {
275
309
  result: this.processApproval(transactionMeta, {
276
310
  isExisting: Boolean(existingTransactionMeta),
277
311
  requireApproval,
312
+ actionId,
278
313
  }),
279
314
  transactionMeta,
280
315
  };
@@ -291,22 +326,6 @@ class TransactionController extends base_controller_1.BaseController {
291
326
  yield this.incomingTransactionHelper.update();
292
327
  });
293
328
  }
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
329
  /**
311
330
  * Attempts to cancel a transaction based on its ID by setting its status to "rejected"
312
331
  * and emitting a `<tx.id>:finished` hub event.
@@ -314,9 +333,10 @@ class TransactionController extends base_controller_1.BaseController {
314
333
  * @param transactionId - The ID of the transaction to cancel.
315
334
  * @param gasValues - The gas values to use for the cancellation transaction.
316
335
  * @param options - The options for the cancellation transaction.
336
+ * @param options.actionId - Unique ID to prevent duplicate requests.
317
337
  * @param options.estimatedBaseFee - The estimated base fee of the transaction.
318
338
  */
319
- stopTransaction(transactionId, gasValues, { estimatedBaseFee } = {}) {
339
+ stopTransaction(transactionId, gasValues, { estimatedBaseFee, actionId, } = {}) {
320
340
  var _a, _b;
321
341
  return __awaiter(this, void 0, void 0, function* () {
322
342
  if (gasValues) {
@@ -355,7 +375,7 @@ class TransactionController extends base_controller_1.BaseController {
355
375
  gasLimit: transactionMeta.txParams.gas,
356
376
  maxFeePerGas: newMaxFeePerGas,
357
377
  maxPriorityFeePerGas: newMaxPriorityFeePerGas,
358
- type: 2,
378
+ type: '2',
359
379
  nonce: transactionMeta.txParams.nonce,
360
380
  to: transactionMeta.txParams.from,
361
381
  value: '0x0',
@@ -375,6 +395,9 @@ class TransactionController extends base_controller_1.BaseController {
375
395
  yield (0, controller_utils_1.query)(this.ethQuery, 'sendRawTransaction', [rawTx]);
376
396
  transactionMeta.estimatedBaseFee = estimatedBaseFee;
377
397
  transactionMeta.status = types_1.TransactionStatus.cancelled;
398
+ // stopTransaction has no approval request, so we assume the user has already approved the transaction
399
+ this.hub.emit('transaction-approved', { transactionMeta, actionId });
400
+ this.hub.emit('transaction-submitted', { transactionMeta, actionId });
378
401
  this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
379
402
  });
380
403
  }
@@ -428,7 +451,7 @@ class TransactionController extends base_controller_1.BaseController {
428
451
  (0, utils_1.validateMinimumIncrease)(maxPriorityFeePerGasValues, minMaxPriorityFeePerGas)) ||
429
452
  (existingMaxPriorityFeePerGas && minMaxPriorityFeePerGas);
430
453
  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 });
454
+ ? 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 });
432
455
  const unsignedEthTx = this.prepareUnsignedEthTx(txParams);
433
456
  const signedTx = yield this.sign(unsignedEthTx, transactionMeta.txParams.from);
434
457
  yield this.updateTransactionMetaRSV(transactionMeta, signedTx);
@@ -440,6 +463,15 @@ class TransactionController extends base_controller_1.BaseController {
440
463
  ? 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
464
  transactions.push(newTransactionMeta);
442
465
  this.update({ transactions: this.trimTransactionsForState(transactions) });
466
+ // speedUpTransaction has no approval request, so we assume the user has already approved the transaction
467
+ this.hub.emit('transaction-approved', {
468
+ transactionMeta: newTransactionMeta,
469
+ actionId,
470
+ });
471
+ this.hub.emit('transaction-submitted', {
472
+ transactionMeta: newTransactionMeta,
473
+ actionId,
474
+ });
443
475
  this.hub.emit(`${transactionMeta.id}:speedup`, newTransactionMeta);
444
476
  });
445
477
  }
@@ -529,6 +561,26 @@ class TransactionController extends base_controller_1.BaseController {
529
561
  this.markNonceDuplicatesDropped(transactionId);
530
562
  // Update external provided transaction with updated gas values and confirmed status.
531
563
  this.updateTransaction(transactionMeta, 'TransactionController:confirmExternalTransaction - Add external transaction');
564
+ if (transactionMeta.type === types_1.TransactionType.swap) {
565
+ (0, swaps_1.updatePostTransactionBalance)(transactionMeta, {
566
+ ethQuery: this.ethQuery,
567
+ getTransaction: this.getTransaction.bind(this),
568
+ updateTransaction: this.updateTransaction.bind(this),
569
+ })
570
+ .then(({ updatedTransactionMeta, approvalTransactionMeta }) => {
571
+ this.hub.emit('post-transaction-balance-updated', {
572
+ transactionMeta: updatedTransactionMeta,
573
+ approvalTransactionMeta,
574
+ });
575
+ })
576
+ .catch((error) => {
577
+ /* istanbul ignore next */
578
+ (0, logger_1.projectLogger)('Error while updating post transaction balance', error);
579
+ });
580
+ }
581
+ this.hub.emit('transaction-confirmed', {
582
+ transactionMeta,
583
+ });
532
584
  }
533
585
  catch (error) {
534
586
  console.error(error);
@@ -610,6 +662,36 @@ class TransactionController extends base_controller_1.BaseController {
610
662
  this.updateTransaction(updatedMeta, 'TransactionController:updateTransactionGasFees - gas values updated');
611
663
  return this.getTransaction(transactionId);
612
664
  }
665
+ /**
666
+ * Update the previous gas values of a transaction.
667
+ *
668
+ * @param transactionId - The ID of the transaction to update.
669
+ * @param previousGas - Previous gas values to update.
670
+ * @param previousGas.gasLimit - Maxmimum number of units of gas to use for this transaction.
671
+ * @param previousGas.maxFeePerGas - Maximum amount per gas to pay for the transaction, including the priority fee.
672
+ * @param previousGas.maxPriorityFeePerGas - Maximum amount per gas to give to validator as incentive.
673
+ * @returns The updated transactionMeta.
674
+ */
675
+ updatePreviousGasParams(transactionId, { gasLimit, maxFeePerGas, maxPriorityFeePerGas, }) {
676
+ const transactionMeta = this.getTransaction(transactionId);
677
+ if (!transactionMeta) {
678
+ throw new Error(`Cannot update transaction as no transaction metadata found`);
679
+ }
680
+ (0, utils_1.validateIfTransactionUnapproved)(transactionMeta, 'updatePreviousGasParams');
681
+ const transactionPreviousGas = {
682
+ previousGas: {
683
+ gasLimit,
684
+ maxFeePerGas,
685
+ maxPriorityFeePerGas,
686
+ },
687
+ };
688
+ // only update what is defined
689
+ transactionPreviousGas.previousGas = (0, lodash_1.pickBy)(transactionPreviousGas.previousGas);
690
+ // merge updated previous gas values with existing transaction meta
691
+ const updatedMeta = (0, lodash_1.merge)(transactionMeta, transactionPreviousGas);
692
+ this.updateTransaction(updatedMeta, 'TransactionController:updatePreviousGasParams - Previous gas values updated');
693
+ return this.getTransaction(transactionId);
694
+ }
613
695
  /**
614
696
  * Gets the next nonce according to the nonce-tracker.
615
697
  * Ensure `releaseLock` is called once processing of the `nonce` value is complete.
@@ -622,7 +704,158 @@ class TransactionController extends base_controller_1.BaseController {
622
704
  return this.nonceTracker.getNonceLock(address);
623
705
  });
624
706
  }
625
- processApproval(transactionMeta, { isExisting = false, requireApproval, shouldShowRequest = true, }) {
707
+ /**
708
+ * Signs and returns the raw transaction data for provided transaction params list.
709
+ *
710
+ * @param listOfTxParams - The list of transaction params to approve.
711
+ * @returns The raw transactions.
712
+ */
713
+ approveTransactionsWithSameNonce(listOfTxParams = []) {
714
+ return __awaiter(this, void 0, void 0, function* () {
715
+ if (listOfTxParams.length === 0) {
716
+ return '';
717
+ }
718
+ const initialTx = listOfTxParams[0];
719
+ const common = this.getCommonConfiguration();
720
+ const initialTxAsEthTx = tx_1.TransactionFactory.fromTxData(initialTx, {
721
+ common,
722
+ });
723
+ const initialTxAsSerializedHex = (0, ethereumjs_util_1.bufferToHex)(initialTxAsEthTx.serialize());
724
+ if (this.inProcessOfSigning.has(initialTxAsSerializedHex)) {
725
+ return '';
726
+ }
727
+ this.inProcessOfSigning.add(initialTxAsSerializedHex);
728
+ let rawTransactions, nonceLock;
729
+ try {
730
+ // TODO: we should add a check to verify that all transactions have the same from address
731
+ const fromAddress = initialTx.from;
732
+ nonceLock = yield this.nonceTracker.getNonceLock(fromAddress);
733
+ const nonce = nonceLock.nextNonce;
734
+ rawTransactions = yield Promise.all(listOfTxParams.map((txParams) => {
735
+ txParams.nonce = (0, ethereumjs_util_1.addHexPrefix)(nonce.toString(16));
736
+ return this.signExternalTransaction(txParams);
737
+ }));
738
+ }
739
+ catch (err) {
740
+ (0, logger_1.projectLogger)('Error while signing transactions with same nonce', err);
741
+ // Must set transaction to submitted/failed before releasing lock
742
+ // continue with error chain
743
+ throw err;
744
+ }
745
+ finally {
746
+ if (nonceLock) {
747
+ nonceLock.releaseLock();
748
+ }
749
+ this.inProcessOfSigning.delete(initialTxAsSerializedHex);
750
+ }
751
+ return rawTransactions;
752
+ });
753
+ }
754
+ signExternalTransaction(transactionParams) {
755
+ return __awaiter(this, void 0, void 0, function* () {
756
+ if (!this.sign) {
757
+ throw new Error('No sign method defined.');
758
+ }
759
+ const normalizedtransactionParams = (0, utils_1.normalizeTxParams)(transactionParams);
760
+ const chainId = this.getChainId();
761
+ const type = (0, utils_1.isEIP1559Transaction)(normalizedtransactionParams)
762
+ ? types_1.TransactionEnvelopeType.feeMarket
763
+ : types_1.TransactionEnvelopeType.legacy;
764
+ const updatedTransactionParams = Object.assign(Object.assign({}, normalizedtransactionParams), { type, gasLimit: normalizedtransactionParams.gas, chainId });
765
+ const { from } = updatedTransactionParams;
766
+ const common = this.getCommonConfiguration();
767
+ const unsignedTransaction = tx_1.TransactionFactory.fromTxData(updatedTransactionParams, { common });
768
+ const signedTransaction = yield this.sign(unsignedTransaction, from);
769
+ const rawTransaction = (0, ethereumjs_util_1.bufferToHex)(signedTransaction.serialize());
770
+ return rawTransaction;
771
+ });
772
+ }
773
+ /**
774
+ * Removes unapproved transactions from state.
775
+ */
776
+ clearUnapprovedTransactions() {
777
+ const transactions = this.state.transactions.filter(({ status }) => status !== types_1.TransactionStatus.unapproved);
778
+ this.update({ transactions: this.trimTransactionsForState(transactions) });
779
+ }
780
+ updateGasProperties(transactionMeta) {
781
+ return __awaiter(this, void 0, void 0, function* () {
782
+ const isEIP1559Compatible = yield this.getEIP1559Compatibility();
783
+ const chainId = this.getChainId();
784
+ yield (0, gas_1.updateGas)({
785
+ ethQuery: this.ethQuery,
786
+ providerConfig: this.getNetworkState().providerConfig,
787
+ txMeta: transactionMeta,
788
+ });
789
+ yield (0, gas_fees_1.updateGasFees)({
790
+ eip1559: isEIP1559Compatible,
791
+ ethQuery: this.ethQuery,
792
+ getSavedGasFees: this.getSavedGasFees.bind(this, chainId),
793
+ getGasFeeEstimates: this.getGasFeeEstimates.bind(this),
794
+ txMeta: transactionMeta,
795
+ });
796
+ });
797
+ }
798
+ getCurrentChainTransactionsByStatus(status) {
799
+ const chainId = this.getChainId();
800
+ return this.state.transactions.filter((transaction) => transaction.status === status && transaction.chainId === chainId);
801
+ }
802
+ onBootCleanup() {
803
+ this.createApprovalsForUnapprovedTransactions();
804
+ this.loadGasValuesForUnapprovedTransactions();
805
+ this.submitApprovedTransactions();
806
+ }
807
+ /**
808
+ * Create approvals for all unapproved transactions on current chain.
809
+ */
810
+ createApprovalsForUnapprovedTransactions() {
811
+ const unapprovedTransactions = this.getCurrentChainTransactionsByStatus(types_1.TransactionStatus.unapproved);
812
+ for (const transactionMeta of unapprovedTransactions) {
813
+ this.processApproval(transactionMeta, {
814
+ shouldShowRequest: false,
815
+ }).catch((error) => {
816
+ if ((error === null || error === void 0 ? void 0 : error.code) === rpc_errors_1.errorCodes.provider.userRejectedRequest) {
817
+ return;
818
+ }
819
+ /* istanbul ignore next */
820
+ console.error('Error during persisted transaction approval', error);
821
+ });
822
+ }
823
+ }
824
+ /**
825
+ * Update the gas values of all unapproved transactions on current chain.
826
+ */
827
+ loadGasValuesForUnapprovedTransactions() {
828
+ return __awaiter(this, void 0, void 0, function* () {
829
+ const unapprovedTransactions = this.getCurrentChainTransactionsByStatus(types_1.TransactionStatus.unapproved);
830
+ const results = yield Promise.allSettled(unapprovedTransactions.map((transactionMeta) => __awaiter(this, void 0, void 0, function* () {
831
+ yield this.updateGasProperties(transactionMeta);
832
+ this.updateTransaction(transactionMeta, 'TransactionController:loadGasValuesForUnapprovedTransactions - Gas values updated');
833
+ })));
834
+ for (const [index, result] of results.entries()) {
835
+ if (result.status === 'rejected') {
836
+ const transactionMeta = unapprovedTransactions[index];
837
+ this.failTransaction(transactionMeta, result.reason);
838
+ /* istanbul ignore next */
839
+ console.error('Error while loading gas values for persisted transaction id: ', transactionMeta.id, result.reason);
840
+ }
841
+ }
842
+ });
843
+ }
844
+ /**
845
+ * Force to submit approved transactions on current chain.
846
+ */
847
+ submitApprovedTransactions() {
848
+ const approvedTransactions = this.getCurrentChainTransactionsByStatus(types_1.TransactionStatus.approved);
849
+ for (const transactionMeta of approvedTransactions) {
850
+ if (this.beforeApproveOnInit(transactionMeta)) {
851
+ this.approveTransaction(transactionMeta.id).catch((error) => {
852
+ /* istanbul ignore next */
853
+ console.error('Error while submitting persisted transaction', error);
854
+ });
855
+ }
856
+ }
857
+ }
858
+ processApproval(transactionMeta, { isExisting = false, requireApproval, shouldShowRequest = true, actionId, }) {
626
859
  return __awaiter(this, void 0, void 0, function* () {
627
860
  const transactionId = transactionMeta.id;
628
861
  let resultCallbacks;
@@ -641,17 +874,22 @@ class TransactionController extends base_controller_1.BaseController {
641
874
  const { isCompleted: isTxCompleted } = this.isTransactionCompleted(transactionId);
642
875
  if (!isTxCompleted) {
643
876
  yield this.approveTransaction(transactionId);
877
+ const updatedTransactionMeta = this.getTransaction(transactionId);
878
+ this.hub.emit('transaction-approved', {
879
+ transactionMeta: updatedTransactionMeta,
880
+ actionId,
881
+ });
644
882
  }
645
883
  }
646
884
  catch (error) {
647
885
  const { isCompleted: isTxCompleted } = this.isTransactionCompleted(transactionId);
648
886
  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');
887
+ if ((error === null || error === void 0 ? void 0 : error.code) === rpc_errors_1.errorCodes.provider.userRejectedRequest) {
888
+ this.cancelTransaction(transactionId, actionId);
889
+ throw rpc_errors_1.providerErrors.userRejectedRequest('MetaMask Tx Signature: User denied transaction signature.');
652
890
  }
653
891
  else {
654
- this.failTransaction(meta, error);
892
+ this.failTransaction(meta, error, actionId);
655
893
  }
656
894
  }
657
895
  }
@@ -703,6 +941,10 @@ class TransactionController extends base_controller_1.BaseController {
703
941
  this.failTransaction(transactionMeta, new Error('No chainId defined.'));
704
942
  return;
705
943
  }
944
+ if (this.inProcessOfSigning.has(transactionId)) {
945
+ (0, logger_1.projectLogger)('Skipping approval as signing in progress', transactionId);
946
+ return;
947
+ }
706
948
  const { approved: status } = types_1.TransactionStatus;
707
949
  let nonceToUse = nonce;
708
950
  // if a nonce already exists on the transactionMeta it means this is a speedup or cancel transaction
@@ -715,27 +957,31 @@ class TransactionController extends base_controller_1.BaseController {
715
957
  transactionMeta.txParams.nonce = nonceToUse;
716
958
  transactionMeta.txParams.chainId = chainId;
717
959
  const baseTxParams = Object.assign(Object.assign({}, transactionMeta.txParams), { gasLimit: transactionMeta.txParams.gas });
960
+ this.updateTransaction(transactionMeta, 'TransactionController#approveTransaction - Transaction approved');
718
961
  const isEIP1559 = (0, utils_1.isEIP1559Transaction)(transactionMeta.txParams);
719
962
  const txParams = isEIP1559
720
963
  ? Object.assign(Object.assign({}, baseTxParams), { maxFeePerGas: transactionMeta.txParams.maxFeePerGas, maxPriorityFeePerGas: transactionMeta.txParams.maxPriorityFeePerGas, estimatedBaseFee: transactionMeta.txParams.estimatedBaseFee,
721
964
  // specify type 2 if maxFeePerGas and maxPriorityFeePerGas are set
722
- type: 2 }) : baseTxParams;
965
+ type: '2' }) : baseTxParams;
723
966
  // delete gasPrice if maxFeePerGas and maxPriorityFeePerGas are set
724
967
  if (isEIP1559) {
725
968
  delete txParams.gasPrice;
726
969
  }
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]);
970
+ const rawTx = yield this.signTransaction(transactionMeta);
971
+ if (!this.beforePublish(transactionMeta)) {
972
+ (0, logger_1.projectLogger)('Skipping publishing transaction based on hook');
973
+ return;
974
+ }
975
+ if (!rawTx) {
976
+ return;
977
+ }
978
+ const hash = yield this.publishTransaction(rawTx);
736
979
  transactionMeta.hash = hash;
737
980
  transactionMeta.status = types_1.TransactionStatus.submitted;
738
981
  transactionMeta.submittedTime = new Date().getTime();
982
+ this.hub.emit('transaction-submitted', {
983
+ transactionMeta,
984
+ });
739
985
  this.updateTransaction(transactionMeta, 'TransactionController#approveTransaction - Transaction submitted');
740
986
  this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
741
987
  }
@@ -743,6 +989,7 @@ class TransactionController extends base_controller_1.BaseController {
743
989
  this.failTransaction(transactionMeta, error);
744
990
  }
745
991
  finally {
992
+ this.inProcessOfSigning.delete(transactionId);
746
993
  // must set transaction to submitted/failed before releasing lock
747
994
  if (nonceLock) {
748
995
  nonceLock.releaseLock();
@@ -751,19 +998,29 @@ class TransactionController extends base_controller_1.BaseController {
751
998
  }
752
999
  });
753
1000
  }
1001
+ publishTransaction(rawTransaction) {
1002
+ return __awaiter(this, void 0, void 0, function* () {
1003
+ return yield (0, controller_utils_1.query)(this.ethQuery, 'sendRawTransaction', [rawTransaction]);
1004
+ });
1005
+ }
754
1006
  /**
755
1007
  * Cancels a transaction based on its ID by setting its status to "rejected"
756
1008
  * and emitting a `<tx.id>:finished` hub event.
757
1009
  *
758
1010
  * @param transactionId - The ID of the transaction to cancel.
1011
+ * @param actionId - The actionId passed from UI
759
1012
  */
760
- cancelTransaction(transactionId) {
1013
+ cancelTransaction(transactionId, actionId) {
761
1014
  const transactionMeta = this.state.transactions.find(({ id }) => id === transactionId);
762
1015
  if (!transactionMeta) {
763
1016
  return;
764
1017
  }
765
1018
  transactionMeta.status = types_1.TransactionStatus.rejected;
766
1019
  this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
1020
+ this.hub.emit('transaction-rejected', {
1021
+ transactionMeta,
1022
+ actionId,
1023
+ });
767
1024
  const transactions = this.state.transactions.filter(({ id }) => id !== transactionId);
768
1025
  this.update({ transactions: this.trimTransactionsForState(transactions) });
769
1026
  }
@@ -910,10 +1167,6 @@ class TransactionController extends base_controller_1.BaseController {
910
1167
  this.update({ lastFetchedBlockNumbers });
911
1168
  this.hub.emit('incomingTransactionBlock', blockNumber);
912
1169
  }
913
- onPendingTransactionsUpdate(transactions) {
914
- (0, logger_1.pendingTransactionsLogger)('Updated pending transactions');
915
- this.update({ transactions: this.trimTransactionsForState(transactions) });
916
- }
917
1170
  generateDappSuggestedGasFees(txParams, origin) {
918
1171
  if (!origin || origin === controller_utils_1.ORIGIN_METAMASK) {
919
1172
  return undefined;
@@ -1005,6 +1258,9 @@ class TransactionController extends base_controller_1.BaseController {
1005
1258
  */
1006
1259
  setTransactionStatusDropped(transactionMeta) {
1007
1260
  transactionMeta.status = types_1.TransactionStatus.dropped;
1261
+ this.hub.emit('transaction-dropped', {
1262
+ transactionMeta,
1263
+ });
1008
1264
  this.updateTransaction(transactionMeta, 'TransactionController#setTransactionStatusDropped - Transaction dropped');
1009
1265
  }
1010
1266
  /**
@@ -1053,6 +1309,39 @@ class TransactionController extends base_controller_1.BaseController {
1053
1309
  return (currentNetworkIsEIP1559Compatible && currentAccountIsEIP1559Compatible);
1054
1310
  });
1055
1311
  }
1312
+ addPendingTransactionTrackerListeners() {
1313
+ this.pendingTransactionTracker.hub.on('transaction-confirmed', (transactionMeta) => {
1314
+ this.hub.emit('transaction-confirmed', { transactionMeta });
1315
+ this.hub.emit(`${transactionMeta.id}:confirmed`, transactionMeta);
1316
+ });
1317
+ this.pendingTransactionTracker.hub.on('transaction-dropped', this.setTransactionStatusDropped.bind(this));
1318
+ this.pendingTransactionTracker.hub.on('transaction-failed', this.failTransaction.bind(this));
1319
+ this.pendingTransactionTracker.hub.on('transaction-updated', this.updateTransaction.bind(this));
1320
+ }
1321
+ signTransaction(transactionMeta) {
1322
+ var _a;
1323
+ return __awaiter(this, void 0, void 0, function* () {
1324
+ const { txParams } = transactionMeta;
1325
+ const unsignedEthTx = this.prepareUnsignedEthTx(txParams);
1326
+ this.inProcessOfSigning.add(transactionMeta.id);
1327
+ const signedTx = yield ((_a = this.sign) === null || _a === void 0 ? void 0 : _a.call(this, unsignedEthTx, txParams.from, ...this.getAdditionalSignArguments(transactionMeta)));
1328
+ if (!signedTx) {
1329
+ (0, logger_1.projectLogger)('Skipping signed status as no signed transaction');
1330
+ return undefined;
1331
+ }
1332
+ if (!this.afterSign(transactionMeta, signedTx)) {
1333
+ (0, logger_1.projectLogger)('Skipping signed status based on hook');
1334
+ return undefined;
1335
+ }
1336
+ yield this.updateTransactionMetaRSV(transactionMeta, signedTx);
1337
+ transactionMeta.status = types_1.TransactionStatus.signed;
1338
+ this.updateTransaction(transactionMeta, 'TransactionController#approveTransaction - Transaction signed');
1339
+ const rawTx = (0, ethereumjs_util_1.bufferToHex)(signedTx.serialize());
1340
+ transactionMeta.rawTx = rawTx;
1341
+ this.updateTransaction(transactionMeta, 'TransactionController#approveTransaction - RawTransaction added');
1342
+ return rawTx;
1343
+ });
1344
+ }
1056
1345
  }
1057
1346
  exports.TransactionController = TransactionController;
1058
1347
  exports.default = TransactionController;