@metamask-previews/transaction-controller 9.1.0-preview.04c2e62 → 9.2.0-preview.b963821

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.
@@ -26,6 +26,7 @@ const events_1 = require("events");
26
26
  const nonce_tracker_1 = __importDefault(require("nonce-tracker"));
27
27
  const uuid_1 = require("uuid");
28
28
  const EtherscanRemoteTransactionSource_1 = require("./EtherscanRemoteTransactionSource");
29
+ const external_transactions_1 = require("./external-transactions");
29
30
  const IncomingTransactionHelper_1 = require("./IncomingTransactionHelper");
30
31
  const types_1 = require("./types");
31
32
  const utils_1 = require("./utils");
@@ -176,22 +177,24 @@ class TransactionController extends base_controller_1.BaseController {
176
177
  *
177
178
  * @param transaction - The transaction object to add.
178
179
  * @param opts - Additional options to control how the transaction is added.
180
+ * @param opts.actionId - Unique ID to prevent duplicate requests.
179
181
  * @param opts.deviceConfirmedOn - An enum to indicate what device confirmed the transaction.
180
182
  * @param opts.origin - The origin of the transaction request, such as a dApp hostname.
181
183
  * @param opts.requireApproval - Whether the transaction requires approval by the user, defaults to true unless explicitly disabled.
182
184
  * @param opts.securityAlertResponse - Response from security validator.
183
185
  * @returns Object containing a promise resolving to the transaction hash if approved.
184
186
  */
185
- addTransaction(transaction, { deviceConfirmedOn, origin, requireApproval, securityAlertResponse, } = {}) {
187
+ addTransaction(transaction, { actionId, deviceConfirmedOn, origin, requireApproval, securityAlertResponse, } = {}) {
186
188
  return __awaiter(this, void 0, void 0, function* () {
187
- const { chainId, networkId } = this.getChainAndNetworkId();
189
+ const chainId = this.getChainId();
188
190
  const { transactions } = this.state;
189
191
  transaction = (0, utils_1.normalizeTransaction)(transaction);
190
192
  (0, utils_1.validateTransaction)(transaction);
191
193
  const dappSuggestedGasFees = this.generateDappSuggestedGasFees(transaction, origin);
192
- const transactionMeta = {
194
+ const existingTransactionMeta = this.getTransactionWithActionId(actionId);
195
+ // If a request to add a transaction with the same actionId is submitted again, a new transaction will not be created for it.
196
+ const transactionMeta = existingTransactionMeta || {
193
197
  id: (0, uuid_1.v1)(),
194
- networkID: networkId !== null && networkId !== void 0 ? networkId : undefined,
195
198
  chainId,
196
199
  origin,
197
200
  status: types_1.TransactionStatus.unapproved,
@@ -201,6 +204,8 @@ class TransactionController extends base_controller_1.BaseController {
201
204
  verifiedOnBlockchain: false,
202
205
  dappSuggestedGasFees,
203
206
  securityAlertResponse,
207
+ // Add actionId to txMeta to check if same actionId is seen again
208
+ actionId,
204
209
  };
205
210
  try {
206
211
  const { gas, estimateGasError } = yield this.estimateGas(transaction);
@@ -211,11 +216,17 @@ class TransactionController extends base_controller_1.BaseController {
211
216
  this.failTransaction(transactionMeta, error);
212
217
  return Promise.reject(error);
213
218
  }
214
- transactions.push(transactionMeta);
215
- this.update({ transactions: this.trimTransactionsForState(transactions) });
216
- this.hub.emit(`unapprovedTransaction`, transactionMeta);
219
+ // Checks if a transaction already exists with a given actionId
220
+ if (!existingTransactionMeta) {
221
+ transactions.push(transactionMeta);
222
+ this.update({
223
+ transactions: this.trimTransactionsForState(transactions),
224
+ });
225
+ this.hub.emit(`unapprovedTransaction`, transactionMeta);
226
+ }
217
227
  return {
218
228
  result: this.processApproval(transactionMeta, {
229
+ isExisting: Boolean(existingTransactionMeta),
219
230
  requireApproval,
220
231
  }),
221
232
  transactionMeta,
@@ -237,9 +248,9 @@ class TransactionController extends base_controller_1.BaseController {
237
248
  * Creates approvals for all unapproved transactions persisted.
238
249
  */
239
250
  initApprovals() {
240
- const { networkId, chainId } = this.getChainAndNetworkId();
251
+ const chainId = this.getChainId();
241
252
  const unapprovedTxs = this.state.transactions.filter((transaction) => transaction.status === types_1.TransactionStatus.unapproved &&
242
- (0, utils_1.transactionMatchesNetwork)(transaction, chainId, networkId));
253
+ transaction.chainId === chainId);
243
254
  for (const txMeta of unapprovedTxs) {
244
255
  this.processApproval(txMeta, {
245
256
  shouldShowRequest: false,
@@ -255,8 +266,10 @@ class TransactionController extends base_controller_1.BaseController {
255
266
  *
256
267
  * @param transactionID - The ID of the transaction to cancel.
257
268
  * @param gasValues - The gas values to use for the cancellation transaction.
269
+ * @param options - The options for the cancellation transaction.
270
+ * @param options.estimatedBaseFee - The estimated base fee of the transaction.
258
271
  */
259
- stopTransaction(transactionID, gasValues) {
272
+ stopTransaction(transactionID, gasValues, { estimatedBaseFee } = {}) {
260
273
  var _a, _b;
261
274
  return __awaiter(this, void 0, void 0, function* () {
262
275
  if (gasValues) {
@@ -312,6 +325,7 @@ class TransactionController extends base_controller_1.BaseController {
312
325
  const signedTx = yield this.sign(unsignedEthTx, transactionMeta.transaction.from);
313
326
  const rawTransaction = (0, ethereumjs_util_1.bufferToHex)(signedTx.serialize());
314
327
  yield (0, controller_utils_1.query)(this.ethQuery, 'sendRawTransaction', [rawTransaction]);
328
+ transactionMeta.estimatedBaseFee = estimatedBaseFee;
315
329
  transactionMeta.status = types_1.TransactionStatus.cancelled;
316
330
  this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
317
331
  });
@@ -320,11 +334,18 @@ class TransactionController extends base_controller_1.BaseController {
320
334
  * Attempts to speed up a transaction increasing transaction gasPrice by ten percent.
321
335
  *
322
336
  * @param transactionID - The ID of the transaction to speed up.
323
- * @param gasValues - The gas values to use for the speed up transation.
337
+ * @param gasValues - The gas values to use for the speed up transaction.
338
+ * @param options - The options for the speed up transaction.
339
+ * @param options.actionId - Unique ID to prevent duplicate requests
340
+ * @param options.estimatedBaseFee - The estimated base fee of the transaction.
324
341
  */
325
- speedUpTransaction(transactionID, gasValues) {
342
+ speedUpTransaction(transactionID, gasValues, { actionId, estimatedBaseFee, } = {}) {
326
343
  var _a, _b;
327
344
  return __awaiter(this, void 0, void 0, function* () {
345
+ // If transaction is found for same action id, do not create a new speed up transaction.
346
+ if (this.getTransactionWithActionId(actionId)) {
347
+ return;
348
+ }
328
349
  if (gasValues) {
329
350
  (0, utils_1.validateGasValues)(gasValues);
330
351
  }
@@ -366,7 +387,8 @@ class TransactionController extends base_controller_1.BaseController {
366
387
  const transactionHash = yield (0, controller_utils_1.query)(this.ethQuery, 'sendRawTransaction', [
367
388
  rawTransaction,
368
389
  ]);
369
- const baseTransactionMeta = Object.assign(Object.assign({}, transactionMeta), { id: (0, uuid_1.v1)(), time: Date.now(), transactionHash });
390
+ const baseTransactionMeta = Object.assign(Object.assign({}, transactionMeta), { estimatedBaseFee, id: (0, uuid_1.v1)(), time: Date.now(), transactionHash,
391
+ actionId });
370
392
  const newTransactionMeta = newMaxFeePerGas && newMaxPriorityFeePerGas
371
393
  ? Object.assign(Object.assign({}, baseTransactionMeta), { transaction: Object.assign(Object.assign({}, transactionMeta.transaction), { maxFeePerGas: newMaxFeePerGas, maxPriorityFeePerGas: newMaxPriorityFeePerGas }) }) : Object.assign(Object.assign({}, baseTransactionMeta), { transaction: Object.assign(Object.assign({}, transactionMeta.transaction), { gasPrice: newGasPrice }) });
372
394
  transactions.push(newTransactionMeta);
@@ -452,14 +474,10 @@ class TransactionController extends base_controller_1.BaseController {
452
474
  queryTransactionStatuses() {
453
475
  return __awaiter(this, void 0, void 0, function* () {
454
476
  const { transactions } = this.state;
455
- const { chainId: currentChainId, networkId: currentNetworkID } = this.getChainAndNetworkId();
477
+ const currentChainId = this.getChainId();
456
478
  let gotUpdates = false;
457
479
  yield (0, controller_utils_1.safelyExecute)(() => Promise.all(transactions.map((meta, index) => __awaiter(this, void 0, void 0, function* () {
458
- // Using fallback to networkID only when there is no chainId present.
459
- // Should be removed when networkID is completely removed.
460
- const txBelongsToCurrentChain = meta.chainId === currentChainId ||
461
- (!meta.chainId && meta.networkID === currentNetworkID);
462
- if (!meta.verifiedOnBlockchain && txBelongsToCurrentChain) {
480
+ if (!meta.verifiedOnBlockchain && meta.chainId === currentChainId) {
463
481
  const [reconciledTx, updateRequired] = yield this.blockchainTransactionStateReconciler(meta);
464
482
  if (updateRequired) {
465
483
  transactions[index] = reconciledTx;
@@ -502,13 +520,10 @@ class TransactionController extends base_controller_1.BaseController {
502
520
  this.update({ transactions: [] });
503
521
  return;
504
522
  }
505
- const { chainId: currentChainId, networkId: currentNetworkID } = this.getChainAndNetworkId();
506
- const newTransactions = this.state.transactions.filter(({ networkID, chainId, transaction }) => {
523
+ const currentChainId = this.getChainId();
524
+ const newTransactions = this.state.transactions.filter(({ chainId, transaction }) => {
507
525
  var _a;
508
- // Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed.
509
- const isMatchingNetwork = ignoreNetwork ||
510
- chainId === currentChainId ||
511
- (!chainId && networkID === currentNetworkID);
526
+ const isMatchingNetwork = ignoreNetwork || chainId === currentChainId;
512
527
  if (!isMatchingNetwork) {
513
528
  return true;
514
529
  }
@@ -525,35 +540,70 @@ class TransactionController extends base_controller_1.BaseController {
525
540
  stopIncomingTransactionProcessing() {
526
541
  this.incomingTransactionHelper.stop();
527
542
  }
528
- processApproval(transactionMeta, { requireApproval, shouldShowRequest = true, }) {
543
+ /**
544
+ * Adds external provided transaction to state as confirmed transaction.
545
+ *
546
+ * @param transactionMeta - TransactionMeta to add transactions.
547
+ * @param transactionReceipt - TransactionReceipt of the external transaction.
548
+ * @param baseFeePerGas - Base fee per gas of the external transaction.
549
+ */
550
+ confirmExternalTransaction(transactionMeta, transactionReceipt, baseFeePerGas) {
529
551
  return __awaiter(this, void 0, void 0, function* () {
530
- const transactionId = transactionMeta.id;
531
- let resultCallbacks;
552
+ // Run validation and add external transaction to state.
553
+ this.addExternalTransaction(transactionMeta);
532
554
  try {
533
- if (requireApproval !== false) {
534
- const acceptResult = yield this.requestApproval(transactionMeta, {
535
- shouldShowRequest,
536
- });
537
- resultCallbacks = acceptResult.resultCallbacks;
538
- }
539
- const { meta, isCompleted } = this.isTransactionCompleted(transactionId);
540
- if (meta && !isCompleted) {
541
- yield this.approveTransaction(transactionId);
555
+ const transactionId = transactionMeta.id;
556
+ // Make sure status is confirmed and define gasUsed as in receipt.
557
+ transactionMeta.status = types_1.TransactionStatus.confirmed;
558
+ transactionMeta.txReceipt = transactionReceipt;
559
+ if (baseFeePerGas) {
560
+ transactionMeta.baseFeePerGas = baseFeePerGas;
542
561
  }
562
+ // Update same nonce local transactions as dropped and define replacedBy properties.
563
+ this.markNonceDuplicatesDropped(transactionId);
564
+ // Update external provided transaction with updated gas values and confirmed status.
565
+ this.updateTransaction(transactionMeta);
543
566
  }
544
567
  catch (error) {
545
- const { meta, isCompleted } = this.isTransactionCompleted(transactionId);
546
- if (meta && !isCompleted) {
547
- if (error.code === eth_rpc_errors_1.errorCodes.provider.userRejectedRequest) {
548
- this.cancelTransaction(transactionId);
549
- throw eth_rpc_errors_1.ethErrors.provider.userRejectedRequest('User rejected the transaction');
568
+ console.error(error);
569
+ }
570
+ });
571
+ }
572
+ processApproval(transactionMeta, { isExisting = false, requireApproval, shouldShowRequest = true, }) {
573
+ return __awaiter(this, void 0, void 0, function* () {
574
+ const transactionId = transactionMeta.id;
575
+ let resultCallbacks;
576
+ const { meta, isCompleted } = this.isTransactionCompleted(transactionId);
577
+ const finishedPromise = isCompleted
578
+ ? Promise.resolve(meta)
579
+ : this.waitForTransactionFinished(transactionId);
580
+ if (meta && !isExisting && !isCompleted) {
581
+ try {
582
+ if (requireApproval !== false) {
583
+ const acceptResult = yield this.requestApproval(transactionMeta, {
584
+ shouldShowRequest,
585
+ });
586
+ resultCallbacks = acceptResult.resultCallbacks;
550
587
  }
551
- else {
552
- this.failTransaction(meta, error);
588
+ const { isCompleted: isTxCompleted } = this.isTransactionCompleted(transactionId);
589
+ if (!isTxCompleted) {
590
+ yield this.approveTransaction(transactionId);
591
+ }
592
+ }
593
+ catch (error) {
594
+ const { isCompleted: isTxCompleted } = this.isTransactionCompleted(transactionId);
595
+ if (!isTxCompleted) {
596
+ if (error.code === eth_rpc_errors_1.errorCodes.provider.userRejectedRequest) {
597
+ this.cancelTransaction(transactionId);
598
+ throw eth_rpc_errors_1.ethErrors.provider.userRejectedRequest('User rejected the transaction');
599
+ }
600
+ else {
601
+ this.failTransaction(meta, error);
602
+ }
553
603
  }
554
604
  }
555
605
  }
556
- const finalMeta = this.getTransaction(transactionId);
606
+ const finalMeta = yield finishedPromise;
557
607
  switch (finalMeta === null || finalMeta === void 0 ? void 0 : finalMeta.status) {
558
608
  case types_1.TransactionStatus.failed:
559
609
  resultCallbacks === null || resultCallbacks === void 0 ? void 0 : resultCallbacks.error(finalMeta.error);
@@ -584,7 +634,7 @@ class TransactionController extends base_controller_1.BaseController {
584
634
  return __awaiter(this, void 0, void 0, function* () {
585
635
  const { transactions } = this.state;
586
636
  const releaseLock = yield this.mutex.acquire();
587
- const { chainId } = this.getChainAndNetworkId();
637
+ const chainId = this.getChainId();
588
638
  const index = transactions.findIndex(({ id }) => transactionID === id);
589
639
  const transactionMeta = transactions[index];
590
640
  const { transaction: { nonce, from }, } = transactionMeta;
@@ -633,6 +683,7 @@ class TransactionController extends base_controller_1.BaseController {
633
683
  ]);
634
684
  transactionMeta.transactionHash = transactionHash;
635
685
  transactionMeta.status = types_1.TransactionStatus.submitted;
686
+ transactionMeta.submittedTime = new Date().getTime();
636
687
  this.updateTransaction(transactionMeta);
637
688
  this.hub.emit(`${transactionMeta.id}:finished`, transactionMeta);
638
689
  }
@@ -681,9 +732,9 @@ class TransactionController extends base_controller_1.BaseController {
681
732
  trimTransactionsForState(transactions) {
682
733
  const nonceNetworkSet = new Set();
683
734
  const txsToKeep = transactions.reverse().filter((tx) => {
684
- const { chainId, networkID, status, transaction, time } = tx;
735
+ const { chainId, status, transaction, time } = tx;
685
736
  if (transaction) {
686
- const key = `${transaction.nonce}-${chainId ? (0, controller_utils_1.convertHexToDecimal)(chainId) : networkID}-${new Date(time).toDateString()}`;
737
+ const key = `${transaction.nonce}-${chainId}-${new Date(time).toDateString()}`;
687
738
  if (nonceNetworkSet.has(key)) {
688
739
  return true;
689
740
  }
@@ -819,25 +870,24 @@ class TransactionController extends base_controller_1.BaseController {
819
870
  }, shouldShowRequest));
820
871
  });
821
872
  }
822
- getTransaction(transactionID) {
873
+ getTransaction(transactionId) {
823
874
  const { transactions } = this.state;
824
- return transactions.find(({ id }) => id === transactionID);
875
+ return transactions.find(({ id }) => id === transactionId);
825
876
  }
826
877
  getApprovalId(txMeta) {
827
878
  return String(txMeta.id);
828
879
  }
829
- isTransactionCompleted(transactionid) {
830
- const transaction = this.getTransaction(transactionid);
880
+ isTransactionCompleted(transactionId) {
881
+ const transaction = this.getTransaction(transactionId);
831
882
  if (!transaction) {
832
883
  return { meta: undefined, isCompleted: false };
833
884
  }
834
885
  const isCompleted = this.isLocalFinalState(transaction.status);
835
886
  return { meta: transaction, isCompleted };
836
887
  }
837
- getChainAndNetworkId() {
838
- const { networkId, providerConfig } = this.getNetworkState();
839
- const chainId = providerConfig === null || providerConfig === void 0 ? void 0 : providerConfig.chainId;
840
- return { networkId, chainId };
888
+ getChainId() {
889
+ const { providerConfig } = this.getNetworkState();
890
+ return providerConfig.chainId;
841
891
  }
842
892
  prepareUnsignedEthTx(txParams) {
843
893
  return tx_1.TransactionFactory.fromTxData(txParams, {
@@ -855,7 +905,7 @@ class TransactionController extends base_controller_1.BaseController {
855
905
  * @returns common configuration object
856
906
  */
857
907
  getCommonConfiguration() {
858
- const { networkId, providerConfig: { type: chain, chainId, nickname: name }, } = this.getNetworkState();
908
+ const { providerConfig: { type: chain, chainId, nickname: name }, } = this.getNetworkState();
859
909
  if (chain !== controller_utils_1.RPC &&
860
910
  chain !== controller_utils_1.NetworkType['linea-goerli'] &&
861
911
  chain !== controller_utils_1.NetworkType['linea-mainnet']) {
@@ -864,7 +914,6 @@ class TransactionController extends base_controller_1.BaseController {
864
914
  const customChainParams = {
865
915
  name,
866
916
  chainId: parseInt(chainId, 16),
867
- networkId: networkId === null ? NaN : parseInt(networkId, undefined),
868
917
  defaultHardfork: exports.HARDFORK,
869
918
  };
870
919
  return common_1.Common.custom(customChainParams);
@@ -911,6 +960,86 @@ class TransactionController extends base_controller_1.BaseController {
911
960
  }
912
961
  return dappSuggestedGasFees;
913
962
  }
963
+ /**
964
+ * Validates and adds external provided transaction to state.
965
+ *
966
+ * @param transactionMeta - Nominated external transaction to be added to state.
967
+ */
968
+ addExternalTransaction(transactionMeta) {
969
+ var _a;
970
+ return __awaiter(this, void 0, void 0, function* () {
971
+ const chainId = this.getChainId();
972
+ const { transactions } = this.state;
973
+ const fromAddress = (_a = transactionMeta === null || transactionMeta === void 0 ? void 0 : transactionMeta.transaction) === null || _a === void 0 ? void 0 : _a.from;
974
+ const sameFromAndNetworkTransactions = transactions.filter((transaction) => transaction.transaction.from === fromAddress &&
975
+ transaction.chainId === chainId);
976
+ const confirmedTxs = sameFromAndNetworkTransactions.filter((transaction) => transaction.status === types_1.TransactionStatus.confirmed);
977
+ const pendingTxs = sameFromAndNetworkTransactions.filter((transaction) => transaction.status === types_1.TransactionStatus.submitted);
978
+ (0, external_transactions_1.validateConfirmedExternalTransaction)(transactionMeta, confirmedTxs, pendingTxs);
979
+ const updatedTransactions = [...transactions, transactionMeta];
980
+ this.update({
981
+ transactions: this.trimTransactionsForState(updatedTransactions),
982
+ });
983
+ });
984
+ }
985
+ /**
986
+ * Sets other txMeta statuses to dropped if the txMeta that has been confirmed has other transactions
987
+ * in the transactions have the same nonce.
988
+ *
989
+ * @param transactionId - Used to identify original transaction.
990
+ */
991
+ markNonceDuplicatesDropped(transactionId) {
992
+ var _a, _b;
993
+ const chainId = this.getChainId();
994
+ const transactionMeta = this.getTransaction(transactionId);
995
+ const nonce = (_a = transactionMeta === null || transactionMeta === void 0 ? void 0 : transactionMeta.transaction) === null || _a === void 0 ? void 0 : _a.nonce;
996
+ const from = (_b = transactionMeta === null || transactionMeta === void 0 ? void 0 : transactionMeta.transaction) === null || _b === void 0 ? void 0 : _b.from;
997
+ const sameNonceTxs = this.state.transactions.filter((transaction) => transaction.transaction.from === from &&
998
+ transaction.transaction.nonce === nonce &&
999
+ transaction.chainId === chainId);
1000
+ if (!sameNonceTxs.length) {
1001
+ return;
1002
+ }
1003
+ // Mark all same nonce transactions as dropped and give it a replacedBy hash
1004
+ for (const transaction of sameNonceTxs) {
1005
+ if (transaction.id === transactionId) {
1006
+ continue;
1007
+ }
1008
+ transaction.replacedBy = transactionMeta === null || transactionMeta === void 0 ? void 0 : transactionMeta.hash;
1009
+ transaction.replacedById = transactionMeta === null || transactionMeta === void 0 ? void 0 : transactionMeta.id;
1010
+ // Drop any transaction that wasn't previously failed (off chain failure)
1011
+ if (transaction.status !== types_1.TransactionStatus.failed) {
1012
+ this.setTransactionStatusDropped(transaction);
1013
+ }
1014
+ }
1015
+ }
1016
+ /**
1017
+ * Method to set transaction status to dropped.
1018
+ *
1019
+ * @param transactionMeta - TransactionMeta of transaction to be marked as dropped.
1020
+ */
1021
+ setTransactionStatusDropped(transactionMeta) {
1022
+ transactionMeta.status = types_1.TransactionStatus.dropped;
1023
+ this.updateTransaction(transactionMeta);
1024
+ }
1025
+ /**
1026
+ * Get transaction with provided actionId.
1027
+ *
1028
+ * @param actionId - Unique ID to prevent duplicate requests
1029
+ * @returns the filtered transaction
1030
+ */
1031
+ getTransactionWithActionId(actionId) {
1032
+ return this.state.transactions.find((transaction) => actionId && transaction.actionId === actionId);
1033
+ }
1034
+ waitForTransactionFinished(transactionId) {
1035
+ return __awaiter(this, void 0, void 0, function* () {
1036
+ return new Promise((resolve) => {
1037
+ this.hub.once(`${transactionId}:finished`, (txMeta) => {
1038
+ resolve(txMeta);
1039
+ });
1040
+ });
1041
+ });
1042
+ }
914
1043
  }
915
1044
  exports.TransactionController = TransactionController;
916
1045
  exports.default = TransactionController;