@sidhujag/sysweb3-keyring 2.1.0 → 2.1.2

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 (53) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +46 -108
  3. package/cjs/evm-fees.js +223 -0
  4. package/cjs/evm-fees.js.map +1 -0
  5. package/cjs/keyring-core.js +1 -227
  6. package/cjs/keyring-core.js.map +1 -1
  7. package/cjs/keyring-manager.js +4 -3
  8. package/cjs/keyring-manager.js.map +1 -1
  9. package/cjs/software.js +12 -11
  10. package/cjs/software.js.map +1 -1
  11. package/cjs/transactions/ethereum.js +268 -195
  12. package/cjs/transactions/ethereum.js.map +1 -1
  13. package/cjs/transactions/evm-local-signer.js +31 -8
  14. package/cjs/transactions/evm-local-signer.js.map +1 -1
  15. package/cjs/transactions/evm-submission.js +200 -0
  16. package/cjs/transactions/evm-submission.js.map +1 -0
  17. package/cjs/transactions/index.js +1 -0
  18. package/cjs/transactions/index.js.map +1 -1
  19. package/cjs/transactions/prepared-evm.js +5 -6
  20. package/cjs/transactions/prepared-evm.js.map +1 -1
  21. package/cjs/types.js.map +1 -1
  22. package/cjs/utils/derivation-paths.js +4 -3
  23. package/cjs/utils/derivation-paths.js.map +1 -1
  24. package/cjs/utils/watch-descriptor.js +3 -2
  25. package/cjs/utils/watch-descriptor.js.map +1 -1
  26. package/esm/types.js.map +1 -1
  27. package/esm/utils/derivation-paths.js +2 -1
  28. package/esm/utils/derivation-paths.js.map +1 -1
  29. package/package.json +24 -11
  30. package/types/evm-fees.d.ts +42 -0
  31. package/types/keyring-core.d.ts +3 -8
  32. package/types/keyring-manager.d.ts +8 -3
  33. package/types/software.d.ts +6 -11
  34. package/types/transactions/ethereum.d.ts +10 -8
  35. package/types/transactions/evm-local-signer.d.ts +4 -1
  36. package/types/transactions/evm-submission.d.ts +51 -0
  37. package/types/transactions/index.d.ts +1 -0
  38. package/types/types.d.ts +4 -0
  39. package/cjs/bip84-native.js +0 -8
  40. package/cjs/bip84-native.js.map +0 -1
  41. package/cjs/public-bip84.js +0 -157
  42. package/cjs/public-bip84.js.map +0 -1
  43. package/cjs/software-utxo-account.js +0 -12
  44. package/cjs/software-utxo-account.js.map +0 -1
  45. package/cjs/transactions/bip84-native-plan.js +0 -481
  46. package/cjs/transactions/bip84-native-plan.js.map +0 -1
  47. package/cjs/transactions/prepared-bip84.js +0 -214
  48. package/cjs/transactions/prepared-bip84.js.map +0 -1
  49. package/types/bip84-native.d.ts +0 -3
  50. package/types/public-bip84.d.ts +0 -39
  51. package/types/software-utxo-account.d.ts +0 -13
  52. package/types/transactions/bip84-native-plan.d.ts +0 -83
  53. package/types/transactions/prepared-bip84.d.ts +0 -27
@@ -8,11 +8,14 @@ const eth_sig_util_1 = require("@metamask/eth-sig-util");
8
8
  const sysweb3_network_1 = require("@sidhujag/sysweb3-network");
9
9
  const sysweb3_utils_1 = require("@sidhujag/sysweb3-utils");
10
10
  const transaction_1 = require("ethers/transaction");
11
+ const utils_1 = require("ethers/utils");
11
12
  const omit_1 = __importDefault(require("lodash/omit"));
12
13
  const ethers_v6_1 = require("../ethers-v6");
14
+ const evm_fees_1 = require("../evm-fees");
13
15
  const providers_1 = require("../providers");
14
16
  const types_1 = require("../types");
15
17
  const evm_local_signer_1 = require("./evm-local-signer");
18
+ const evm_submission_1 = require("./evm-submission");
16
19
  const derivation_paths_1 = require("../utils/derivation-paths");
17
20
  const hardwareSignerUnavailable = (hardware) => Object.assign(new Error(`${hardware} signer is unavailable in this host`), {
18
21
  code: 'HARDWARE_SIGNER_UNAVAILABLE',
@@ -25,7 +28,11 @@ const assertHardwareTransactionSender = (signedTx, expectedAddress) => {
25
28
  });
26
29
  }
27
30
  };
28
- const stripHexPrefix = (value) => value.startsWith('0x') || value.startsWith('0X') ? value.slice(2) : value;
31
+ const stripHexPrefix = (value) => {
32
+ return value.startsWith('0x') || value.startsWith('0X')
33
+ ? value.slice(2)
34
+ : value;
35
+ };
29
36
  // Connect returns hexadecimal signature fields; normalize their encoding
30
37
  // without mutating the response or reparsing an already numeric recovery v.
31
38
  const trezorSignature = (signature) => {
@@ -47,55 +54,14 @@ const trezorSignature = (signature) => {
47
54
  };
48
55
  };
49
56
  const normalizeEthersOverrideValue = (value) => (0, ethers_v6_1.normalizeTxValue)(value);
50
- const nonNegativeFeeQuantity = (value, label) => {
51
- // Match the exported BigNumberish object contract without trusting a
52
- // private marker or BigNumber.from's null/empty-to-zero coercions. Normalize
53
- // once so validation and conversion use the same integer representation.
54
- if (value !== null && typeof value === 'object') {
55
- const objectValue = value;
56
- value =
57
- objectValue._hex ??
58
- objectValue.hex ??
59
- (typeof objectValue.toString === 'function'
60
- ? objectValue.toString()
61
- : undefined);
62
- if (typeof value !== 'string')
63
- throw new Error(`Invalid ${label}`);
64
- }
65
- if (!((typeof value === 'string' && /^(?:0x[\da-f]+|\d+)$/i.test(value)) ||
66
- (typeof value === 'number' &&
67
- Number.isSafeInteger(value) &&
68
- value >= 0) ||
69
- (typeof value === 'bigint' && value >= 0n)))
70
- throw new Error(`Invalid ${label}`);
71
- const quantity = ethers_v6_1.BigNumber.from(value);
72
- if (quantity.isNegative())
73
- throw new Error(`Invalid ${label}`);
74
- return quantity;
75
- };
76
- const isUnsupportedPriorityFeeRpc = (error) => {
77
- const unsupportedCode = (code) => code === -32601 || code === 4200;
78
- if (unsupportedCode(error?.code))
79
- return true;
80
- // Ethers' HTTP normalizer retains the matched JSON-RPC error and request
81
- // under one of these two shapes. A generic outer unsupported/network error
82
- // is not evidence that this particular RPC method is unavailable.
83
- if (!['UNKNOWN_ERROR', 'UNSUPPORTED_OPERATION'].includes(error?.code)) {
84
- return false;
85
- }
86
- if (error?.operation != null &&
87
- error.operation !== 'eth_maxPriorityFeePerGas') {
88
- return false;
89
- }
90
- return [
91
- { cause: error.error, payload: error.payload },
92
- { cause: error.info?.error, payload: error.info?.payload },
93
- ].some(({ cause, payload }) => cause?.name !== 'AbortError' &&
94
- unsupportedCode(cause?.code) &&
95
- payload?.method === 'eth_maxPriorityFeePerGas' &&
96
- Array.isArray(payload.params) &&
97
- payload.params.length === 0);
57
+ // The same protocol bound applies to software and external-device envelopes.
58
+ // Keep this at serialization as well as before handing a request to a device.
59
+ const assertTransactionGasLimit = (transaction) => (0, evm_fees_1.assertEvmGasLimit)((0, evm_fees_1.parseEvmFeeQuantity)(transaction.gasLimit, 'gas limit'), (0, evm_fees_1.parseEvmFeeQuantity)(transaction.chainId ?? 0, 'chain ID'));
60
+ const serializeTransaction = (...args) => {
61
+ const snapshot = (0, evm_local_signer_1.normalizeGasLimitedTransaction)(args[0]);
62
+ return (0, ethers_v6_1.serializeTransaction)(snapshot, args[1]);
98
63
  };
64
+ const nonNegativeFeeQuantity = (value, label) => ethers_v6_1.BigNumber.from((0, evm_fees_1.parseEvmFeeQuantity)(value, label));
99
65
  const normalizeEthersOverrides = (overrides) => {
100
66
  const normalized = { ...overrides };
101
67
  for (const field of [
@@ -159,7 +125,7 @@ const assertSynchronousOriginalValidation = (result, message = 'Original transac
159
125
  }
160
126
  };
161
127
  class EthereumTransactions {
162
- constructor(getNetwork, getDecryptedPrivateKey, getState, ledgerSigner, trezorSigner, captureSigningContext) {
128
+ constructor(getNetwork, getDecryptedPrivateKey, getState, ledgerSigner, trezorSigner, captureSigningContext, captureSubmissionLifecycle) {
163
129
  // Allow manual override of gas parameters when automatic estimation fails
164
130
  this.gasOverrides = {};
165
131
  this.requireLedgerSigner = () => {
@@ -182,22 +148,29 @@ class EthereumTransactions {
182
148
  if (type === types_1.KeyringAccountType.Trezor || account?.isTrezorWallet)
183
149
  this.requireTrezorSigner();
184
150
  };
185
- this.captureEvmSigningContext = () => {
186
- this.assertHardwareSignerAvailable();
187
- const { activeAccountType, activeAccountId, accounts, activeNetwork } = this.getState();
188
- const selectedAccount = accounts[activeAccountType]?.[activeAccountId];
189
- if (!selectedAccount)
151
+ this.captureEvmSigningContext = (target) => {
152
+ this.assertHardwareSignerAvailable(target);
153
+ const state = this.getState();
154
+ const selectedType = state.activeAccountType;
155
+ const selectedId = state.activeAccountId;
156
+ const selectedAddress = state.accounts[selectedType]?.[selectedId]?.address;
157
+ const activeAccountType = target?.type ?? selectedType;
158
+ const activeAccountId = target?.id ?? selectedId;
159
+ const selectedAccount = state.accounts[activeAccountType]?.[activeAccountId];
160
+ if (!selectedAccount || !selectedAddress)
190
161
  throw new Error('Active account not found');
191
162
  const account = { address: selectedAccount.address };
192
- const network = { ...activeNetwork };
163
+ const network = { ...state.activeNetwork };
193
164
  const assertSession = this.captureSigningContext?.();
194
165
  const provider = this.web3Provider;
195
166
  const assertCurrent = () => {
196
167
  assertSession?.();
197
168
  const current = this.getState();
198
- const currentAccount = current.accounts[current.activeAccountType]?.[current.activeAccountId];
199
- if (current.activeAccountId !== activeAccountId ||
200
- current.activeAccountType !== activeAccountType ||
169
+ const currentAccount = current.accounts[activeAccountType]?.[activeAccountId];
170
+ if (current.activeAccountId !== selectedId ||
171
+ current.activeAccountType !== selectedType ||
172
+ current.accounts[selectedType]?.[selectedId]?.address?.toLowerCase() !==
173
+ selectedAddress.toLowerCase() ||
201
174
  currentAccount?.address?.toLowerCase() !==
202
175
  account.address.toLowerCase() ||
203
176
  [current.activeNetwork, this.getNetwork()].some((selected) => selected.kind !== network.kind ||
@@ -217,6 +190,17 @@ class EthereumTransactions {
217
190
  assertCurrent,
218
191
  };
219
192
  };
193
+ this.captureSubmission = (operation, account, type, id, network, assertCurrent, originalHash) => (0, evm_submission_1.captureEvmSubmission)({
194
+ operation,
195
+ account: { address: account.address, type, id },
196
+ network: {
197
+ kind: network.kind,
198
+ chainId: network.chainId,
199
+ slip44: network.slip44,
200
+ url: network.url,
201
+ },
202
+ ...(originalHash !== undefined ? { originalHash } : {}),
203
+ }, assertCurrent, this.captureSubmissionLifecycle);
220
204
  this.signTypedData = async (addr, typedData, version) => {
221
205
  const { address, decryptedPrivateKey } = this.getDecryptedPrivateKey();
222
206
  const { activeAccountType, accounts, activeAccountId } = this.getState();
@@ -615,68 +599,21 @@ class EthereumTransactions {
615
599
  }
616
600
  };
617
601
  this.getFeeDataWithDynamicMaxPriorityFeePerGas = async (provider = this.web3Provider) => {
618
- // Keep the actual legacy-price recommendation separate from the buffered
619
- // EIP-1559 maximum. A failed read is not evidence of a zero/legacy fee.
620
- const gasPrice = nonNegativeFeeQuantity(await provider.getGasPrice(), 'gas price');
621
- const block = await provider.getBlock('latest');
622
- if (!block)
623
- throw new Error('Block not found');
624
- if (block.baseFeePerGas == null) {
625
- return {
626
- gasPrice,
627
- maxFeePerGas: ethers_v6_1.BigNumber.from(0),
628
- maxPriorityFeePerGas: ethers_v6_1.BigNumber.from(0),
629
- };
630
- }
631
- const baseFeePerGas = nonNegativeFeeQuantity(block.baseFeePerGas, 'base fee');
632
- let maxPriorityFeePerGas;
633
- try {
634
- maxPriorityFeePerGas = nonNegativeFeeQuantity(await provider.send('eth_maxPriorityFeePerGas', []), 'priority fee');
635
- }
636
- catch (error) {
637
- if (error?.code === 'SYSWEB3_RPC_RESPONSE_INVALID' ||
638
- error?.name === 'AbortError' ||
639
- this.abortController.signal.aborted ||
640
- provider.signal?.aborted ||
641
- !isUnsupportedPriorityFeeRpc(error))
642
- throw error;
643
- // An explicitly unsupported method may use actual observed history,
644
- // including zero rewards. Missing/corrupt history never invents a tip.
645
- const history = await provider.send('eth_feeHistory', [
646
- '0x5',
647
- 'latest',
648
- [25, 50, 75],
649
- ]);
650
- if (!Array.isArray(history?.reward) || history.reward.length === 0)
651
- throw new Error('Priority fee history unavailable');
652
- const recentFees = history.reward
653
- .map((reward) => {
654
- if (!Array.isArray(reward) || reward.length !== 3)
655
- throw new Error('Invalid priority fee history');
656
- return nonNegativeFeeQuantity(reward[1], 'priority fee history');
657
- })
658
- .sort((a, b) => (a.lt(b) ? -1 : a.gt(b) ? 1 : 0));
659
- maxPriorityFeePerGas = recentFees[Math.floor(recentFees.length / 2)]
660
- .mul(150)
661
- .div(100);
662
- }
663
- if (this.gasOverrides.minPriorityFee &&
664
- maxPriorityFeePerGas.lt(this.gasOverrides.minPriorityFee))
665
- maxPriorityFeePerGas = this.gasOverrides.minPriorityFee;
666
- const baselineMaxFee = gasPrice.mul(120).div(100);
667
- const calculatedMaxFee = baseFeePerGas
668
- .mul(this.gasOverrides.feeMultiplier || 250)
669
- .div(100)
670
- .add(maxPriorityFeePerGas);
671
- let maxFeePerGas = calculatedMaxFee.gt(baselineMaxFee)
672
- ? calculatedMaxFee
673
- : baselineMaxFee;
674
- const minMaxFee = maxPriorityFeePerGas.mul(120).div(100);
675
- if (maxFeePerGas.lt(minMaxFee))
676
- maxFeePerGas = minMaxFee;
677
- if (maxFeePerGas.isZero())
678
- throw new Error('Invalid EIP-1559 maximum fee');
679
- return { gasPrice, baseFeePerGas, maxFeePerGas, maxPriorityFeePerGas };
602
+ const fee = await (0, evm_fees_1.getEvmFeeData)(provider, {
603
+ signal: this.abortController.signal,
604
+ minPriorityFee: this.gasOverrides.minPriorityFee,
605
+ feeMultiplier: this.gasOverrides.feeMultiplier,
606
+ });
607
+ return {
608
+ gasPrice: ethers_v6_1.BigNumber.from(fee.gasPrice),
609
+ ...(fee.baseFeePerGas === undefined
610
+ ? {}
611
+ : {
612
+ baseFeePerGas: ethers_v6_1.BigNumber.from(fee.baseFeePerGas),
613
+ }),
614
+ maxFeePerGas: ethers_v6_1.BigNumber.from(fee.maxFeePerGas),
615
+ maxPriorityFeePerGas: ethers_v6_1.BigNumber.from(fee.maxPriorityFeePerGas),
616
+ };
680
617
  };
681
618
  this.calculateNewGasValues = (oldTxsParams, isForCancel, isLegacy) => {
682
619
  const newGasValues = {
@@ -780,6 +717,9 @@ class EthereumTransactions {
780
717
  assertKeyringContext();
781
718
  };
782
719
  assertCurrent();
720
+ const submission = previewOnly
721
+ ? undefined
722
+ : this.captureSubmission('cancel', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertCurrent, txHash);
783
723
  const currentEip1559Fees = async () => {
784
724
  const feeData = await this.getFeeDataWithDynamicMaxPriorityFeePerGas(provider);
785
725
  assertCurrent();
@@ -956,7 +896,7 @@ class EthereumTransactions {
956
896
  type: 2, // Need explicit type for hardware wallet serialization
957
897
  };
958
898
  assertApprovedEnvelope(txFormattedForEthers);
959
- const rawTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers);
899
+ const rawTx = serializeTransaction(txFormattedForEthers);
960
900
  const signature = await this.requireLedgerSigner().evm.signEVMTransaction({
961
901
  rawTx: rawTx.replace('0x', ''),
962
902
  accountIndex: activeAccountId,
@@ -969,10 +909,10 @@ class EthereumTransactions {
969
909
  v: parseInt(signature.v, 16),
970
910
  };
971
911
  if (signature) {
972
- const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers, formattedSignature);
912
+ const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
973
913
  assertCurrent();
974
914
  assertHardwareTransactionSender(signedTx, activeAccount.address);
975
- const transactionResponse = await provider.sendTransaction(signedTx);
915
+ const transactionResponse = await (0, evm_submission_1.submitSignedEvmTransaction)(signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, rawTx);
976
916
  return {
977
917
  isCanceled: true,
978
918
  transaction: transactionResponse,
@@ -987,6 +927,7 @@ class EthereumTransactions {
987
927
  }
988
928
  catch (error) {
989
929
  return {
930
+ ...(0, evm_submission_1.evmSubmissionFailure)(error),
990
931
  isCanceled: false,
991
932
  error: true,
992
933
  };
@@ -1026,6 +967,8 @@ class EthereumTransactions {
1026
967
  : this.toHex0x(formattedTx.maxPriorityFeePerGas, 'maxPriorityFeePerGas');
1027
968
  }
1028
969
  assertApprovedEnvelope(txFormattedForTrezor);
970
+ assertTransactionGasLimit(txFormattedForTrezor);
971
+ const expectedUnsignedTransaction = serializeTransaction(txFormattedForTrezor);
1029
972
  const signature = await this.requireTrezorSigner().signEthTransaction({
1030
973
  coin: trezorCoin,
1031
974
  tx: txFormattedForTrezor,
@@ -1034,10 +977,10 @@ class EthereumTransactions {
1034
977
  });
1035
978
  assertCurrent();
1036
979
  if (signature.success) {
1037
- const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForTrezor, trezorSignature(signature.payload));
980
+ const signedTx = serializeTransaction(txFormattedForTrezor, trezorSignature(signature.payload));
1038
981
  assertCurrent();
1039
982
  assertHardwareTransactionSender(signedTx, activeAccount.address);
1040
- const transactionResponse = await provider.sendTransaction(signedTx);
983
+ const transactionResponse = await (0, evm_submission_1.submitSignedEvmTransaction)(signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, expectedUnsignedTransaction);
1041
984
  return {
1042
985
  isCanceled: true,
1043
986
  transaction: transactionResponse,
@@ -1052,6 +995,7 @@ class EthereumTransactions {
1052
995
  }
1053
996
  catch (error) {
1054
997
  return {
998
+ ...(0, evm_submission_1.evmSubmissionFailure)(error),
1055
999
  isCanceled: false,
1056
1000
  error: true,
1057
1001
  };
@@ -1065,7 +1009,7 @@ class EthereumTransactions {
1065
1009
  if (address?.toLowerCase() !== activeAccount.address.toLowerCase()) {
1066
1010
  throw new Error('Wallet signing context changed');
1067
1011
  }
1068
- const transactionResponse = await (0, evm_local_signer_1.sendLocalEvmTransaction)(provider, decryptedPrivateKey, changedTxToCancel, assertCurrent, assertCurrent, assertApprovedEnvelope);
1012
+ const transactionResponse = await (0, evm_local_signer_1.sendLocalEvmTransaction)(provider, decryptedPrivateKey, changedTxToCancel, assertCurrent, assertCurrent, assertApprovedEnvelope, submission);
1069
1013
  if (transactionResponse) {
1070
1014
  return {
1071
1015
  isCanceled: true,
@@ -1080,6 +1024,7 @@ class EthereumTransactions {
1080
1024
  }
1081
1025
  catch (error) {
1082
1026
  return {
1027
+ ...(0, evm_submission_1.evmSubmissionFailure)(error),
1083
1028
  isCanceled: false,
1084
1029
  error: true,
1085
1030
  };
@@ -1115,11 +1060,15 @@ class EthereumTransactions {
1115
1060
  catch (error) {
1116
1061
  // A broadcast transport error may mean the node accepted the signed
1117
1062
  // transaction. Derive this marker from this invocation, never RPC data.
1118
- const submissionError = Object.assign(new Error(error instanceof Error ? error.message : String(error)), error, { transactionNotBroadcast: !broadcastAttempted });
1063
+ const submissionError = Object.assign(new Error(error instanceof Error ? error.message : String(error)), error, {
1064
+ transactionNotBroadcast: !broadcastAttempted ||
1065
+ (0, evm_submission_1.evmSubmissionFailure)(error).transactionNotBroadcast,
1066
+ });
1119
1067
  if (error instanceof Error) {
1120
1068
  submissionError.name = error.name;
1121
1069
  submissionError.stack = error.stack;
1122
1070
  }
1071
+ (0, evm_submission_1.copyEvmSubmissionFailure)(error, submissionError);
1123
1072
  throw submissionError;
1124
1073
  }
1125
1074
  };
@@ -1140,6 +1089,54 @@ class EthereumTransactions {
1140
1089
  beforeBroadcast();
1141
1090
  };
1142
1091
  assertCurrent();
1092
+ const submission = this.captureSubmission('send', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertCurrent);
1093
+ // Detach supplied values before any RPC/device await. Autofill updates only
1094
+ // this invocation; caller mutations cannot replace already supplied fields.
1095
+ if (!params || typeof params !== 'object' || Array.isArray(params))
1096
+ throw new TypeError('Invalid EVM transaction request');
1097
+ const supplied = { ...params };
1098
+ // Validate original scalar spellings before conversion. Normalization's
1099
+ // permissive zero coercion must not bypass the bounded wire parser.
1100
+ for (const field of [
1101
+ 'chainId',
1102
+ 'nonce',
1103
+ 'gasLimit',
1104
+ 'gasPrice',
1105
+ 'maxFeePerGas',
1106
+ 'maxPriorityFeePerGas',
1107
+ 'value',
1108
+ 'type',
1109
+ ]) {
1110
+ if (supplied[field] != null) {
1111
+ const value = (0, evm_fees_1.parseEvmFeeQuantity)(supplied[field], field);
1112
+ if (field === 'nonce' || field === 'type') {
1113
+ if (value > BigInt(Number.MAX_SAFE_INTEGER))
1114
+ throw new Error(`Invalid transaction ${field}`);
1115
+ supplied[field] = Number(value);
1116
+ }
1117
+ else
1118
+ supplied[field] = value;
1119
+ }
1120
+ }
1121
+ if (supplied.data != null)
1122
+ supplied.data = (0, utils_1.hexlify)(supplied.data);
1123
+ if (supplied.accessList != null)
1124
+ supplied.accessList = (0, transaction_1.accessListify)(supplied.accessList);
1125
+ params = supplied;
1126
+ assertCurrent();
1127
+ // Capture the reviewed nonce before any fee/RPC await. Hardware and the
1128
+ // final serializer must use the same immutable integer, including zero.
1129
+ let explicitHardwareNonce;
1130
+ if (activeAccountType === types_1.KeyringAccountType.Ledger ||
1131
+ activeAccountType === types_1.KeyringAccountType.Trezor) {
1132
+ const suppliedNonce = params.nonce;
1133
+ if (suppliedNonce != null) {
1134
+ const nonce = (0, evm_fees_1.parseEvmFeeQuantity)(suppliedNonce, 'transaction nonce');
1135
+ if (nonce > BigInt(Number.MAX_SAFE_INTEGER))
1136
+ throw new Error('Invalid transaction nonce');
1137
+ explicitHardwareNonce = Number(nonce);
1138
+ }
1139
+ }
1143
1140
  const explicitEip1559 = params.type != null && ethers_v6_1.BigNumber.from(params.type).eq(2);
1144
1141
  const requestedEip1559 = explicitEip1559 ||
1145
1142
  params.maxFeePerGas != null ||
@@ -1160,7 +1157,7 @@ class EthereumTransactions {
1160
1157
  validateEip1559Fees();
1161
1158
  // Only apply a minimum gas limit when a network-specific override is set.
1162
1159
  // Otherwise keep the user/dapp-approved gas limit unchanged.
1163
- if (params.gasLimit && this.gasOverrides.minGasLimit) {
1160
+ if (params.gasLimit != null && this.gasOverrides.minGasLimit) {
1164
1161
  const minGasLimit = this.gasOverrides.minGasLimit;
1165
1162
  const currentGasLimit = ethers_v6_1.BigNumber.from(params.gasLimit);
1166
1163
  if (currentGasLimit.lt(minGasLimit)) {
@@ -1176,12 +1173,16 @@ class EthereumTransactions {
1176
1173
  ...params,
1177
1174
  from: params.from || activeAccount.address,
1178
1175
  }, ['gasPrice']);
1179
- if (!params.gasLimit) {
1176
+ if (params.gasLimit == null) {
1180
1177
  params.gasLimit = await provider.estimateGas(txForEstimation);
1181
1178
  assertCurrent();
1182
1179
  }
1180
+ assertTransactionGasLimit({
1181
+ gasLimit: params.gasLimit,
1182
+ chainId: activeNetwork.chainId,
1183
+ });
1183
1184
  if (isLegacy) {
1184
- if (!params.gasPrice) {
1185
+ if (params.gasPrice == null) {
1185
1186
  params.gasPrice = await provider.getGasPrice();
1186
1187
  assertCurrent();
1187
1188
  }
@@ -1207,7 +1208,8 @@ class EthereumTransactions {
1207
1208
  validateEip1559Fees();
1208
1209
  const sendEVMLedgerTransaction = async () => {
1209
1210
  assertCurrent();
1210
- const transactionNonce = await provider.getTransactionCount(activeAccount.address, 'pending');
1211
+ const transactionNonce = explicitHardwareNonce ??
1212
+ (await provider.getTransactionCount(activeAccount.address, 'pending'));
1211
1213
  assertCurrent();
1212
1214
  const formatParams = isLegacy
1213
1215
  ? (0, omit_1.default)(params, 'from')
@@ -1225,7 +1227,7 @@ class EthereumTransactions {
1225
1227
  chainId: activeNetwork.chainId,
1226
1228
  type: 2, // Need explicit type for hardware wallet serialization
1227
1229
  };
1228
- const rawTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers);
1230
+ const rawTx = serializeTransaction(txFormattedForEthers);
1229
1231
  const signature = await this.requireLedgerSigner().evm.signEVMTransaction({
1230
1232
  rawTx: rawTx.replace('0x', ''),
1231
1233
  accountIndex: activeAccountId,
@@ -1239,10 +1241,9 @@ class EthereumTransactions {
1239
1241
  };
1240
1242
  if (signature) {
1241
1243
  try {
1242
- const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers, formattedSignature);
1244
+ const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
1243
1245
  assertHardwareTransactionSender(signedTx, activeAccount.address);
1244
- assertBeforeBroadcast();
1245
- const finalTx = await provider.sendTransaction(signedTx);
1246
+ const finalTx = await (0, evm_submission_1.submitSignedEvmTransaction)(signedTx, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, rawTx);
1246
1247
  return finalTx;
1247
1248
  }
1248
1249
  catch (error) {
@@ -1255,7 +1256,8 @@ class EthereumTransactions {
1255
1256
  };
1256
1257
  const sendEVMTrezorTransaction = async () => {
1257
1258
  assertCurrent();
1258
- const transactionNonce = await provider.getTransactionCount(activeAccount.address, 'pending');
1259
+ const transactionNonce = explicitHardwareNonce ??
1260
+ (await provider.getTransactionCount(activeAccount.address, 'pending'));
1259
1261
  assertCurrent();
1260
1262
  let txFormattedForTrezor = {};
1261
1263
  const formatParams = isLegacy
@@ -1265,6 +1267,7 @@ class EthereumTransactions {
1265
1267
  case true:
1266
1268
  txFormattedForTrezor = {
1267
1269
  ...formatParams,
1270
+ gasPrice: this.toHex0x(params.gasPrice, 'gasPrice'),
1268
1271
  gasLimit: this.toHex0x(formatParams.gasLimit ?? params.gasLimit, 'gasLimit'),
1269
1272
  value: this.toHex0x(formatParams.value ?? params.value, 'value'),
1270
1273
  nonce: this.toBigNumber(transactionNonce)._hex,
@@ -1301,6 +1304,11 @@ class EthereumTransactions {
1301
1304
  };
1302
1305
  break;
1303
1306
  }
1307
+ assertTransactionGasLimit(txFormattedForTrezor);
1308
+ const expectedUnsignedTransaction = serializeTransaction({
1309
+ ...txFormattedForTrezor,
1310
+ type: isLegacy ? 0 : 2,
1311
+ });
1304
1312
  const signature = await this.requireTrezorSigner().signEthTransaction({
1305
1313
  index: `${activeAccountId}`,
1306
1314
  tx: txFormattedForTrezor,
@@ -1323,10 +1331,9 @@ class EthereumTransactions {
1323
1331
  chainId: activeNetwork.chainId,
1324
1332
  type: 2, // Need explicit type for hardware wallet serialization
1325
1333
  };
1326
- const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers, trezorSignature(signature.payload));
1334
+ const signedTx = serializeTransaction(txFormattedForEthers, trezorSignature(signature.payload));
1327
1335
  assertHardwareTransactionSender(signedTx, activeAccount.address);
1328
- assertBeforeBroadcast();
1329
- const finalTx = await provider.sendTransaction(signedTx);
1336
+ const finalTx = await (0, evm_submission_1.submitSignedEvmTransaction)(signedTx, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, expectedUnsignedTransaction);
1330
1337
  return finalTx;
1331
1338
  }
1332
1339
  catch (error) {
@@ -1351,7 +1358,7 @@ class EthereumTransactions {
1351
1358
  ? { ...params, type: 0 } // Force Type 0 for legacy transactions
1352
1359
  : (0, omit_1.default)(params, ['gasPrice']); // Strip gasPrice for EIP-1559
1353
1360
  try {
1354
- const transaction = await (0, evm_local_signer_1.sendLocalEvmTransaction)(provider, decryptedPrivateKey, tx, assertBeforeBroadcast, assertCurrent);
1361
+ const transaction = await (0, evm_local_signer_1.sendLocalEvmTransaction)(provider, decryptedPrivateKey, tx, assertBeforeBroadcast, assertCurrent, undefined, submission);
1355
1362
  // The broadcast response already has the hash. Pending transactions
1356
1363
  // have no block number, so a block lookup can fail after a successful
1357
1364
  // broadcast and incorrectly report the send as failed.
@@ -1380,7 +1387,14 @@ class EthereumTransactions {
1380
1387
  !/^\d{1,78}$/.test(approvedMaximumFee))) {
1381
1388
  throw new Error('Replacement fee approval required');
1382
1389
  }
1383
- const assertCurrent = () => assertCurrentContext?.();
1390
+ let signing;
1391
+ const assertCurrent = () => {
1392
+ const result = assertCurrentContext?.();
1393
+ assertSynchronousOriginalValidation(result, 'Transaction context guard must be synchronous');
1394
+ if (result !== undefined)
1395
+ throw new Error('Transaction context guard must return undefined');
1396
+ signing?.assertCurrent();
1397
+ };
1384
1398
  const assertBeforeBroadcast = () => {
1385
1399
  assertCurrent();
1386
1400
  beforeBroadcast?.();
@@ -1406,10 +1420,15 @@ class EthereumTransactions {
1406
1420
  (targetAccount && activeAccountType === types_1.KeyringAccountType.SmartAccount)) {
1407
1421
  throw new Error('Replacement signing account is unavailable');
1408
1422
  }
1423
+ if (!previewOnly)
1424
+ signing = this.captureEvmSigningContext(targetAccount);
1409
1425
  const expectedSender = activeAccount.address.toLowerCase();
1410
1426
  const requestedHash = txHash.toLowerCase();
1411
1427
  const provider = this.web3Provider;
1412
1428
  assertCurrent();
1429
+ const submission = previewOnly
1430
+ ? undefined
1431
+ : this.captureSubmission('speed-up', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertCurrent, txHash);
1413
1432
  const validateLookup = (transaction) => {
1414
1433
  assertCurrent();
1415
1434
  if (!transaction)
@@ -1547,7 +1566,7 @@ class EthereumTransactions {
1547
1566
  chainId: activeNetwork.chainId,
1548
1567
  type: 2, // Need explicit type for hardware wallet serialization
1549
1568
  };
1550
- const rawTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers);
1569
+ const rawTx = serializeTransaction(txFormattedForEthers);
1551
1570
  assertCurrent();
1552
1571
  const signature = await this.requireLedgerSigner().evm.signEVMTransaction({
1553
1572
  rawTx: rawTx.replace('0x', ''),
@@ -1560,11 +1579,10 @@ class EthereumTransactions {
1560
1579
  v: parseInt(signature.v, 16),
1561
1580
  };
1562
1581
  if (signature) {
1563
- const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers, formattedSignature);
1582
+ const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
1564
1583
  assertSignedSender(signedTx);
1565
1584
  assertHardwareTransactionSender(signedTx, activeAccount.address);
1566
- assertBeforeBroadcast();
1567
- const transactionResponse = await provider.sendTransaction(signedTx);
1585
+ const transactionResponse = await (0, evm_submission_1.submitSignedEvmTransaction)(signedTx, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, rawTx);
1568
1586
  return {
1569
1587
  isSpeedUp: true,
1570
1588
  transaction: transactionResponse,
@@ -1588,6 +1606,7 @@ class EthereumTransactions {
1588
1606
  ? 'REPLACEMENT_ALREADY_KNOWN'
1589
1607
  : 'REPLACEMENT_SEND_FAILED';
1590
1608
  return {
1609
+ ...(0, evm_submission_1.evmSubmissionFailure)(error),
1591
1610
  isSpeedUp: false,
1592
1611
  error: true,
1593
1612
  code,
@@ -1632,6 +1651,8 @@ class EthereumTransactions {
1632
1651
  : this.toHex0x(formattedTx.maxPriorityFeePerGas, 'maxPriorityFeePerGas');
1633
1652
  }
1634
1653
  assertCurrent();
1654
+ assertTransactionGasLimit(txFormattedForTrezor);
1655
+ const expectedUnsignedTransaction = serializeTransaction(txFormattedForTrezor);
1635
1656
  const signature = await this.requireTrezorSigner().signEthTransaction({
1636
1657
  coin: trezorCoin,
1637
1658
  tx: txFormattedForTrezor,
@@ -1639,11 +1660,10 @@ class EthereumTransactions {
1639
1660
  slip44: activeNetwork.slip44,
1640
1661
  });
1641
1662
  if (signature.success) {
1642
- const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForTrezor, trezorSignature(signature.payload));
1663
+ const signedTx = serializeTransaction(txFormattedForTrezor, trezorSignature(signature.payload));
1643
1664
  assertSignedSender(signedTx);
1644
1665
  assertHardwareTransactionSender(signedTx, activeAccount.address);
1645
- assertBeforeBroadcast();
1646
- const transactionResponse = await provider.sendTransaction(signedTx);
1666
+ const transactionResponse = await (0, evm_submission_1.submitSignedEvmTransaction)(signedTx, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, expectedUnsignedTransaction);
1647
1667
  return {
1648
1668
  isSpeedUp: true,
1649
1669
  transaction: transactionResponse,
@@ -1667,6 +1687,7 @@ class EthereumTransactions {
1667
1687
  ? 'REPLACEMENT_ALREADY_KNOWN'
1668
1688
  : 'REPLACEMENT_SEND_FAILED';
1669
1689
  return {
1690
+ ...(0, evm_submission_1.evmSubmissionFailure)(error),
1670
1691
  isSpeedUp: false,
1671
1692
  error: true,
1672
1693
  code,
@@ -1683,7 +1704,7 @@ class EthereumTransactions {
1683
1704
  if (address.toLowerCase() !== expectedSender) {
1684
1705
  throw new Error('Replacement signing account changed');
1685
1706
  }
1686
- const transactionResponse = await (0, evm_local_signer_1.sendLocalEvmTransaction)(provider, decryptedPrivateKey, txWithEditedFee, assertBeforeBroadcast, assertCurrent);
1707
+ const transactionResponse = await (0, evm_local_signer_1.sendLocalEvmTransaction)(provider, decryptedPrivateKey, txWithEditedFee, assertBeforeBroadcast, assertCurrent, undefined, submission);
1687
1708
  if (transactionResponse) {
1688
1709
  return {
1689
1710
  isSpeedUp: true,
@@ -1709,6 +1730,7 @@ class EthereumTransactions {
1709
1730
  ? 'INSUFFICIENT_FUNDS_REPLACEMENT'
1710
1731
  : 'REPLACEMENT_SEND_FAILED';
1711
1732
  return {
1733
+ ...(0, evm_submission_1.evmSubmissionFailure)(error),
1712
1734
  isSpeedUp: false,
1713
1735
  error: true,
1714
1736
  code,
@@ -1729,6 +1751,7 @@ class EthereumTransactions {
1729
1751
  this.sendSignedErc20Transaction = async ({ receiver, tokenAddress, tokenAmount, isLegacy = false, maxPriorityFeePerGas, maxFeePerGas, gasPrice, decimals, gasLimit, saveTrezorTx, }) => {
1730
1752
  const { activeAccount, activeAccountType, activeAccountId, activeNetwork, provider, assertCurrent, } = this.captureEvmSigningContext();
1731
1753
  const activeAccountAddress = activeAccount.address;
1754
+ const submission = this.captureSubmission('erc20', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertCurrent);
1732
1755
  ({ gasPrice, isLegacy, maxFeePerGas, maxPriorityFeePerGas } =
1733
1756
  await this.resolveEvmFeeParams({
1734
1757
  gasPrice,
@@ -1769,7 +1792,7 @@ class EthereumTransactions {
1769
1792
  chainId: activeNetwork.chainId,
1770
1793
  from: activeAccountAddress,
1771
1794
  value: '0x0',
1772
- }, assertCurrent, assertCurrent);
1795
+ }, assertCurrent, assertCurrent, undefined, submission);
1773
1796
  }
1774
1797
  catch (error) {
1775
1798
  throw error;
@@ -1815,7 +1838,7 @@ class EthereumTransactions {
1815
1838
  type: 2, // Need explicit type for hardware wallet serialization
1816
1839
  };
1817
1840
  }
1818
- const rawTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers);
1841
+ const rawTx = serializeTransaction(txFormattedForEthers);
1819
1842
  const signature = await this.requireLedgerSigner().evm.signEVMTransaction({
1820
1843
  rawTx: rawTx.replace('0x', ''),
1821
1844
  accountIndex: activeAccountId,
@@ -1829,11 +1852,14 @@ class EthereumTransactions {
1829
1852
  };
1830
1853
  if (signature) {
1831
1854
  try {
1832
- const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers, formattedSignature);
1855
+ const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
1833
1856
  assertCurrent();
1834
1857
  assertHardwareTransactionSender(signedTx, activeAccount.address);
1835
- const finalTx = await provider.sendTransaction(signedTx);
1836
- saveTrezorTx && saveTrezorTx(finalTx);
1858
+ const finalTx = await (0, evm_submission_1.submitSignedEvmTransaction)(signedTx, async (raw) => {
1859
+ const response = await provider.sendTransaction(raw);
1860
+ saveTrezorTx && saveTrezorTx(response);
1861
+ return response;
1862
+ }, submission, undefined, rawTx);
1837
1863
  return finalTx;
1838
1864
  }
1839
1865
  catch (error) {
@@ -1886,6 +1912,11 @@ class EthereumTransactions {
1886
1912
  data: txData,
1887
1913
  };
1888
1914
  }
1915
+ assertTransactionGasLimit(txToBeSignedByTrezor);
1916
+ const expectedUnsignedTransaction = serializeTransaction({
1917
+ ...txToBeSignedByTrezor,
1918
+ type: isLegacy ? 0 : 2,
1919
+ });
1889
1920
  const signature = await this.requireTrezorSigner().signEthTransaction({
1890
1921
  index: `${activeAccountId}`,
1891
1922
  tx: txToBeSignedByTrezor,
@@ -1921,11 +1952,14 @@ class EthereumTransactions {
1921
1952
  type: 2, // Need explicit type for hardware wallet serialization
1922
1953
  };
1923
1954
  }
1924
- const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers, trezorSignature(signature.payload));
1955
+ const signedTx = serializeTransaction(txFormattedForEthers, trezorSignature(signature.payload));
1925
1956
  assertCurrent();
1926
1957
  assertHardwareTransactionSender(signedTx, activeAccount.address);
1927
- const finalTx = await provider.sendTransaction(signedTx);
1928
- saveTrezorTx && saveTrezorTx(finalTx);
1958
+ const finalTx = await (0, evm_submission_1.submitSignedEvmTransaction)(signedTx, async (raw) => {
1959
+ const response = await provider.sendTransaction(raw);
1960
+ saveTrezorTx && saveTrezorTx(response);
1961
+ return response;
1962
+ }, submission, undefined, expectedUnsignedTransaction);
1929
1963
  return finalTx;
1930
1964
  }
1931
1965
  catch (error) {
@@ -1952,6 +1986,7 @@ class EthereumTransactions {
1952
1986
  this.sendSignedErc721Transaction = async ({ receiver, tokenAddress, tokenId, isLegacy, maxPriorityFeePerGas, maxFeePerGas, gasPrice, gasLimit, }) => {
1953
1987
  const { activeAccount, activeAccountType, activeAccountId, activeNetwork, provider, assertCurrent, } = this.captureEvmSigningContext();
1954
1988
  const activeAccountAddress = activeAccount.address;
1989
+ const submission = this.captureSubmission('erc721', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertCurrent);
1955
1990
  if (tokenId === undefined || tokenId === null) {
1956
1991
  throw new Error('ERC721 tokenId is required');
1957
1992
  }
@@ -1993,7 +2028,7 @@ class EthereumTransactions {
1993
2028
  chainId: activeNetwork.chainId,
1994
2029
  from: activeAccountAddress,
1995
2030
  value: '0x0',
1996
- }, assertCurrent, assertCurrent);
2031
+ }, assertCurrent, assertCurrent, undefined, submission);
1997
2032
  }
1998
2033
  catch (error) {
1999
2034
  throw error;
@@ -2038,7 +2073,7 @@ class EthereumTransactions {
2038
2073
  type: 2, // Need explicit type for hardware wallet serialization
2039
2074
  };
2040
2075
  }
2041
- const rawTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers);
2076
+ const rawTx = serializeTransaction(txFormattedForEthers);
2042
2077
  const signature = await this.requireLedgerSigner().evm.signEVMTransaction({
2043
2078
  rawTx: rawTx.replace('0x', ''),
2044
2079
  accountIndex: activeAccountId,
@@ -2052,10 +2087,10 @@ class EthereumTransactions {
2052
2087
  };
2053
2088
  if (signature) {
2054
2089
  try {
2055
- const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers, formattedSignature);
2090
+ const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
2056
2091
  assertCurrent();
2057
2092
  assertHardwareTransactionSender(signedTx, activeAccount.address);
2058
- const finalTx = await provider.sendTransaction(signedTx);
2093
+ const finalTx = await (0, evm_submission_1.submitSignedEvmTransaction)(signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, rawTx);
2059
2094
  return finalTx;
2060
2095
  }
2061
2096
  catch (error) {
@@ -2110,6 +2145,11 @@ class EthereumTransactions {
2110
2145
  }
2111
2146
  // For EVM networks, Trezor expects 'eth' regardless of the network's currency
2112
2147
  const trezorCoin = activeNetwork.slip44 === 60 ? 'eth' : activeNetwork.currency;
2148
+ assertTransactionGasLimit(txToBeSignedByTrezor);
2149
+ const expectedUnsignedTransaction = serializeTransaction({
2150
+ ...txToBeSignedByTrezor,
2151
+ type: isLegacy ? 0 : 2,
2152
+ });
2113
2153
  const signature = await this.requireTrezorSigner().signEthTransaction({
2114
2154
  index: `${activeAccountId}`,
2115
2155
  tx: txToBeSignedByTrezor,
@@ -2145,10 +2185,10 @@ class EthereumTransactions {
2145
2185
  type: 2, // Need explicit type for hardware wallet serialization
2146
2186
  };
2147
2187
  }
2148
- const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers, trezorSignature(signature.payload));
2188
+ const signedTx = serializeTransaction(txFormattedForEthers, trezorSignature(signature.payload));
2149
2189
  assertCurrent();
2150
2190
  assertHardwareTransactionSender(signedTx, activeAccount.address);
2151
- const finalTx = await provider.sendTransaction(signedTx);
2191
+ const finalTx = await (0, evm_submission_1.submitSignedEvmTransaction)(signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, expectedUnsignedTransaction);
2152
2192
  return finalTx;
2153
2193
  }
2154
2194
  catch (error) {
@@ -2177,6 +2217,7 @@ class EthereumTransactions {
2177
2217
  this.sendSignedErc1155Transaction = async ({ receiver, tokenAddress, tokenId, tokenAmount, isLegacy, maxPriorityFeePerGas, maxFeePerGas, gasPrice, gasLimit, }) => {
2178
2218
  const { activeAccount, activeAccountType, activeAccountId, activeNetwork, provider, assertCurrent, } = this.captureEvmSigningContext();
2179
2219
  const activeAccountAddress = activeAccount.address;
2220
+ const submission = this.captureSubmission('erc1155', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertCurrent);
2180
2221
  ({ gasPrice, isLegacy, maxFeePerGas, maxPriorityFeePerGas } =
2181
2222
  await this.resolveEvmFeeParams({
2182
2223
  gasPrice,
@@ -2216,7 +2257,7 @@ class EthereumTransactions {
2216
2257
  chainId: activeNetwork.chainId,
2217
2258
  from: activeAccountAddress,
2218
2259
  value: '0x0',
2219
- }, assertCurrent, assertCurrent);
2260
+ }, assertCurrent, assertCurrent, undefined, submission);
2220
2261
  }
2221
2262
  catch (error) {
2222
2263
  throw error;
@@ -2258,7 +2299,7 @@ class EthereumTransactions {
2258
2299
  type: 2, // Need explicit type for hardware wallet serialization
2259
2300
  };
2260
2301
  }
2261
- const rawTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers);
2302
+ const rawTx = serializeTransaction(txFormattedForEthers);
2262
2303
  const signature = await this.requireLedgerSigner().evm.signEVMTransaction({
2263
2304
  rawTx: rawTx.replace('0x', ''),
2264
2305
  accountIndex: activeAccountId,
@@ -2272,10 +2313,10 @@ class EthereumTransactions {
2272
2313
  };
2273
2314
  if (signature) {
2274
2315
  try {
2275
- const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers, formattedSignature);
2316
+ const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
2276
2317
  assertCurrent();
2277
2318
  assertHardwareTransactionSender(signedTx, activeAccount.address);
2278
- const finalTx = await provider.sendTransaction(signedTx);
2319
+ const finalTx = await (0, evm_submission_1.submitSignedEvmTransaction)(signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, rawTx);
2279
2320
  return finalTx;
2280
2321
  }
2281
2322
  catch (error) {
@@ -2324,6 +2365,11 @@ class EthereumTransactions {
2324
2365
  data: txData,
2325
2366
  };
2326
2367
  }
2368
+ assertTransactionGasLimit(txToBeSignedByTrezor);
2369
+ const expectedUnsignedTransaction = serializeTransaction({
2370
+ ...txToBeSignedByTrezor,
2371
+ type: isLegacy ? 0 : 2,
2372
+ });
2327
2373
  const signature = await this.requireTrezorSigner().signEthTransaction({
2328
2374
  index: `${activeAccountId}`,
2329
2375
  tx: txToBeSignedByTrezor,
@@ -2359,10 +2405,10 @@ class EthereumTransactions {
2359
2405
  type: 2, // Need explicit type for hardware wallet serialization
2360
2406
  };
2361
2407
  }
2362
- const signedTx = (0, ethers_v6_1.serializeTransaction)(txFormattedForEthers, trezorSignature(signature.payload));
2408
+ const signedTx = serializeTransaction(txFormattedForEthers, trezorSignature(signature.payload));
2363
2409
  assertCurrent();
2364
2410
  assertHardwareTransactionSender(signedTx, activeAccount.address);
2365
- const finalTx = await provider.sendTransaction(signedTx);
2411
+ const finalTx = await (0, evm_submission_1.submitSignedEvmTransaction)(signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, expectedUnsignedTransaction);
2366
2412
  return finalTx;
2367
2413
  }
2368
2414
  catch (error) {
@@ -2424,56 +2470,82 @@ class EthereumTransactions {
2424
2470
  }
2425
2471
  };
2426
2472
  this.getTxGasLimit = async (tx) => {
2427
- const hasCalldata = Boolean(tx.data && tx.data !== '0x' && tx.data !== '0x0');
2473
+ const request = { ...tx };
2474
+ const network = this.getNetwork();
2475
+ const identity = {
2476
+ kind: network.kind,
2477
+ chainId: network.chainId,
2478
+ slip44: network.slip44,
2479
+ url: network.url,
2480
+ };
2481
+ const chainId = (0, evm_fees_1.parseEvmFeeQuantity)(identity.chainId, 'chain ID');
2482
+ const provider = this.web3Provider;
2483
+ const controller = this.abortController;
2484
+ const minimum = this.gasOverrides.minGasLimit;
2485
+ const assertCurrent = () => {
2486
+ const current = this.getNetwork();
2487
+ if (controller.signal.aborted ||
2488
+ controller !== this.abortController ||
2489
+ current.kind !== identity.kind ||
2490
+ current.chainId !== identity.chainId ||
2491
+ current.slip44 !== identity.slip44 ||
2492
+ current.url !== identity.url ||
2493
+ this.web3Provider !== provider) {
2494
+ const error = new Error('EVM gas estimate context changed');
2495
+ error.name = 'AbortError';
2496
+ throw error;
2497
+ }
2498
+ };
2499
+ const checked = (limit) => {
2500
+ assertCurrent();
2501
+ return ethers_v6_1.BigNumber.from((0, evm_fees_1.assertEvmGasLimit)(typeof limit === 'bigint' ? limit : limit.toBigInt(), chainId));
2502
+ };
2503
+ const hasCalldata = Boolean(request.data && request.data !== '0x' && request.data !== '0x0');
2428
2504
  try {
2429
2505
  // First attempt: standard estimation
2430
- const estimated = await this.web3Provider.estimateGas(tx);
2431
- // Add 20% buffer to the estimated gas
2432
- const withBuffer = estimated.mul(120).div(100);
2433
- // Apply an explicit per-network floor only when configured.
2434
- if (this.gasOverrides.minGasLimit &&
2435
- withBuffer.lt(this.gasOverrides.minGasLimit)) {
2436
- return this.gasOverrides.minGasLimit;
2437
- }
2438
- return withBuffer;
2506
+ assertCurrent();
2507
+ const estimated = await provider.estimateGas(request);
2508
+ return checked((0, evm_fees_1.bufferedEvmGasLimit)(estimated.toBigInt(), minimum?.toBigInt()));
2439
2509
  }
2440
2510
  catch (error) {
2441
- if (error?.code === 'SYSWEB3_RPC_RESPONSE_INVALID' ||
2511
+ assertCurrent();
2512
+ if (error instanceof evm_fees_1.EvmGasLimitError ||
2513
+ error?.code === 'SYSWEB3_RPC_RESPONSE_INVALID' ||
2442
2514
  error?.name === 'AbortError' ||
2443
2515
  this.abortController.signal.aborted)
2444
2516
  throw error;
2445
2517
  console.warn('Gas estimation failed:', error);
2446
2518
  if (hasCalldata) {
2447
2519
  const calldataFallback = ethers_v6_1.BigNumber.from('100000');
2448
- if (this.gasOverrides.minGasLimit &&
2449
- calldataFallback.lt(this.gasOverrides.minGasLimit)) {
2450
- return this.gasOverrides.minGasLimit;
2520
+ if (minimum && calldataFallback.lt(minimum)) {
2521
+ return checked(minimum);
2451
2522
  }
2452
- return calldataFallback;
2523
+ return checked(calldataFallback);
2453
2524
  }
2454
2525
  // Try a simpler estimation for basic transfers
2455
2526
  try {
2456
- const simpleEstimate = await this.web3Provider.estimateGas({
2457
- to: tx.to,
2458
- from: tx.from,
2459
- value: tx.value || '0x0',
2527
+ const simpleEstimate = await provider.estimateGas({
2528
+ to: request.to,
2529
+ from: request.from,
2530
+ value: request.value || '0x0',
2460
2531
  });
2461
2532
  const withBuffer = simpleEstimate.mul(150).div(100); // 50% buffer for failed estimations
2462
- if (this.gasOverrides.minGasLimit &&
2463
- withBuffer.lt(this.gasOverrides.minGasLimit)) {
2464
- return this.gasOverrides.minGasLimit;
2533
+ if (minimum && withBuffer.lt(minimum)) {
2534
+ return checked(minimum);
2465
2535
  }
2466
- return withBuffer;
2536
+ return checked(withBuffer);
2467
2537
  }
2468
2538
  catch (secondError) {
2469
- if (secondError?.code === 'SYSWEB3_RPC_RESPONSE_INVALID' ||
2539
+ assertCurrent();
2540
+ if (secondError instanceof evm_fees_1.EvmGasLimitError ||
2541
+ secondError?.code === 'SYSWEB3_RPC_RESPONSE_INVALID' ||
2470
2542
  secondError?.name === 'AbortError' ||
2471
2543
  this.abortController.signal.aborted) {
2472
2544
  throw secondError;
2473
2545
  }
2474
2546
  // Ultimate fallback
2475
2547
  console.warn('Simple estimation also failed, using default', secondError);
2476
- return this.gasOverrides.minGasLimit || ethers_v6_1.BigNumber.from('100000');
2548
+ return checked(minimum || ethers_v6_1.BigNumber.from('100000'));
2477
2549
  }
2478
2550
  }
2479
2551
  };
@@ -2548,6 +2620,7 @@ class EthereumTransactions {
2548
2620
  this.trezorSigner = trezorSigner;
2549
2621
  this.ledgerSigner = ledgerSigner;
2550
2622
  this.captureSigningContext = captureSigningContext;
2623
+ this.captureSubmissionLifecycle = captureSubmissionLifecycle;
2551
2624
  }
2552
2625
  // Getter that automatically ensures providers are initialized when accessed
2553
2626
  get web3Provider() {