@sidhujag/sysweb3-keyring 2.1.5 → 2.1.6

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 (36) hide show
  1. package/cjs/evm-fees.js +73 -0
  2. package/cjs/evm-fees.js.map +1 -1
  3. package/cjs/software.js.map +1 -1
  4. package/cjs/transactions/ethereum.js +220 -160
  5. package/cjs/transactions/ethereum.js.map +1 -1
  6. package/cjs/transactions/evm-cancellation.js +51 -0
  7. package/cjs/transactions/evm-cancellation.js.map +1 -1
  8. package/cjs/transactions/evm-local-signer.js +1 -1
  9. package/cjs/transactions/evm-local-signer.js.map +1 -1
  10. package/cjs/transactions/evm-submission.js +78 -8
  11. package/cjs/transactions/evm-submission.js.map +1 -1
  12. package/cjs/transactions/syscoin-core.js +128 -6
  13. package/cjs/transactions/syscoin-core.js.map +1 -1
  14. package/cjs/types.js.map +1 -1
  15. package/esm/evm-fees.js +72 -0
  16. package/esm/evm-fees.js.map +1 -1
  17. package/esm/transactions/ethereum.js +222 -162
  18. package/esm/transactions/ethereum.js.map +1 -1
  19. package/esm/transactions/evm-cancellation.js +50 -0
  20. package/esm/transactions/evm-cancellation.js.map +1 -1
  21. package/esm/transactions/evm-local-signer.js +1 -1
  22. package/esm/transactions/evm-local-signer.js.map +1 -1
  23. package/esm/transactions/evm-submission.js +78 -8
  24. package/esm/transactions/evm-submission.js.map +1 -1
  25. package/esm/transactions/syscoin-core.js +129 -7
  26. package/esm/transactions/syscoin-core.js.map +1 -1
  27. package/esm/types.js.map +1 -1
  28. package/package.json +3 -3
  29. package/types/evm-fees.d.ts +31 -0
  30. package/types/software.d.ts +1 -1
  31. package/types/transactions/ethereum.d.ts +9 -3
  32. package/types/transactions/evm-cancellation.d.ts +3 -0
  33. package/types/transactions/evm-local-signer.d.ts +1 -1
  34. package/types/transactions/evm-submission.d.ts +14 -2
  35. package/types/transactions/syscoin-core.d.ts +3 -1
  36. package/types/types.d.ts +49 -1
@@ -5,10 +5,10 @@ import { accessListify, Transaction } from 'ethers/transaction';
5
5
  import { hexlify } from 'ethers/utils';
6
6
  import omit from 'lodash/omit';
7
7
  import { BigNumber, Contract, Zero, formatEther, formatUnits, isHexString, normalizeTransactionRequest, normalizeTxValue, resolveProperties, serializeTransaction as serializeUncheckedTransaction, } from '../ethers-v6';
8
- import { assertEvmGasLimit, bufferedEvmGasLimit, EvmGasLimitError, getEvmFeeData, parseEvmFeeQuantity, planEvmCancellationFees, } from '../evm-fees';
8
+ import { assertEvmGasLimit, bufferedEvmGasLimit, EvmGasLimitError, getEvmFeeData, parseEvmFeeQuantity, planEvmCancellationFees, planEvmSpeedUpFees, } from '../evm-fees';
9
9
  import { CustomJsonRpcProvider, wrapTransactionResponse } from '../providers';
10
10
  import { KeyringAccountType, } from '../types';
11
- import { assertEvmCancellationEnvelope } from './evm-cancellation';
11
+ import { assertEvmCancellationEnvelope, authenticateRetainedCancellation, } from './evm-cancellation';
12
12
  import { deriveEvmAccountFromMnemonic, parsePersonalMessage as parseLocalPersonalMessage, privateKeyToAccount, normalizeGasLimitedTransaction, sendLocalEvmTransaction, signDigestHex, signPersonalMessage as signLocalPersonalMessage, } from './evm-local-signer';
13
13
  import { captureEvmSubmission, submitSignedEvmTransaction, submitSignedHardwareEvmTransaction, evmSubmissionFailure, copyEvmSubmissionFailure, } from './evm-submission';
14
14
  import { signPreparedEvmEnvelope, snapshotPreparedEvmRequest, } from './prepared-evm';
@@ -196,7 +196,7 @@ export class EthereumTransactions {
196
196
  assertCurrent,
197
197
  };
198
198
  };
199
- this.captureSubmission = (operation, account, type, id, network, assertCurrent, originalHash) => {
199
+ this.captureSubmission = (operation, account, type, id, network, assertCurrent, details = {}) => {
200
200
  if (!this.captureSubmissionLifecycle)
201
201
  throw new TypeError('A durable EVM submission lifecycle is required');
202
202
  return captureEvmSubmission({
@@ -208,7 +208,7 @@ export class EthereumTransactions {
208
208
  slip44: network.slip44,
209
209
  url: network.url,
210
210
  },
211
- ...(originalHash !== undefined ? { originalHash } : {}),
211
+ ...details,
212
212
  }, assertCurrent, this.captureSubmissionLifecycle);
213
213
  };
214
214
  this.authorizeHardwareTransaction = async (provider, assertCurrent, signal = this.abortController.signal) => {
@@ -578,7 +578,9 @@ export class EthereumTransactions {
578
578
  typeof approval.assertCurrent !== 'function' ||
579
579
  !(approval.signal instanceof AbortSignal))
580
580
  throw new Error(`${standard} transaction approval required`);
581
- const { assertCurrent, signal } = approval;
581
+ const { assertCurrent, signal, nonce } = approval;
582
+ if (!Number.isSafeInteger(nonce) || nonce < 0)
583
+ throw new Error(`Invalid ${standard} transaction approval nonce`);
582
584
  const assertApproval = () => {
583
585
  if (signal.aborted)
584
586
  throw Object.assign(new Error(`${standard} transaction approval expired`), {
@@ -587,7 +589,7 @@ export class EthereumTransactions {
587
589
  assertSynchronousOriginalValidation(assertCurrent(), `${standard} transaction approval must be synchronous`);
588
590
  };
589
591
  assertApproval();
590
- return { assertApproval, signal };
592
+ return { assertApproval, signal, nonce };
591
593
  };
592
594
  this.reviewedTokenFees = (request, chainId, standard) => {
593
595
  const { isLegacy } = request;
@@ -644,7 +646,7 @@ export class EthereumTransactions {
644
646
  maxPriorityFeePerGas: BigNumber.from(fee.maxPriorityFeePerGas),
645
647
  };
646
648
  };
647
- this.calculateNewGasValues = (oldTxsParams, isForCancel, isLegacy) => {
649
+ this.calculateNewGasValues = (oldTxsParams, isForCancel, isLegacy, bounds) => {
648
650
  const newGasValues = {
649
651
  maxFeePerGas: undefined,
650
652
  maxPriorityFeePerGas: undefined,
@@ -652,8 +654,34 @@ export class EthereumTransactions {
652
654
  gasLimit: undefined,
653
655
  };
654
656
  const { maxFeePerGas, maxPriorityFeePerGas, gasLimit, gasPrice } = oldTxsParams;
657
+ if (!isForCancel) {
658
+ const originalGasLimit = parseEvmFeeQuantity(gasLimit, 'replacement gas limit');
659
+ const plan = isLegacy
660
+ ? planEvmSpeedUpFees({
661
+ ...bounds,
662
+ type: 0,
663
+ gasLimit: originalGasLimit,
664
+ gasPrice: parseEvmFeeQuantity(gasPrice, 'replacement gas price'),
665
+ })
666
+ : planEvmSpeedUpFees({
667
+ ...bounds,
668
+ type: 2,
669
+ gasLimit: originalGasLimit,
670
+ maxFeePerGas: parseEvmFeeQuantity(maxFeePerGas, 'replacement fee cap'),
671
+ maxPriorityFeePerGas: parseEvmFeeQuantity(maxPriorityFeePerGas, 'replacement priority fee'),
672
+ });
673
+ newGasValues.gasLimit = BigNumber.from(plan.gasLimit);
674
+ if (plan.type === 0)
675
+ newGasValues.gasPrice = BigNumber.from(plan.gasPrice);
676
+ else {
677
+ newGasValues.maxFeePerGas = BigNumber.from(plan.maxFeePerGas);
678
+ newGasValues.maxPriorityFeePerGas = BigNumber.from(plan.maxPriorityFeePerGas);
679
+ }
680
+ return newGasValues;
681
+ }
682
+ // Keep the legacy direct cancellation helper. The guarded cancellation
683
+ // transaction path already uses planEvmCancellationFees.
655
684
  const calculateAndConvertNewValue = (feeValue) => {
656
- // Apply multiplier for replacement transaction (cancel or speedup)
657
685
  const newValue = feeValue * multiplierToUse;
658
686
  const calculateValue = String(newValue);
659
687
  const convertValueToHex = '0x' + parseInt(calculateValue, 10).toString(16);
@@ -661,7 +689,6 @@ export class EthereumTransactions {
661
689
  };
662
690
  const maxFeePerGasToNumber = maxFeePerGas?.toNumber();
663
691
  const maxPriorityFeePerGasToNumber = maxPriorityFeePerGas?.toNumber();
664
- const gasLimitToNumber = gasLimit?.toNumber();
665
692
  const gasPriceToNumber = gasPrice?.toNumber();
666
693
  const multiplierToUse = 1.2; //The same calculation we used in the edit fee modal, always using the 0.2 multiplier
667
694
  if (!isLegacy) {
@@ -672,14 +699,8 @@ export class EthereumTransactions {
672
699
  if (isLegacy) {
673
700
  newGasValues.gasPrice = calculateAndConvertNewValue(gasPriceToNumber);
674
701
  }
675
- if (isForCancel) {
676
- const DEFAULT_GAS_LIMIT_VALUE = '42000';
677
- const convertToHex = '0x' + parseInt(DEFAULT_GAS_LIMIT_VALUE, 10).toString(16);
678
- newGasValues.gasLimit = BigNumber.from(convertToHex);
679
- }
680
- if (!isForCancel) {
681
- newGasValues.gasLimit = calculateAndConvertNewValue(gasLimitToNumber);
682
- }
702
+ newGasValues.gasLimit = BigNumber.from(42000);
703
+ assertEvmGasLimit(newGasValues.gasLimit.toBigInt(), bounds.chainId, bounds.sampledBlockGasLimit);
683
704
  return newGasValues;
684
705
  };
685
706
  this.cancelSentTransaction = async (txHash, isLegacy, fallbackNonce, options) => {
@@ -688,8 +709,13 @@ export class EthereumTransactions {
688
709
  if (typeof options?.assertCurrentContext !== 'function') {
689
710
  throw new Error('Cancellation context required');
690
711
  }
691
- if (typeof options.validateOriginal !== 'function') {
692
- throw new Error('Cancellation original validator required');
712
+ const { assertCurrentContext, validateOriginal, origin } = options;
713
+ if (origin === undefined) {
714
+ if (typeof validateOriginal !== 'function')
715
+ throw new Error('Cancellation original validator required');
716
+ }
717
+ else if (validateOriginal !== undefined || fallbackNonce !== undefined) {
718
+ throw new Error('Retained cancellation cannot use RPC fallback');
693
719
  }
694
720
  if (fallbackNonce !== undefined &&
695
721
  (!Number.isSafeInteger(fallbackNonce) || fallbackNonce < 0)) {
@@ -702,7 +728,6 @@ export class EthereumTransactions {
702
728
  !/^(0|[1-9][0-9]{0,77})$/.test(approvedMaximumFee))) {
703
729
  throw new Error('Cancellation fee approval required');
704
730
  }
705
- const { assertCurrentContext, validateOriginal } = options;
706
731
  const assertHostContext = () => assertSynchronousOriginalValidation(assertCurrentContext(), 'Transaction context guard must be synchronous');
707
732
  assertHostContext();
708
733
  const state = this.getState();
@@ -747,9 +772,22 @@ export class EthereumTransactions {
747
772
  assertKeyringContext();
748
773
  };
749
774
  assertSigningCurrent();
775
+ const retained = origin === undefined
776
+ ? undefined
777
+ : authenticateRetainedCancellation(origin, txHash.toLowerCase(), activeAccount.address, activeNetwork.chainId);
778
+ if (retained) {
779
+ const legacy = retained.feeFloor.type === 0;
780
+ if (activeNetwork.kind !== INetworkType.Ethereum ||
781
+ (isLegacy !== undefined && isLegacy !== legacy))
782
+ throw new Error('Retained cancellation network or type changed');
783
+ isLegacy = legacy;
784
+ }
750
785
  const submission = previewOnly
751
786
  ? undefined
752
- : this.captureSubmission('cancel', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertSigningCurrent, txHash);
787
+ : this.captureSubmission('cancel', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertSigningCurrent, {
788
+ originalHash: retained ? txHash.toLowerCase() : txHash,
789
+ origin: retained,
790
+ });
753
791
  const assertCurrent = submission?.assertCurrent ?? assertSigningCurrent;
754
792
  const currentEip1559Fees = async () => {
755
793
  const feeData = await this.getFeeDataWithDynamicMaxPriorityFeePerGas(provider);
@@ -761,64 +799,53 @@ export class EthereumTransactions {
761
799
  }
762
800
  return { maxFeePerGas, maxPriorityFeePerGas };
763
801
  };
764
- const response = await provider.getTransaction(txHash);
765
- assertCurrent();
766
- if (response?.blockNumber != null || response?.blockHash != null) {
767
- throw new Error('Original transaction is already mined');
768
- }
769
- let tx = response === null
770
- ? null
771
- : authenticateReplacementOriginal(response, txHash.toLowerCase(), activeAccount.address.toLowerCase(), activeNetwork.chainId);
772
- const software = activeAccountType !== KeyringAccountType.Ledger &&
773
- activeAccountType !== KeyringAccountType.Trezor;
774
- const softwareNonce = software ? tx?.nonce ?? fallbackNonce : undefined;
775
- // The host validator may retain the wrapped response and its fee objects.
776
- const softwareOriginalFees = software && tx
777
- ? {
778
- gasPrice: tx.gasPrice.toBigInt(),
779
- maxFeePerGas: tx.maxFeePerGas.toBigInt(),
780
- maxPriorityFeePerGas: tx.maxPriorityFeePerGas.toBigInt(),
781
- }
782
- : undefined;
783
- assertSynchronousOriginalValidation(validateOriginal(tx));
784
- assertCurrent();
785
- // If transaction not found, create a minimal tx object with current gas prices
786
- // This handles cases where tx with 0 gas never made it to the mempool
787
- if (!tx && fallbackNonce !== undefined) {
788
- // Fetch current network gas prices for the cancellation
789
- if (isLegacy) {
790
- const currentGasPrice = nonNegativeFeeQuantity(await provider.getGasPrice(), 'cancellation gas price');
791
- assertCurrent();
792
- tx = {
793
- from: activeAccount.address,
794
- to: activeAccount.address,
795
- value: Zero,
796
- nonce: fallbackNonce,
797
- gasPrice: currentGasPrice,
798
- data: '0x',
799
- };
802
+ let cancellationNonce;
803
+ let originalFees;
804
+ if (retained) {
805
+ cancellationNonce = retained.nonce;
806
+ originalFees = retained.feeFloor;
807
+ }
808
+ else {
809
+ const response = await provider.getTransaction(txHash);
810
+ assertCurrent();
811
+ if (response?.blockNumber != null || response?.blockHash != null) {
812
+ throw new Error('Original transaction is already mined');
813
+ }
814
+ const tx = response === null
815
+ ? null
816
+ : authenticateReplacementOriginal(response, txHash.toLowerCase(), activeAccount.address.toLowerCase(), activeNetwork.chainId);
817
+ const software = activeAccountType !== KeyringAccountType.Ledger &&
818
+ activeAccountType !== KeyringAccountType.Trezor;
819
+ // The host validator may retain the wrapped response and its fee objects.
820
+ const softwareNonce = software ? tx?.nonce ?? fallbackNonce : undefined;
821
+ const softwareOriginalFees = software && tx
822
+ ? {
823
+ gasPrice: tx.gasPrice.toBigInt(),
824
+ maxFeePerGas: tx.maxFeePerGas.toBigInt(),
825
+ maxPriorityFeePerGas: tx.maxPriorityFeePerGas.toBigInt(),
826
+ }
827
+ : undefined;
828
+ assertSynchronousOriginalValidation(validateOriginal(tx));
829
+ assertCurrent();
830
+ if (tx) {
831
+ cancellationNonce = software ? softwareNonce : tx.nonce;
832
+ originalFees = softwareOriginalFees ?? tx;
800
833
  }
801
834
  else {
802
- const feeData = await currentEip1559Fees();
803
- tx = {
804
- from: activeAccount.address,
805
- to: activeAccount.address,
806
- value: Zero,
807
- nonce: fallbackNonce,
808
- maxFeePerGas: feeData.maxFeePerGas,
809
- maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
810
- data: '0x',
811
- };
835
+ if (fallbackNonce === undefined)
836
+ return { isCanceled: false, error: true };
837
+ cancellationNonce = fallbackNonce;
838
+ // Preserve the ordinary host's explicitly admitted missing-RPC path.
839
+ if (isLegacy) {
840
+ const gasPrice = nonNegativeFeeQuantity(await provider.getGasPrice(), 'cancellation gas price');
841
+ assertCurrent();
842
+ originalFees = { gasPrice };
843
+ }
844
+ else
845
+ originalFees = await currentEip1559Fees();
812
846
  }
813
847
  }
814
- else if (!tx) {
815
- // No fallback nonce provided and tx not found
816
- return {
817
- isCanceled: false,
818
- error: true,
819
- };
820
- }
821
- const { gasPrice: originalGasPrice, maxFeePerGas: originalMaxFeePerGas, maxPriorityFeePerGas: originalMaxPriorityFeePerGas, } = softwareOriginalFees ?? tx;
848
+ const { gasPrice: originalGasPrice, maxFeePerGas: originalMaxFeePerGas, maxPriorityFeePerGas: originalMaxPriorityFeePerGas, } = originalFees;
822
849
  let feePlan;
823
850
  if (isLegacy) {
824
851
  const currentGasPrice = parseEvmFeeQuantity(await provider.getGasPrice(), 'cancellation gas price');
@@ -855,6 +882,16 @@ export class EthereumTransactions {
855
882
  };
856
883
  if (maximumFee.gt(approvedMaximumFee))
857
884
  throw new Error('Cancellation fee exceeds approval');
885
+ const assertCancellationEnvelope = (unsigned) => {
886
+ if (cancellationNonce === undefined)
887
+ throw new Error('Cancellation nonce required');
888
+ assertEvmCancellationEnvelope(unsigned, {
889
+ payer: activeAccount.address,
890
+ chainId: activeNetwork.chainId,
891
+ nonce: cancellationNonce,
892
+ feePlan,
893
+ });
894
+ };
858
895
  const assertApprovedEnvelope = (transaction) => {
859
896
  const gas = nonNegativeFeeQuantity(transaction.gasLimit, 'cancellation gas limit');
860
897
  const cap = nonNegativeFeeQuantity(isLegacy ? transaction.gasPrice : transaction.maxFeePerGas, 'cancellation maximum fee');
@@ -863,7 +900,7 @@ export class EthereumTransactions {
863
900
  };
864
901
  // Base cancel transaction parameters (same for all wallet types)
865
902
  const baseCancelTx = {
866
- nonce: software ? softwareNonce : tx.nonce,
903
+ nonce: cancellationNonce,
867
904
  from: activeAccount.address,
868
905
  to: activeAccount.address,
869
906
  value: Zero,
@@ -889,7 +926,7 @@ export class EthereumTransactions {
889
926
  assertCurrent();
890
927
  const formatParams = {
891
928
  ...resolvedParams,
892
- nonce: resolvedParams.nonce
929
+ nonce: resolvedParams.nonce != null
893
930
  ? Number(resolvedParams.nonce.toString())
894
931
  : undefined,
895
932
  };
@@ -906,6 +943,8 @@ export class EthereumTransactions {
906
943
  };
907
944
  assertApprovedEnvelope(txFormattedForEthers);
908
945
  const rawTx = serializeTransaction(txFormattedForEthers);
946
+ if (retained)
947
+ assertCancellationEnvelope(rawTx);
909
948
  const signature = await this.requireLedgerSigner().evm.signEVMTransaction({
910
949
  rawTx: rawTx.replace('0x', ''),
911
950
  accountIndex: activeAccountId,
@@ -921,7 +960,7 @@ export class EthereumTransactions {
921
960
  const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
922
961
  assertCurrent();
923
962
  assertHardwareTransactionSender(signedTx, activeAccount.address);
924
- const transactionResponse = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, rawTx);
963
+ const transactionResponse = await this.submitSignedHardwareTransaction(provider, signedTx, ({ rawTransaction, admit }) => admit(() => provider.sendTransaction(rawTransaction)), submission, undefined, rawTx);
925
964
  return {
926
965
  isCanceled: true,
927
966
  transaction: transactionResponse,
@@ -978,6 +1017,8 @@ export class EthereumTransactions {
978
1017
  assertApprovedEnvelope(txFormattedForTrezor);
979
1018
  assertTransactionGasLimit(txFormattedForTrezor);
980
1019
  const expectedUnsignedTransaction = serializeTransaction(txFormattedForTrezor);
1020
+ if (retained)
1021
+ assertCancellationEnvelope(expectedUnsignedTransaction);
981
1022
  const signature = await this.requireTrezorSigner().signEthTransaction({
982
1023
  coin: trezorCoin,
983
1024
  tx: txFormattedForTrezor,
@@ -989,7 +1030,7 @@ export class EthereumTransactions {
989
1030
  const signedTx = serializeTransaction(txFormattedForTrezor, trezorSignature(signature.payload));
990
1031
  assertCurrent();
991
1032
  assertHardwareTransactionSender(signedTx, activeAccount.address);
992
- const transactionResponse = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, expectedUnsignedTransaction);
1033
+ const transactionResponse = await this.submitSignedHardwareTransaction(provider, signedTx, ({ rawTransaction, admit }) => admit(() => provider.sendTransaction(rawTransaction)), submission, undefined, expectedUnsignedTransaction);
993
1034
  return {
994
1035
  isCanceled: true,
995
1036
  transaction: transactionResponse,
@@ -1020,12 +1061,7 @@ export class EthereumTransactions {
1020
1061
  }
1021
1062
  const transactionResponse = await sendLocalEvmTransaction(provider, decryptedPrivateKey, changedTxToCancel, assertCurrent, assertCurrent, (transaction) => {
1022
1063
  assertApprovedEnvelope(transaction);
1023
- assertEvmCancellationEnvelope(serializeTransaction(transaction), {
1024
- payer: activeAccount.address,
1025
- chainId: activeNetwork.chainId,
1026
- nonce: softwareNonce,
1027
- feePlan,
1028
- });
1064
+ assertCancellationEnvelope(serializeTransaction(transaction));
1029
1065
  }, submission);
1030
1066
  if (transactionResponse) {
1031
1067
  return {
@@ -1071,7 +1107,8 @@ export class EthereumTransactions {
1071
1107
  throw new Error('Explicit transaction approval is required');
1072
1108
  // Capture once; later caller mutations cannot replace this invocation's
1073
1109
  // admission decision while chain checks or device approval are pending.
1074
- const { assertCurrent: assertHostCurrent, mode } = options;
1110
+ const { assertCurrent: assertHostCurrent, mode, walletNonce } = options;
1111
+ const nonceApproval = walletNonce === undefined ? undefined : { ...walletNonce };
1075
1112
  const assertApproved = () => {
1076
1113
  const result = assertHostCurrent();
1077
1114
  if (result !== undefined) {
@@ -1082,7 +1119,7 @@ export class EthereumTransactions {
1082
1119
  assertApproved();
1083
1120
  return await this.sendFormattedTransactionInternal(params, isLegacy, () => {
1084
1121
  broadcastAttempted = true;
1085
- }, assertApproved, mode);
1122
+ }, assertApproved, mode, nonceApproval);
1086
1123
  }
1087
1124
  catch (error) {
1088
1125
  // A broadcast transport error may mean the node accepted the signed
@@ -1099,7 +1136,7 @@ export class EthereumTransactions {
1099
1136
  throw submissionError;
1100
1137
  }
1101
1138
  };
1102
- this.sendFormattedTransactionInternal = async (input, isLegacy, beforeBroadcast, assertHostCurrent, mode) => {
1139
+ this.sendFormattedTransactionInternal = async (input, isLegacy, beforeBroadcast, assertHostCurrent, mode, walletNonce) => {
1103
1140
  // Hardware approval may outlive the selected account, network or unlocked
1104
1141
  // session. Keep immutable identities and one provider for this invocation.
1105
1142
  const { activeAccount, activeAccountType, activeAccountId, activeNetwork, provider, assertCurrent: assertKeyringCurrent, } = this.captureEvmSigningContext();
@@ -1111,8 +1148,13 @@ export class EthereumTransactions {
1111
1148
  assertCurrent();
1112
1149
  beforeBroadcast();
1113
1150
  };
1114
- const submission = this.captureSubmission('send', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertSigningCurrent);
1151
+ const submission = this.captureSubmission('send', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertSigningCurrent, { walletNonce });
1115
1152
  const assertCurrent = submission.assertCurrent;
1153
+ const approvedNonce = submission.context.walletNonce?.nonce;
1154
+ const assertApprovedNonce = (nonce) => {
1155
+ if (approvedNonce !== undefined && nonce !== approvedNonce)
1156
+ throw new Error('EVM transaction nonce does not match wallet approval');
1157
+ };
1116
1158
  if (mode === 'reviewed-native') {
1117
1159
  const { transaction } = snapshotPreparedEvmRequest({
1118
1160
  account: {
@@ -1128,6 +1170,7 @@ export class EthereumTransactions {
1128
1170
  },
1129
1171
  transaction: input,
1130
1172
  });
1173
+ assertApprovedNonce(Number(transaction.nonce));
1131
1174
  const minimum = this.gasOverrides.minGasLimit;
1132
1175
  if (transaction.data !== '0x' ||
1133
1176
  BigInt(transaction.value) === 0n ||
@@ -1146,7 +1189,7 @@ export class EthereumTransactions {
1146
1189
  throw new Error('Reviewed native signer does not match the approved account');
1147
1190
  assertCurrent();
1148
1191
  const { signed, expectedUnsigned } = signPreparedEvmEnvelope(transaction, decryptedPrivateKey);
1149
- return submitSignedEvmTransaction(signed.rawTransaction, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, expectedUnsigned);
1192
+ return submitSignedEvmTransaction(signed.rawTransaction, ({ rawTransaction, admit }) => admit(() => provider.sendTransaction(rawTransaction)), submission, assertBeforeBroadcast, expectedUnsigned);
1150
1193
  }
1151
1194
  // Detach supplied values before any RPC/device await. Autofill updates only
1152
1195
  // this invocation; caller mutations cannot replace already supplied fields.
@@ -1181,6 +1224,7 @@ export class EthereumTransactions {
1181
1224
  supplied.data = hexlify(supplied.data);
1182
1225
  if (supplied.accessList != null)
1183
1226
  supplied.accessList = accessListify(supplied.accessList);
1227
+ assertApprovedNonce(supplied.nonce);
1184
1228
  params = supplied;
1185
1229
  assertCurrent();
1186
1230
  // Capture the reviewed nonce before any fee/RPC await. Hardware and the
@@ -1302,7 +1346,7 @@ export class EthereumTransactions {
1302
1346
  try {
1303
1347
  const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
1304
1348
  assertHardwareTransactionSender(signedTx, activeAccount.address);
1305
- const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, rawTx);
1349
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, ({ rawTransaction, admit }) => admit(() => provider.sendTransaction(rawTransaction)), submission, assertBeforeBroadcast, rawTx);
1306
1350
  return finalTx;
1307
1351
  }
1308
1352
  catch (error) {
@@ -1392,7 +1436,7 @@ export class EthereumTransactions {
1392
1436
  };
1393
1437
  const signedTx = serializeTransaction(txFormattedForEthers, trezorSignature(signature.payload));
1394
1438
  assertHardwareTransactionSender(signedTx, activeAccount.address);
1395
- const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, expectedUnsignedTransaction);
1439
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, ({ rawTransaction, admit }) => admit(() => provider.sendTransaction(rawTransaction)), submission, assertBeforeBroadcast, expectedUnsignedTransaction);
1396
1440
  return finalTx;
1397
1441
  }
1398
1442
  catch (error) {
@@ -1447,7 +1491,7 @@ export class EthereumTransactions {
1447
1491
  throw new Error('Replacement context required');
1448
1492
  if (typeof options.validateOriginal !== 'function')
1449
1493
  throw new Error('Replacement original validator required');
1450
- const { assertCurrentContext, validateOriginal, beforeBroadcast } = options;
1494
+ const { assertCurrentContext, validateOriginal, beforeBroadcast, retainedCancellation, } = options;
1451
1495
  if (!previewOnly)
1452
1496
  this.assertHardwareSignerAvailable(options.targetAccount);
1453
1497
  isLegacy ?? (isLegacy = false);
@@ -1488,9 +1532,27 @@ export class EthereumTransactions {
1488
1532
  const requestedHash = txHash.toLowerCase();
1489
1533
  const provider = this.web3Provider;
1490
1534
  assertSigningCurrent();
1535
+ let cancellation;
1536
+ if (retainedCancellation !== undefined) {
1537
+ if (typeof retainedCancellation !== 'string' ||
1538
+ retainedCancellation.length > 1024)
1539
+ throw new Error('Invalid retained cancellation');
1540
+ cancellation = Transaction.from(retainedCancellation);
1541
+ if (!cancellation.isSigned() ||
1542
+ cancellation.serialized !== retainedCancellation ||
1543
+ cancellation.hash !== requestedHash ||
1544
+ cancellation.chainId !== BigInt(activeNetwork.chainId) ||
1545
+ cancellation.from?.toLowerCase() !== expectedSender ||
1546
+ cancellation.to?.toLowerCase() !== expectedSender ||
1547
+ cancellation.value !== 0n ||
1548
+ cancellation.data !== '0x' ||
1549
+ cancellation.accessList?.length ||
1550
+ (cancellation.type !== 0 && cancellation.type !== 2))
1551
+ throw new Error('Invalid retained cancellation');
1552
+ }
1491
1553
  const submission = previewOnly
1492
1554
  ? undefined
1493
- : this.captureSubmission('speed-up', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertSigningCurrent, txHash);
1555
+ : this.captureSubmission('speed-up', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertSigningCurrent, { originalHash: txHash });
1494
1556
  const assertCurrent = submission?.assertCurrent ?? assertSigningCurrent;
1495
1557
  const validateLookup = (transaction) => {
1496
1558
  assertCurrent();
@@ -1498,6 +1560,10 @@ export class EthereumTransactions {
1498
1560
  return null;
1499
1561
  const authenticated = authenticateReplacementOriginal(transaction, requestedHash, expectedSender, activeNetwork.chainId);
1500
1562
  const type = authenticated.type;
1563
+ if (cancellation &&
1564
+ (type !== cancellation.type ||
1565
+ parseEvmFeeQuantity(authenticated.gasLimit, 'retained cancellation gas limit') !== cancellation.gasLimit))
1566
+ throw new Error('Retained cancellation does not match original');
1501
1567
  if (type !== 1)
1502
1568
  isLegacy = type === 0;
1503
1569
  if (isLegacy &&
@@ -1526,9 +1592,20 @@ export class EthereumTransactions {
1526
1592
  };
1527
1593
  }
1528
1594
  }
1529
- // Check if this might be a max send transaction by comparing total cost to balance
1530
1595
  const currentBalance = await provider.getBalance(activeAccount.address);
1531
1596
  assertCurrent();
1597
+ const sampledBlock = await provider.getBlock('latest');
1598
+ assertCurrent();
1599
+ const bounds = Object.freeze({
1600
+ chainId: BigInt(activeNetwork.chainId),
1601
+ ...(cancellation
1602
+ ? { retainedCancellationGasLimit: cancellation.gasLimit }
1603
+ : {}),
1604
+ sampledBlockGasLimit: parseEvmFeeQuantity(sampledBlock?.gasLimit, 'replacement block gas limit'),
1605
+ sampledBaseFeePerGas: sampledBlock?.baseFeePerGas == null
1606
+ ? null
1607
+ : parseEvmFeeQuantity(sampledBlock.baseFeePerGas, 'replacement base fee'),
1608
+ });
1532
1609
  // Ensure all transaction values are resolved from promises
1533
1610
  const gasLimit = await Promise.resolve(tx.gasLimit);
1534
1611
  const gasPrice = await Promise.resolve(tx.gasPrice || 0);
@@ -1537,15 +1614,6 @@ export class EthereumTransactions {
1537
1614
  const txValue = await Promise.resolve(tx.value);
1538
1615
  const txData = await Promise.resolve(tx.data || '0x');
1539
1616
  assertCurrent();
1540
- // Check if this is a contract call (has data)
1541
- const isContractCall = txData && txData !== '0x' && txData.length > 2;
1542
- const originalGasCost = isLegacy
1543
- ? gasLimit.mul(gasPrice || 0)
1544
- : gasLimit.mul(maxFeePerGas || 0);
1545
- const originalTotalCost = txValue.add(originalGasCost);
1546
- // If original transaction used >95% of balance, it's likely a max send
1547
- const balanceThreshold = currentBalance.mul(95).div(100);
1548
- const isLikelyMaxSend = originalTotalCost.gt(balanceThreshold);
1549
1617
  const oldTxsGasValues = {
1550
1618
  maxFeePerGas: maxFeePerGas,
1551
1619
  maxPriorityFeePerGas: maxPriorityFeePerGas,
@@ -1559,26 +1627,33 @@ export class EthereumTransactions {
1559
1627
  assertCurrent();
1560
1628
  }
1561
1629
  else if (!isLegacy && oldTxsGasValues.maxFeePerGas.isZero()) {
1562
- const feeData = await this.getFeeDataWithDynamicMaxPriorityFeePerGas(provider);
1630
+ const feeData = await this.getFeeDataWithDynamicMaxPriorityFeePerGas({
1631
+ getGasPrice: () => provider.getGasPrice(),
1632
+ getBlock: async () => sampledBlock,
1633
+ send: (method, params) => provider.send(method, params),
1634
+ });
1563
1635
  assertCurrent();
1564
1636
  oldTxsGasValues.maxFeePerGas = BigNumber.from(feeData.maxFeePerGas || 0);
1565
1637
  oldTxsGasValues.maxPriorityFeePerGas = BigNumber.from(feeData.maxPriorityFeePerGas || 0);
1566
1638
  }
1567
- const newGasValues = this.calculateNewGasValues(oldTxsGasValues, false, isLegacy);
1639
+ const newGasValues = this.calculateNewGasValues(oldTxsGasValues, false, isLegacy, bounds);
1568
1640
  if (!newGasValues.gasLimit.gt(0))
1569
1641
  throw new Error('Invalid replacement gas limit');
1642
+ assertTransactionGasLimit({
1643
+ gasLimit: newGasValues.gasLimit,
1644
+ chainId: activeNetwork.chainId,
1645
+ });
1570
1646
  if (!isLegacy &&
1571
1647
  newGasValues.maxPriorityFeePerGas.gt(newGasValues.maxFeePerGas))
1572
1648
  throw new Error('Invalid replacement fee');
1573
1649
  const maximumFee = newGasValues.gasLimit.mul(isLegacy ? newGasValues.gasPrice : newGasValues.maxFeePerGas);
1574
- if (isLikelyMaxSend && txValue.add(maximumFee).gt(currentBalance)) {
1575
- if (isContractCall) {
1576
- console.error('[SpeedUp] Cannot adjust value for contract call - rejecting speedup');
1650
+ if (txValue.add(maximumFee).gt(currentBalance)) {
1651
+ if (txData.length > 2) {
1577
1652
  return {
1578
1653
  isSpeedUp: false,
1579
1654
  error: true,
1580
1655
  code: 'CONTRACT_CALL_MAX_SEND',
1581
- message: 'Cannot speed up a likely max-send contract call; value cannot be adjusted to fit new gas',
1656
+ message: 'Add funds before speeding up this contract call; the original amount will not be changed',
1582
1657
  };
1583
1658
  }
1584
1659
  return {
@@ -1645,7 +1720,7 @@ export class EthereumTransactions {
1645
1720
  const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
1646
1721
  assertSignedSender(signedTx);
1647
1722
  assertHardwareTransactionSender(signedTx, activeAccount.address);
1648
- const transactionResponse = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, rawTx);
1723
+ const transactionResponse = await this.submitSignedHardwareTransaction(provider, signedTx, ({ rawTransaction, admit }) => admit(() => provider.sendTransaction(rawTransaction)), submission, assertBeforeBroadcast, rawTx);
1649
1724
  return {
1650
1725
  isSpeedUp: true,
1651
1726
  transaction: transactionResponse,
@@ -1726,7 +1801,7 @@ export class EthereumTransactions {
1726
1801
  const signedTx = serializeTransaction(txFormattedForTrezor, trezorSignature(signature.payload));
1727
1802
  assertSignedSender(signedTx);
1728
1803
  assertHardwareTransactionSender(signedTx, activeAccount.address);
1729
- const transactionResponse = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, expectedUnsignedTransaction);
1804
+ const transactionResponse = await this.submitSignedHardwareTransaction(provider, signedTx, ({ rawTransaction, admit }) => admit(() => provider.sendTransaction(rawTransaction)), submission, assertBeforeBroadcast, expectedUnsignedTransaction);
1730
1805
  return {
1731
1806
  isSpeedUp: true,
1732
1807
  transaction: transactionResponse,
@@ -1812,7 +1887,7 @@ export class EthereumTransactions {
1812
1887
  }
1813
1888
  };
1814
1889
  this.sendSignedErc20Transaction = async (request, approval) => {
1815
- const { assertApproval, signal } = this.captureReviewedTokenApproval(approval, 'ERC20');
1890
+ const { assertApproval, signal, nonce } = this.captureReviewedTokenApproval(approval, 'ERC20');
1816
1891
  const { receiver, tokenAddress, tokenAmountUnits, saveTrezorTx, networkUrl, } = request;
1817
1892
  const { activeAccount, activeAccountType, activeAccountId, activeNetwork, provider, assertCurrent: assertSigningCurrent, } = this.captureEvmSigningContext();
1818
1893
  const activeAccountAddress = activeAccount.address;
@@ -1829,7 +1904,7 @@ export class EthereumTransactions {
1829
1904
  const submission = this.captureSubmission('erc20', activeAccount, activeAccountType, activeAccountId, activeNetwork, () => {
1830
1905
  assertApproval();
1831
1906
  assertSigningCurrent();
1832
- });
1907
+ }, { walletNonce: { nonce, source: 'automatic' } });
1833
1908
  const assertCurrent = submission.assertCurrent;
1834
1909
  const sendERC20Token = async () => {
1835
1910
  assertCurrent();
@@ -1839,7 +1914,6 @@ export class EthereumTransactions {
1839
1914
  let transferMethod;
1840
1915
  if (isLegacy) {
1841
1916
  const overrides = {
1842
- nonce: await provider.getTransactionCount(activeAccountAddress, 'pending'),
1843
1917
  gasPrice,
1844
1918
  gasLimit,
1845
1919
  type: 0, // Explicitly set Type 0 for legacy token transfers
@@ -1849,7 +1923,6 @@ export class EthereumTransactions {
1849
1923
  }
1850
1924
  else {
1851
1925
  const overrides = {
1852
- nonce: await provider.getTransactionCount(activeAccountAddress, 'pending'),
1853
1926
  maxPriorityFeePerGas,
1854
1927
  maxFeePerGas,
1855
1928
  gasLimit,
@@ -1859,6 +1932,7 @@ export class EthereumTransactions {
1859
1932
  }
1860
1933
  return await sendLocalEvmTransaction(provider, decryptedPrivateKey, {
1861
1934
  ...transferMethod,
1935
+ nonce,
1862
1936
  chainId: activeNetwork.chainId,
1863
1937
  from: activeAccountAddress,
1864
1938
  value: '0x0',
@@ -1869,8 +1943,6 @@ export class EthereumTransactions {
1869
1943
  }
1870
1944
  };
1871
1945
  const sendERC20TokenOnLedger = async () => {
1872
- assertCurrent();
1873
- const transactionNonce = await provider.getTransactionCount(activeAccountAddress, 'pending');
1874
1946
  assertCurrent();
1875
1947
  try {
1876
1948
  const _contract = new Contract(tokenAddress, getErc20Abi(), provider);
@@ -1886,7 +1958,7 @@ export class EthereumTransactions {
1886
1958
  gasLimit: gasLimit,
1887
1959
  gasPrice,
1888
1960
  data: txData,
1889
- nonce: transactionNonce,
1961
+ nonce,
1890
1962
  chainId: activeNetwork.chainId,
1891
1963
  type: 0,
1892
1964
  };
@@ -1899,7 +1971,7 @@ export class EthereumTransactions {
1899
1971
  maxFeePerGas,
1900
1972
  maxPriorityFeePerGas,
1901
1973
  data: txData,
1902
- nonce: transactionNonce,
1974
+ nonce,
1903
1975
  chainId: activeNetwork.chainId,
1904
1976
  type: 2, // Need explicit type for hardware wallet serialization
1905
1977
  };
@@ -1921,8 +1993,8 @@ export class EthereumTransactions {
1921
1993
  const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
1922
1994
  assertCurrent();
1923
1995
  assertHardwareTransactionSender(signedTx, activeAccount.address);
1924
- const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, async (raw) => {
1925
- const response = await provider.sendTransaction(raw);
1996
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, async ({ rawTransaction, admit }) => {
1997
+ const response = await admit(() => provider.sendTransaction(rawTransaction));
1926
1998
  saveTrezorTx && saveTrezorTx(response);
1927
1999
  return response;
1928
2000
  }, submission, undefined, rawTx);
@@ -1941,8 +2013,6 @@ export class EthereumTransactions {
1941
2013
  }
1942
2014
  };
1943
2015
  const sendERC20TokenOnTrezor = async () => {
1944
- assertCurrent();
1945
- const transactionNonce = await provider.getTransactionCount(activeAccountAddress, 'pending');
1946
2016
  assertCurrent();
1947
2017
  try {
1948
2018
  const _contract = new Contract(tokenAddress, getErc20Abi(), provider);
@@ -1957,7 +2027,7 @@ export class EthereumTransactions {
1957
2027
  value: '0x0',
1958
2028
  gasLimit: this.toHex0x(gasLimit, 'gasLimit'),
1959
2029
  gasPrice: this.toHex0x(gasPrice, 'gasPrice'),
1960
- nonce: this.toBigNumber(transactionNonce)._hex,
2030
+ nonce: this.toBigNumber(nonce)._hex,
1961
2031
  chainId: activeNetwork.chainId,
1962
2032
  data: txData,
1963
2033
  };
@@ -1969,7 +2039,7 @@ export class EthereumTransactions {
1969
2039
  gasLimit: this.toHex0x(gasLimit, 'gasLimit'),
1970
2040
  maxFeePerGas: this.toHex0x(maxFeePerGas, 'maxFeePerGas'),
1971
2041
  maxPriorityFeePerGas: this.toHex0x(maxPriorityFeePerGas, 'maxPriorityFeePerGas'),
1972
- nonce: this.toBigNumber(transactionNonce)._hex,
2042
+ nonce: this.toBigNumber(nonce)._hex,
1973
2043
  chainId: activeNetwork.chainId,
1974
2044
  data: txData,
1975
2045
  };
@@ -1996,7 +2066,7 @@ export class EthereumTransactions {
1996
2066
  gasLimit: gasLimit,
1997
2067
  gasPrice,
1998
2068
  data: txData,
1999
- nonce: transactionNonce,
2069
+ nonce,
2000
2070
  chainId: activeNetwork.chainId,
2001
2071
  type: 0,
2002
2072
  };
@@ -2009,7 +2079,7 @@ export class EthereumTransactions {
2009
2079
  maxFeePerGas,
2010
2080
  maxPriorityFeePerGas,
2011
2081
  data: txData,
2012
- nonce: transactionNonce,
2082
+ nonce,
2013
2083
  chainId: activeNetwork.chainId,
2014
2084
  type: 2, // Need explicit type for hardware wallet serialization
2015
2085
  };
@@ -2017,8 +2087,8 @@ export class EthereumTransactions {
2017
2087
  const signedTx = serializeTransaction(txFormattedForEthers, trezorSignature(signature.payload));
2018
2088
  assertCurrent();
2019
2089
  assertHardwareTransactionSender(signedTx, activeAccount.address);
2020
- const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, async (raw) => {
2021
- const response = await provider.sendTransaction(raw);
2090
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, async ({ rawTransaction, admit }) => {
2091
+ const response = await admit(() => provider.sendTransaction(rawTransaction));
2022
2092
  saveTrezorTx && saveTrezorTx(response);
2023
2093
  return response;
2024
2094
  }, submission, undefined, expectedUnsignedTransaction);
@@ -2046,7 +2116,7 @@ export class EthereumTransactions {
2046
2116
  }
2047
2117
  };
2048
2118
  this.sendSignedErc721Transaction = async (request, approval) => {
2049
- const { assertApproval, signal } = this.captureReviewedTokenApproval(approval, 'ERC721');
2119
+ const { assertApproval, signal, nonce } = this.captureReviewedTokenApproval(approval, 'ERC721');
2050
2120
  const { receiver, tokenAddress, tokenId, networkUrl } = request;
2051
2121
  const { activeAccount, activeAccountType, activeAccountId, activeNetwork, provider, assertCurrent: assertSigningCurrent, } = this.captureEvmSigningContext();
2052
2122
  if (networkUrl !== activeNetwork.url)
@@ -2063,7 +2133,7 @@ export class EthereumTransactions {
2063
2133
  const submission = this.captureSubmission('erc721', activeAccount, activeAccountType, activeAccountId, activeNetwork, () => {
2064
2134
  assertApproval();
2065
2135
  assertSigningCurrent();
2066
- });
2136
+ }, { walletNonce: { nonce, source: 'automatic' } });
2067
2137
  const assertCurrent = submission.assertCurrent;
2068
2138
  const sendERC721Token = async () => {
2069
2139
  assertCurrent();
@@ -2073,7 +2143,6 @@ export class EthereumTransactions {
2073
2143
  const _contract = new Contract(tokenAddress, getErc21Abi(), provider);
2074
2144
  if (isLegacy) {
2075
2145
  const overrides = {
2076
- nonce: await provider.getTransactionCount(activeAccountAddress, 'pending'),
2077
2146
  gasPrice,
2078
2147
  gasLimit,
2079
2148
  type: 0, // Explicitly set Type 0 for legacy NFT transfers
@@ -2082,7 +2151,6 @@ export class EthereumTransactions {
2082
2151
  }
2083
2152
  else {
2084
2153
  const overrides = {
2085
- nonce: await provider.getTransactionCount(activeAccountAddress, 'pending'),
2086
2154
  maxPriorityFeePerGas,
2087
2155
  maxFeePerGas,
2088
2156
  gasLimit,
@@ -2091,6 +2159,7 @@ export class EthereumTransactions {
2091
2159
  }
2092
2160
  return await sendLocalEvmTransaction(provider, decryptedPrivateKey, {
2093
2161
  ...transferMethod,
2162
+ nonce,
2094
2163
  chainId: activeNetwork.chainId,
2095
2164
  from: activeAccountAddress,
2096
2165
  value: '0x0',
@@ -2101,8 +2170,6 @@ export class EthereumTransactions {
2101
2170
  }
2102
2171
  };
2103
2172
  const sendERC721TokenOnLedger = async () => {
2104
- assertCurrent();
2105
- const transactionNonce = await provider.getTransactionCount(activeAccountAddress, 'pending');
2106
2173
  assertCurrent();
2107
2174
  try {
2108
2175
  const _contract = new Contract(tokenAddress, getErc21Abi(), provider);
@@ -2119,7 +2186,7 @@ export class EthereumTransactions {
2119
2186
  gasLimit,
2120
2187
  gasPrice,
2121
2188
  data: txData,
2122
- nonce: transactionNonce,
2189
+ nonce,
2123
2190
  chainId: activeNetwork.chainId,
2124
2191
  type: 0,
2125
2192
  };
@@ -2132,7 +2199,7 @@ export class EthereumTransactions {
2132
2199
  maxFeePerGas,
2133
2200
  maxPriorityFeePerGas,
2134
2201
  data: txData,
2135
- nonce: transactionNonce,
2202
+ nonce,
2136
2203
  chainId: activeNetwork.chainId,
2137
2204
  type: 2, // Need explicit type for hardware wallet serialization
2138
2205
  };
@@ -2154,7 +2221,7 @@ export class EthereumTransactions {
2154
2221
  const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
2155
2222
  assertCurrent();
2156
2223
  assertHardwareTransactionSender(signedTx, activeAccount.address);
2157
- const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, rawTx);
2224
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, ({ rawTransaction, admit }) => admit(() => provider.sendTransaction(rawTransaction)), submission, undefined, rawTx);
2158
2225
  return finalTx;
2159
2226
  }
2160
2227
  catch (error) {
@@ -2170,8 +2237,6 @@ export class EthereumTransactions {
2170
2237
  }
2171
2238
  };
2172
2239
  const sendERC721TokenOnTrezor = async () => {
2173
- assertCurrent();
2174
- const transactionNonce = await provider.getTransactionCount(activeAccountAddress, 'pending');
2175
2240
  assertCurrent();
2176
2241
  try {
2177
2242
  const _contract = new Contract(tokenAddress, getErc21Abi(), provider);
@@ -2187,7 +2252,7 @@ export class EthereumTransactions {
2187
2252
  value: '0x0',
2188
2253
  gasLimit: this.toHex0x(gasLimit, 'gasLimit'),
2189
2254
  gasPrice: this.toHex0x(gasPrice, 'gasPrice'),
2190
- nonce: this.toBigNumber(transactionNonce)._hex,
2255
+ nonce: this.toBigNumber(nonce)._hex,
2191
2256
  chainId: activeNetwork.chainId,
2192
2257
  data: txData,
2193
2258
  };
@@ -2200,7 +2265,7 @@ export class EthereumTransactions {
2200
2265
  gasLimit: this.toHex0x(gasLimit, 'gasLimit'),
2201
2266
  maxFeePerGas: this.toHex0x(maxFeePerGas, 'maxFeePerGas'),
2202
2267
  maxPriorityFeePerGas: this.toHex0x(maxPriorityFeePerGas, 'maxPriorityFeePerGas'),
2203
- nonce: this.toBigNumber(transactionNonce)._hex,
2268
+ nonce: this.toBigNumber(nonce)._hex,
2204
2269
  chainId: activeNetwork.chainId,
2205
2270
  data: txData,
2206
2271
  };
@@ -2229,7 +2294,7 @@ export class EthereumTransactions {
2229
2294
  gasLimit,
2230
2295
  gasPrice,
2231
2296
  data: txData,
2232
- nonce: transactionNonce,
2297
+ nonce,
2233
2298
  chainId: activeNetwork.chainId,
2234
2299
  type: 0,
2235
2300
  };
@@ -2242,7 +2307,7 @@ export class EthereumTransactions {
2242
2307
  maxFeePerGas,
2243
2308
  maxPriorityFeePerGas,
2244
2309
  data: txData,
2245
- nonce: transactionNonce,
2310
+ nonce,
2246
2311
  chainId: activeNetwork.chainId,
2247
2312
  type: 2, // Need explicit type for hardware wallet serialization
2248
2313
  };
@@ -2250,7 +2315,7 @@ export class EthereumTransactions {
2250
2315
  const signedTx = serializeTransaction(txFormattedForEthers, trezorSignature(signature.payload));
2251
2316
  assertCurrent();
2252
2317
  assertHardwareTransactionSender(signedTx, activeAccount.address);
2253
- const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, expectedUnsignedTransaction);
2318
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, ({ rawTransaction, admit }) => admit(() => provider.sendTransaction(rawTransaction)), submission, undefined, expectedUnsignedTransaction);
2254
2319
  return finalTx;
2255
2320
  }
2256
2321
  catch (error) {
@@ -2277,7 +2342,7 @@ export class EthereumTransactions {
2277
2342
  }
2278
2343
  };
2279
2344
  this.sendSignedErc1155Transaction = async (request, approval) => {
2280
- const { assertApproval, signal } = this.captureReviewedTokenApproval(approval, 'ERC1155');
2345
+ const { assertApproval, signal, nonce } = this.captureReviewedTokenApproval(approval, 'ERC1155');
2281
2346
  const { receiver, tokenAddress, tokenId, networkUrl } = request;
2282
2347
  const { activeAccount, activeAccountType, activeAccountId, activeNetwork, provider, assertCurrent: assertSigningCurrent, } = this.captureEvmSigningContext();
2283
2348
  if (networkUrl !== activeNetwork.url)
@@ -2297,7 +2362,7 @@ export class EthereumTransactions {
2297
2362
  const submission = this.captureSubmission('erc1155', activeAccount, activeAccountType, activeAccountId, activeNetwork, () => {
2298
2363
  assertApproval();
2299
2364
  assertSigningCurrent();
2300
- });
2365
+ }, { walletNonce: { nonce, source: 'automatic' } });
2301
2366
  const assertCurrent = submission.assertCurrent;
2302
2367
  const sendERC1155Token = async () => {
2303
2368
  assertCurrent();
@@ -2309,7 +2374,6 @@ export class EthereumTransactions {
2309
2374
  let overrides;
2310
2375
  if (isLegacy) {
2311
2376
  overrides = {
2312
- nonce: await provider.getTransactionCount(activeAccountAddress, 'pending'),
2313
2377
  gasPrice,
2314
2378
  gasLimit,
2315
2379
  type: 0, // Explicitly set Type 0 for legacy ERC1155 transfers
@@ -2317,7 +2381,6 @@ export class EthereumTransactions {
2317
2381
  }
2318
2382
  else {
2319
2383
  overrides = {
2320
- nonce: await provider.getTransactionCount(activeAccountAddress, 'pending'),
2321
2384
  maxPriorityFeePerGas,
2322
2385
  maxFeePerGas,
2323
2386
  gasLimit,
@@ -2326,6 +2389,7 @@ export class EthereumTransactions {
2326
2389
  transferMethod = await _contract.safeTransferFrom.populateTransaction(activeAccountAddress, receiver, normalizedTokenId, amount, '0x', normalizeEthersOverrides(overrides));
2327
2390
  return await sendLocalEvmTransaction(provider, decryptedPrivateKey, {
2328
2391
  ...transferMethod,
2392
+ nonce,
2329
2393
  chainId: activeNetwork.chainId,
2330
2394
  from: activeAccountAddress,
2331
2395
  value: '0x0',
@@ -2336,8 +2400,6 @@ export class EthereumTransactions {
2336
2400
  }
2337
2401
  };
2338
2402
  const sendERC1155TokenOnLedger = async () => {
2339
- assertCurrent();
2340
- const transactionNonce = await provider.getTransactionCount(activeAccountAddress, 'pending');
2341
2403
  assertCurrent();
2342
2404
  try {
2343
2405
  const _contract = new Contract(tokenAddress, getErc55Abi(), provider);
@@ -2350,7 +2412,7 @@ export class EthereumTransactions {
2350
2412
  gasLimit,
2351
2413
  gasPrice,
2352
2414
  data: txData,
2353
- nonce: transactionNonce,
2415
+ nonce,
2354
2416
  chainId: activeNetwork.chainId,
2355
2417
  type: 0,
2356
2418
  };
@@ -2363,7 +2425,7 @@ export class EthereumTransactions {
2363
2425
  maxFeePerGas,
2364
2426
  maxPriorityFeePerGas,
2365
2427
  data: txData,
2366
- nonce: transactionNonce,
2428
+ nonce,
2367
2429
  chainId: activeNetwork.chainId,
2368
2430
  type: 2, // Need explicit type for hardware wallet serialization
2369
2431
  };
@@ -2385,7 +2447,7 @@ export class EthereumTransactions {
2385
2447
  const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
2386
2448
  assertCurrent();
2387
2449
  assertHardwareTransactionSender(signedTx, activeAccount.address);
2388
- const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, rawTx);
2450
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, ({ rawTransaction, admit }) => admit(() => provider.sendTransaction(rawTransaction)), submission, undefined, rawTx);
2389
2451
  return finalTx;
2390
2452
  }
2391
2453
  catch (error) {
@@ -2401,8 +2463,6 @@ export class EthereumTransactions {
2401
2463
  }
2402
2464
  };
2403
2465
  const sendERC1155TokenOnTrezor = async () => {
2404
- assertCurrent();
2405
- const transactionNonce = await provider.getTransactionCount(activeAccountAddress, 'pending');
2406
2466
  assertCurrent();
2407
2467
  try {
2408
2468
  const _contract = new Contract(tokenAddress, getErc55Abi(), provider);
@@ -2414,7 +2474,7 @@ export class EthereumTransactions {
2414
2474
  value: '0x0',
2415
2475
  gasLimit: this.toHex0x(gasLimit, 'gasLimit'),
2416
2476
  gasPrice: this.toHex0x(gasPrice, 'gasPrice'),
2417
- nonce: this.toBigNumber(transactionNonce)._hex,
2477
+ nonce: this.toBigNumber(nonce)._hex,
2418
2478
  chainId: activeNetwork.chainId,
2419
2479
  data: txData,
2420
2480
  };
@@ -2426,7 +2486,7 @@ export class EthereumTransactions {
2426
2486
  gasLimit: this.toHex0x(gasLimit, 'gasLimit'),
2427
2487
  maxFeePerGas: this.toHex0x(maxFeePerGas, 'maxFeePerGas'),
2428
2488
  maxPriorityFeePerGas: this.toHex0x(maxPriorityFeePerGas, 'maxPriorityFeePerGas'),
2429
- nonce: this.toBigNumber(transactionNonce)._hex,
2489
+ nonce: this.toBigNumber(nonce)._hex,
2430
2490
  chainId: activeNetwork.chainId,
2431
2491
  data: txData,
2432
2492
  };
@@ -2453,7 +2513,7 @@ export class EthereumTransactions {
2453
2513
  gasLimit,
2454
2514
  gasPrice,
2455
2515
  data: txData,
2456
- nonce: transactionNonce,
2516
+ nonce,
2457
2517
  chainId: activeNetwork.chainId,
2458
2518
  type: 0,
2459
2519
  };
@@ -2466,7 +2526,7 @@ export class EthereumTransactions {
2466
2526
  maxFeePerGas,
2467
2527
  maxPriorityFeePerGas,
2468
2528
  data: txData,
2469
- nonce: transactionNonce,
2529
+ nonce,
2470
2530
  chainId: activeNetwork.chainId,
2471
2531
  type: 2, // Need explicit type for hardware wallet serialization
2472
2532
  };
@@ -2474,7 +2534,7 @@ export class EthereumTransactions {
2474
2534
  const signedTx = serializeTransaction(txFormattedForEthers, trezorSignature(signature.payload));
2475
2535
  assertCurrent();
2476
2536
  assertHardwareTransactionSender(signedTx, activeAccount.address);
2477
- const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, expectedUnsignedTransaction);
2537
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, ({ rawTransaction, admit }) => admit(() => provider.sendTransaction(rawTransaction)), submission, undefined, expectedUnsignedTransaction);
2478
2538
  return finalTx;
2479
2539
  }
2480
2540
  catch (error) {