@sidhujag/sysweb3-keyring 2.1.3 → 2.1.5

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 (129) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +47 -47
  3. package/cjs/hardware-transaction-authority.js +36 -0
  4. package/cjs/hardware-transaction-authority.js.map +1 -0
  5. package/cjs/index.js +5 -5
  6. package/cjs/index.js.map +1 -1
  7. package/cjs/keyring-core.js +171 -220
  8. package/cjs/keyring-core.js.map +1 -1
  9. package/cjs/keyring-host.js +113 -0
  10. package/cjs/keyring-host.js.map +1 -0
  11. package/cjs/keyring-manager.js +15 -151
  12. package/cjs/keyring-manager.js.map +1 -1
  13. package/cjs/ledger/host-types.js.map +1 -1
  14. package/cjs/ledger/index.js +44 -19
  15. package/cjs/ledger/index.js.map +1 -1
  16. package/cjs/providers.js +3 -1
  17. package/cjs/providers.js.map +1 -1
  18. package/cjs/software.js +10 -56
  19. package/cjs/software.js.map +1 -1
  20. package/cjs/transactions/ethereum.js +306 -229
  21. package/cjs/transactions/ethereum.js.map +1 -1
  22. package/cjs/transactions/evm-local-signer.js +15 -0
  23. package/cjs/transactions/evm-local-signer.js.map +1 -1
  24. package/cjs/transactions/evm-submission.js +61 -65
  25. package/cjs/transactions/evm-submission.js.map +1 -1
  26. package/cjs/transactions/hardware-psbt.js +11 -8
  27. package/cjs/transactions/hardware-psbt.js.map +1 -1
  28. package/cjs/transactions/index.js +10 -2
  29. package/cjs/transactions/index.js.map +1 -1
  30. package/cjs/transactions/prepared-evm.js +10 -7
  31. package/cjs/transactions/prepared-evm.js.map +1 -1
  32. package/cjs/transactions/syscoin-core.js +308 -205
  33. package/cjs/transactions/syscoin-core.js.map +1 -1
  34. package/cjs/trezor/connect.js +19 -2
  35. package/cjs/trezor/connect.js.map +1 -1
  36. package/cjs/trezor/index.js +38 -16
  37. package/cjs/trezor/index.js.map +1 -1
  38. package/cjs/types.js.map +1 -1
  39. package/cjs/utils/excluded-utxos.js +6 -2
  40. package/cjs/utils/excluded-utxos.js.map +1 -1
  41. package/cjs/utils/owned-blockbook-account.js +45 -0
  42. package/cjs/utils/owned-blockbook-account.js.map +1 -0
  43. package/esm/current-vault-refusal.js +3 -0
  44. package/esm/current-vault-refusal.js.map +1 -0
  45. package/esm/evm-fees.js +242 -0
  46. package/esm/evm-fees.js.map +1 -0
  47. package/esm/hardware-transaction-authority.js +33 -0
  48. package/esm/hardware-transaction-authority.js.map +1 -0
  49. package/esm/initial-state.js +102 -0
  50. package/esm/initial-state.js.map +1 -0
  51. package/esm/keyring-core.js +2781 -0
  52. package/esm/keyring-core.js.map +1 -0
  53. package/esm/keyring-host.js +108 -0
  54. package/esm/keyring-host.js.map +1 -0
  55. package/esm/ledger/dmk-session.js +1 -0
  56. package/esm/ledger/dmk-session.js.map +1 -1
  57. package/esm/ledger/dmk-utxo.js +9 -3
  58. package/esm/ledger/dmk-utxo.js.map +1 -1
  59. package/esm/ledger/host-types.js.map +1 -1
  60. package/esm/ledger/index.js +44 -19
  61. package/esm/ledger/index.js.map +1 -1
  62. package/esm/network-utils.js +70 -0
  63. package/esm/network-utils.js.map +1 -0
  64. package/esm/providers.js +3 -1
  65. package/esm/providers.js.map +1 -1
  66. package/esm/software-hd-account.js +20 -0
  67. package/esm/software-hd-account.js.map +1 -0
  68. package/esm/storage-core.js +258 -0
  69. package/esm/storage-core.js.map +1 -0
  70. package/esm/transactions/ethereum.js +2753 -0
  71. package/esm/transactions/ethereum.js.map +1 -0
  72. package/esm/transactions/evm-cancellation.js +40 -0
  73. package/esm/transactions/evm-cancellation.js.map +1 -0
  74. package/esm/transactions/evm-local-signer.js +202 -0
  75. package/esm/transactions/evm-local-signer.js.map +1 -0
  76. package/esm/transactions/evm-submission.js +187 -0
  77. package/esm/transactions/evm-submission.js.map +1 -0
  78. package/esm/transactions/prepared-evm.js +194 -0
  79. package/esm/transactions/prepared-evm.js.map +1 -0
  80. package/esm/transactions/syscoin-core.js +714 -0
  81. package/esm/transactions/syscoin-core.js.map +1 -0
  82. package/esm/trezor/connect.js +19 -2
  83. package/esm/trezor/connect.js.map +1 -1
  84. package/esm/trezor/index.js +38 -16
  85. package/esm/trezor/index.js.map +1 -1
  86. package/esm/types.js.map +1 -1
  87. package/esm/utils/excluded-utxos.js +57 -0
  88. package/esm/utils/excluded-utxos.js.map +1 -0
  89. package/esm/utils/owned-blockbook-account.js +9 -0
  90. package/esm/utils/owned-blockbook-account.js.map +1 -0
  91. package/esm/utils/psbt.js +32 -0
  92. package/esm/utils/psbt.js.map +1 -0
  93. package/esm/utils/watch-descriptor.js +138 -0
  94. package/esm/utils/watch-descriptor.js.map +1 -0
  95. package/package.json +5 -11
  96. package/types/hardware-transaction-authority.d.ts +5 -0
  97. package/types/index.d.ts +3 -3
  98. package/types/keyring-core.d.ts +22 -51
  99. package/types/keyring-host.d.ts +46 -0
  100. package/types/keyring-manager.d.ts +13 -18
  101. package/types/ledger/host-types.d.ts +4 -1
  102. package/types/ledger/index.d.ts +1 -1
  103. package/types/ledger/types.d.ts +3 -2
  104. package/types/providers.d.ts +1 -1
  105. package/types/software.d.ts +6 -19
  106. package/types/transactions/ethereum.d.ts +14 -11
  107. package/types/transactions/evm-local-signer.d.ts +2 -2
  108. package/types/transactions/evm-submission.d.ts +11 -4
  109. package/types/transactions/hardware-psbt.d.ts +1 -1
  110. package/types/transactions/index.d.ts +3 -2
  111. package/types/transactions/prepared-evm.d.ts +4 -1
  112. package/types/transactions/syscoin-core.d.ts +48 -21
  113. package/types/trezor/connect.d.ts +6 -3
  114. package/types/trezor/index.d.ts +4 -3
  115. package/types/types.d.ts +95 -63
  116. package/types/utils/excluded-utxos.d.ts +1 -1
  117. package/types/utils/owned-blockbook-account.d.ts +3 -0
  118. package/cjs/hardware-wallet-manager-singleton.js +0 -34
  119. package/cjs/hardware-wallet-manager-singleton.js.map +0 -1
  120. package/cjs/storage.js +0 -20
  121. package/cjs/storage.js.map +0 -1
  122. package/cjs/transactions/syscoin.js +0 -16
  123. package/cjs/transactions/syscoin.js.map +0 -1
  124. package/cjs/utils/blockbook-cache.js +0 -76
  125. package/cjs/utils/blockbook-cache.js.map +0 -1
  126. package/types/hardware-wallet-manager-singleton.d.ts +0 -19
  127. package/types/storage.d.ts +0 -5
  128. package/types/transactions/syscoin.d.ts +0 -9
  129. package/types/utils/blockbook-cache.d.ts +0 -6
@@ -0,0 +1,2753 @@
1
+ import { decrypt, signTypedData as signTypedDataUtil, getEncryptionPublicKey, recoverPersonalSignature, recoverTypedSignature, } from '@metamask/eth-sig-util';
2
+ import { INetworkType } from '@sidhujag/sysweb3-network';
3
+ import { createContractUsingAbi, getErc20Abi, getErc21Abi, getErc55Abi, } from '@sidhujag/sysweb3-utils';
4
+ import { accessListify, Transaction } from 'ethers/transaction';
5
+ import { hexlify } from 'ethers/utils';
6
+ import omit from 'lodash/omit';
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';
9
+ import { CustomJsonRpcProvider, wrapTransactionResponse } from '../providers';
10
+ import { KeyringAccountType, } from '../types';
11
+ import { assertEvmCancellationEnvelope } from './evm-cancellation';
12
+ import { deriveEvmAccountFromMnemonic, parsePersonalMessage as parseLocalPersonalMessage, privateKeyToAccount, normalizeGasLimitedTransaction, sendLocalEvmTransaction, signDigestHex, signPersonalMessage as signLocalPersonalMessage, } from './evm-local-signer';
13
+ import { captureEvmSubmission, submitSignedEvmTransaction, submitSignedHardwareEvmTransaction, evmSubmissionFailure, copyEvmSubmissionFailure, } from './evm-submission';
14
+ import { signPreparedEvmEnvelope, snapshotPreparedEvmRequest, } from './prepared-evm';
15
+ import { getAddressDerivationPath } from '../utils/derivation-paths';
16
+ const hardwareSignerUnavailable = (hardware) => Object.assign(new Error(`${hardware} signer is unavailable in this host`), {
17
+ code: 'HARDWARE_SIGNER_UNAVAILABLE',
18
+ });
19
+ const assertHardwareTransactionSender = (signedTx, expectedAddress) => {
20
+ if (Transaction.from(signedTx).from?.toLowerCase() !==
21
+ expectedAddress.toLowerCase()) {
22
+ throw Object.assign(new Error('Hardware signature does not match the approved account'), {
23
+ code: 'HARDWARE_ACCOUNT_MISMATCH',
24
+ });
25
+ }
26
+ };
27
+ const stripHexPrefix = (value) => {
28
+ return value.startsWith('0x') || value.startsWith('0X')
29
+ ? value.slice(2)
30
+ : value;
31
+ };
32
+ // Connect returns hexadecimal signature fields; normalize their encoding
33
+ // without mutating the response or reparsing an already numeric recovery v.
34
+ const trezorSignature = (signature) => {
35
+ const { r, s, v } = signature;
36
+ if (![r, s].every((value) => /^(?:0x)?[0-9a-f]{64}$/i.test(value))) {
37
+ throw new Error('Invalid Trezor signature scalar encoding');
38
+ }
39
+ if (typeof v === 'string' && !/^(?:0x)?[0-9a-f]+$/i.test(v)) {
40
+ throw new Error('Invalid Trezor signature recovery value');
41
+ }
42
+ const recovery = typeof v === 'number' ? v : Number.parseInt(v, 16);
43
+ if (!Number.isSafeInteger(recovery) || recovery < 0) {
44
+ throw new Error('Invalid Trezor signature recovery value');
45
+ }
46
+ return {
47
+ r: `0x${stripHexPrefix(r)}`,
48
+ s: `0x${stripHexPrefix(s)}`,
49
+ v: recovery,
50
+ };
51
+ };
52
+ const normalizeEthersOverrideValue = (value) => normalizeTxValue(value);
53
+ // The same protocol bound applies to software and external-device envelopes.
54
+ // Keep this at serialization as well as before handing a request to a device.
55
+ const assertTransactionGasLimit = (transaction) => assertEvmGasLimit(parseEvmFeeQuantity(transaction.gasLimit, 'gas limit'), parseEvmFeeQuantity(transaction.chainId ?? 0, 'chain ID'));
56
+ const serializeTransaction = (...args) => {
57
+ const snapshot = normalizeGasLimitedTransaction(args[0]);
58
+ return serializeUncheckedTransaction(snapshot, args[1]);
59
+ };
60
+ const nonNegativeFeeQuantity = (value, label) => BigNumber.from(parseEvmFeeQuantity(value, label));
61
+ const normalizeEthersOverrides = (overrides) => {
62
+ const normalized = { ...overrides };
63
+ for (const field of [
64
+ 'gasLimit',
65
+ 'gasPrice',
66
+ 'maxFeePerGas',
67
+ 'maxPriorityFeePerGas',
68
+ ]) {
69
+ if (normalized[field] != null) {
70
+ normalized[field] = normalizeEthersOverrideValue(normalized[field]);
71
+ }
72
+ }
73
+ return normalized;
74
+ };
75
+ /** Bind all replacement input fields to the requested signed transaction. */
76
+ const authenticateReplacementOriginal = (transaction, requestedHash, expectedSender, chainId) => {
77
+ const fields = normalizeTransactionRequest(transaction);
78
+ if (fields.hash != null)
79
+ fields.hash = fields.hash.toLowerCase();
80
+ const type = fields.type ??
81
+ (fields.maxFeePerGas != null ? 2 : fields.accessList != null ? 1 : 0);
82
+ if (![0, 1, 2].includes(type)) {
83
+ throw new Error('Unsupported replacement transaction type');
84
+ }
85
+ fields.signature = fields.signature ?? {
86
+ r: fields.r,
87
+ s: fields.s,
88
+ v: fields.v ?? fields.yParity,
89
+ };
90
+ fields.type = type;
91
+ fields.data = fields.data ?? fields.input ?? '0x';
92
+ if (type === 2)
93
+ delete fields.gasPrice; // Not signed in type 2.
94
+ const original = Transaction.from(fields);
95
+ if (!original.signature ||
96
+ original.hash !== requestedHash ||
97
+ original.from?.toLowerCase() !== expectedSender ||
98
+ original.chainId !== BigInt(chainId)) {
99
+ throw new Error('Original transaction is not authenticated');
100
+ }
101
+ // Do not retain mutable RPC fields across subsequent asynchronous work.
102
+ return wrapTransactionResponse({
103
+ ...original.toJSON(),
104
+ hash: original.hash,
105
+ from: original.from,
106
+ gasPrice: original.gasPrice ?? 0,
107
+ maxFeePerGas: original.maxFeePerGas ?? 0,
108
+ maxPriorityFeePerGas: original.maxPriorityFeePerGas ?? 0,
109
+ signature: original.signature,
110
+ blockNumber: transaction.blockNumber,
111
+ });
112
+ };
113
+ const assertSynchronousOriginalValidation = (result, message = 'Original transaction validator must be synchronous') => {
114
+ if (result === undefined)
115
+ return;
116
+ if (result !== null &&
117
+ (typeof result === 'object' || typeof result === 'function') &&
118
+ typeof result.then === 'function') {
119
+ // A void callback type also accepts async functions. Decline their pending
120
+ // decision and consume a later rejection without allowing signing to start.
121
+ void Promise.resolve(result).catch(() => undefined);
122
+ }
123
+ throw new Error(message);
124
+ };
125
+ export class EthereumTransactions {
126
+ constructor(getNetwork, getDecryptedPrivateKey, getState, ledgerSigner, trezorSigner, captureSigningContext, captureSubmissionLifecycle) {
127
+ // Allow manual override of gas parameters when automatic estimation fails
128
+ this.gasOverrides = {};
129
+ this.requireLedgerSigner = () => {
130
+ if (!this.ledgerSigner)
131
+ throw hardwareSignerUnavailable('Ledger');
132
+ return this.ledgerSigner;
133
+ };
134
+ this.requireTrezorSigner = () => {
135
+ if (!this.trezorSigner)
136
+ throw hardwareSignerUnavailable('Trezor');
137
+ return this.trezorSigner;
138
+ };
139
+ this.assertHardwareSignerAvailable = (targetAccount) => {
140
+ const state = this.getState();
141
+ const type = targetAccount?.type ?? state.activeAccountType;
142
+ const id = targetAccount?.id ?? state.activeAccountId;
143
+ const account = state.accounts[type]?.[id];
144
+ if (type === KeyringAccountType.Ledger || account?.isLedgerWallet)
145
+ this.requireLedgerSigner();
146
+ if (type === KeyringAccountType.Trezor || account?.isTrezorWallet)
147
+ this.requireTrezorSigner();
148
+ };
149
+ this.captureSessionGuard = () => {
150
+ const guard = this.captureSigningContext();
151
+ if (typeof guard !== 'function') {
152
+ void Promise.resolve(guard).catch(() => undefined);
153
+ throw new TypeError('Signing context must return a synchronous guard');
154
+ }
155
+ return () => assertSynchronousOriginalValidation(guard(), 'Signing context guard must be synchronous');
156
+ };
157
+ this.captureEvmSigningContext = (target) => {
158
+ this.assertHardwareSignerAvailable(target);
159
+ const state = this.getState();
160
+ const selectedType = state.activeAccountType;
161
+ const selectedId = state.activeAccountId;
162
+ const selectedAddress = state.accounts[selectedType]?.[selectedId]?.address;
163
+ const activeAccountType = target?.type ?? selectedType;
164
+ const activeAccountId = target?.id ?? selectedId;
165
+ const selectedAccount = state.accounts[activeAccountType]?.[activeAccountId];
166
+ if (!selectedAccount || !selectedAddress)
167
+ throw new Error('Active account not found');
168
+ const account = { address: selectedAccount.address };
169
+ const network = { ...state.activeNetwork };
170
+ const assertSession = this.captureSessionGuard();
171
+ const provider = this.web3Provider;
172
+ const assertCurrent = () => {
173
+ assertSession();
174
+ const current = this.getState();
175
+ const currentAccount = current.accounts[activeAccountType]?.[activeAccountId];
176
+ if (current.activeAccountId !== selectedId ||
177
+ current.activeAccountType !== selectedType ||
178
+ current.accounts[selectedType]?.[selectedId]?.address?.toLowerCase() !==
179
+ selectedAddress.toLowerCase() ||
180
+ currentAccount?.address?.toLowerCase() !==
181
+ account.address.toLowerCase() ||
182
+ [current.activeNetwork, this.getNetwork()].some((selected) => selected.kind !== network.kind ||
183
+ selected.chainId !== network.chainId ||
184
+ selected.slip44 !== network.slip44 ||
185
+ selected.url !== network.url) ||
186
+ this.web3Provider !== provider)
187
+ throw new Error('Wallet signing context changed');
188
+ };
189
+ assertCurrent();
190
+ return {
191
+ activeAccount: account,
192
+ activeAccountType,
193
+ activeAccountId,
194
+ activeNetwork: network,
195
+ provider,
196
+ assertCurrent,
197
+ };
198
+ };
199
+ this.captureSubmission = (operation, account, type, id, network, assertCurrent, originalHash) => {
200
+ if (!this.captureSubmissionLifecycle)
201
+ throw new TypeError('A durable EVM submission lifecycle is required');
202
+ return captureEvmSubmission({
203
+ operation,
204
+ account: { address: account.address, type, id },
205
+ network: {
206
+ kind: network.kind,
207
+ chainId: network.chainId,
208
+ slip44: network.slip44,
209
+ url: network.url,
210
+ },
211
+ ...(originalHash !== undefined ? { originalHash } : {}),
212
+ }, assertCurrent, this.captureSubmissionLifecycle);
213
+ };
214
+ this.authorizeHardwareTransaction = async (provider, assertCurrent, signal = this.abortController.signal) => {
215
+ assertCurrent();
216
+ await provider.verifyConfiguredChainId();
217
+ assertCurrent();
218
+ return { assertCurrent, signal };
219
+ };
220
+ this.submitSignedHardwareTransaction = (provider, raw, broadcast, submission, beforeBroadcast, expectedUnsigned) => submitSignedHardwareEvmTransaction(raw, broadcast, submission, beforeBroadcast, expectedUnsigned,
221
+ // A device confirmation or journal write can outlast the earlier check.
222
+ () => provider.verifyConfiguredChainId(true));
223
+ this.signTypedData = async (addr, typedData, version) => {
224
+ const { address, decryptedPrivateKey } = this.getDecryptedPrivateKey();
225
+ const { activeAccountType, accounts, activeAccountId } = this.getState();
226
+ const activeAccount = accounts[activeAccountType][activeAccountId];
227
+ // Validate that the derived address matches the active account to prevent race conditions
228
+ if (address.toLowerCase() !== activeAccount.address.toLowerCase()) {
229
+ throw {
230
+ message: `Account state mismatch detected. Expected ${activeAccount.address} but got ${address}. Please try again after account switching completes.`,
231
+ };
232
+ }
233
+ const signTypedDataLocal = () => {
234
+ if (addr.toLowerCase() !== address.toLowerCase())
235
+ throw {
236
+ message: 'Decrypting for wrong address, change activeAccount maybe',
237
+ };
238
+ const privKey = Buffer.from(stripHexPrefix(decryptedPrivateKey), 'hex');
239
+ return signTypedDataUtil({
240
+ privateKey: privKey,
241
+ data: typedData,
242
+ version,
243
+ });
244
+ };
245
+ const signTypedDataWithLedger = async () => {
246
+ if (addr.toLowerCase() !== activeAccount.address.toLowerCase())
247
+ throw {
248
+ message: 'Decrypting for wrong address, change activeAccount maybe',
249
+ };
250
+ return await this.requireLedgerSigner().evm.signTypedData({
251
+ version,
252
+ accountIndex: activeAccountId,
253
+ expectedAddress: activeAccount.address,
254
+ data: typedData,
255
+ });
256
+ };
257
+ const signTypedDataWithTrezor = async () => {
258
+ if (addr.toLowerCase() !== activeAccount.address.toLowerCase())
259
+ throw {
260
+ message: 'Decrypting for wrong address, change activeAccount maybe',
261
+ };
262
+ return await this.requireTrezorSigner().signTypedData({
263
+ version,
264
+ address: addr,
265
+ data: typedData,
266
+ index: activeAccountId,
267
+ });
268
+ };
269
+ switch (activeAccountType) {
270
+ case KeyringAccountType.Trezor:
271
+ return await signTypedDataWithTrezor();
272
+ case KeyringAccountType.Ledger:
273
+ return await signTypedDataWithLedger();
274
+ default:
275
+ return signTypedDataLocal();
276
+ }
277
+ };
278
+ // Verify a UTXO address on the connected hardware wallet (Ledger or Trezor)
279
+ // Routes to the appropriate device based on activeAccountType
280
+ this.verifyUtxoAddress = async (accountIndex, currency, slip44) => {
281
+ const { activeAccountType } = this.getState();
282
+ switch (activeAccountType) {
283
+ case KeyringAccountType.Ledger:
284
+ return await this.requireLedgerSigner().verifyUtxoAddress(accountIndex, currency, slip44);
285
+ case KeyringAccountType.Trezor:
286
+ return await this.requireTrezorSigner().verifyUtxoAddress(accountIndex, currency, slip44);
287
+ default:
288
+ throw new Error('verifyUtxoAddress is only available for hardware wallet accounts');
289
+ }
290
+ };
291
+ this.verifyTypedSignature = (data, signature, version) => {
292
+ try {
293
+ return recoverTypedSignature({ data: data, signature, version });
294
+ }
295
+ catch (error) {
296
+ throw error;
297
+ }
298
+ };
299
+ this.ethSign = async (params) => {
300
+ const { address, decryptedPrivateKey } = this.getDecryptedPrivateKey();
301
+ const { accounts, activeAccountId, activeAccountType, activeNetwork } = this.getState();
302
+ const activeAccount = accounts[activeAccountType][activeAccountId];
303
+ // Validate that the derived address matches the active account to prevent race conditions
304
+ if (address.toLowerCase() !== activeAccount.address.toLowerCase()) {
305
+ throw {
306
+ message: `Account state mismatch detected. Expected ${activeAccount.address} but got ${address}. Please try again after account switching completes.`,
307
+ };
308
+ }
309
+ let msg = '';
310
+ //Comparisions do not need to care for checksum address
311
+ if (params[0].toLowerCase() === address.toLowerCase()) {
312
+ msg = stripHexPrefix(params[1]);
313
+ }
314
+ else if (params[1].toLowerCase() === address.toLowerCase()) {
315
+ msg = stripHexPrefix(params[0]);
316
+ }
317
+ else {
318
+ throw new Error('Signing for wrong address');
319
+ }
320
+ const sign = () => {
321
+ try {
322
+ // Validate and prepare the message for eth_sign
323
+ let msgHash;
324
+ // Check if message is a valid 32-byte hex string
325
+ if (msg.length === 64 && /^[0-9a-fA-F]+$/.test(msg)) {
326
+ // Message is already a 32-byte hex string
327
+ msgHash = Buffer.from(msg, 'hex');
328
+ }
329
+ else {
330
+ // Message is not a proper hash - provide helpful error
331
+ throw new Error(`Expected message to be an Uint8Array with length 32. ` +
332
+ `Got message of length ${msg.length}: "${msg.substring(0, 50)}${msg.length > 50 ? '...' : ''}". ` +
333
+ `For signing arbitrary text, use personal_sign instead of eth_sign.`);
334
+ }
335
+ return signDigestHex(msgHash, decryptedPrivateKey);
336
+ }
337
+ catch (error) {
338
+ throw error;
339
+ }
340
+ };
341
+ const signWithLedger = async () => {
342
+ try {
343
+ const response = await this.requireLedgerSigner().evm.signPersonalMessage({
344
+ accountIndex: activeAccountId,
345
+ expectedAddress: activeAccount.address,
346
+ message: msg,
347
+ });
348
+ return response;
349
+ }
350
+ catch (error) {
351
+ throw error;
352
+ }
353
+ };
354
+ const signWithTrezor = async () => {
355
+ try {
356
+ // For EVM networks, Trezor expects 'eth' regardless of the network's currency
357
+ const trezorCoin = activeNetwork.slip44 === 60 ? 'eth' : activeNetwork.currency;
358
+ const response = await this.requireTrezorSigner().signMessage({
359
+ coin: trezorCoin,
360
+ address: activeAccount.address,
361
+ index: activeAccountId,
362
+ message: msg,
363
+ slip44: activeNetwork.slip44,
364
+ });
365
+ return response.signature;
366
+ }
367
+ catch (error) {
368
+ throw error;
369
+ }
370
+ };
371
+ switch (activeAccountType) {
372
+ case KeyringAccountType.Trezor:
373
+ return await signWithTrezor();
374
+ case KeyringAccountType.Ledger:
375
+ return await signWithLedger();
376
+ default:
377
+ return sign();
378
+ }
379
+ };
380
+ this.signPersonalMessage = async (params) => {
381
+ const { address, decryptedPrivateKey } = this.getDecryptedPrivateKey();
382
+ const { accounts, activeAccountId, activeAccountType, activeNetwork } = this.getState();
383
+ const activeAccount = accounts[activeAccountType][activeAccountId];
384
+ // Validate that the derived address matches the active account to prevent race conditions
385
+ if (address.toLowerCase() !== activeAccount.address.toLowerCase()) {
386
+ throw {
387
+ message: `Account state mismatch detected. Expected ${activeAccount.address} but got ${address}. Please try again after account switching completes.`,
388
+ };
389
+ }
390
+ let msg = '';
391
+ if (params[0].toLowerCase() === address.toLowerCase()) {
392
+ msg = params[1];
393
+ }
394
+ else if (params[1].toLowerCase() === address.toLowerCase()) {
395
+ msg = params[0];
396
+ }
397
+ else {
398
+ throw new Error('Signing for wrong address');
399
+ }
400
+ const signPersonalMessageWithDefaultWallet = () => {
401
+ try {
402
+ // Handle both hex-encoded and plain text messages for personal_sign
403
+ let message;
404
+ if (msg.startsWith('0x')) {
405
+ const rawHex = stripHexPrefix(msg);
406
+ if (rawHex.length % 2 === 0 && isHexString(msg)) {
407
+ // Message is valid hex-encoded bytes.
408
+ message = Buffer.from(rawHex, 'hex');
409
+ }
410
+ else {
411
+ // Malformed 0x-prefixed strings are literal text, not lossy hex.
412
+ message = Buffer.from(msg, 'utf8');
413
+ }
414
+ }
415
+ else {
416
+ // Message is plain text
417
+ message = Buffer.from(msg, 'utf8');
418
+ }
419
+ return signLocalPersonalMessage(message, decryptedPrivateKey);
420
+ }
421
+ catch (error) {
422
+ throw error;
423
+ }
424
+ };
425
+ const signPersonalMessageWithLedger = async () => {
426
+ try {
427
+ // Handle both hex-encoded and plain text messages for personal_sign
428
+ let messageForLedger;
429
+ if (msg.startsWith('0x')) {
430
+ // Message is hex-encoded, remove 0x prefix
431
+ messageForLedger = msg.replace('0x', '');
432
+ }
433
+ else {
434
+ // Message is plain text, convert to hex
435
+ messageForLedger = Buffer.from(msg, 'utf8').toString('hex');
436
+ }
437
+ const response = await this.requireLedgerSigner().evm.signPersonalMessage({
438
+ accountIndex: activeAccountId,
439
+ expectedAddress: activeAccount.address,
440
+ message: messageForLedger,
441
+ });
442
+ return response;
443
+ }
444
+ catch (error) {
445
+ throw error;
446
+ }
447
+ };
448
+ const signPersonalMessageWithTrezor = async () => {
449
+ try {
450
+ // Handle both hex-encoded and plain text messages for personal_sign
451
+ let messageForTrezor;
452
+ if (msg.startsWith('0x')) {
453
+ // Message is hex-encoded, keep as is
454
+ messageForTrezor = msg;
455
+ }
456
+ else {
457
+ // Message is plain text, convert to hex with 0x prefix
458
+ messageForTrezor = '0x' + Buffer.from(msg, 'utf8').toString('hex');
459
+ }
460
+ // For EVM networks, Trezor expects 'eth' regardless of the network's currency
461
+ const trezorCoin = activeNetwork.slip44 === 60 ? 'eth' : activeNetwork.currency;
462
+ const response = await this.requireTrezorSigner().signMessage({
463
+ coin: trezorCoin,
464
+ address: activeAccount.address,
465
+ index: activeAccountId,
466
+ message: messageForTrezor,
467
+ slip44: activeNetwork.slip44,
468
+ });
469
+ return response.signature;
470
+ }
471
+ catch (error) {
472
+ throw error;
473
+ }
474
+ };
475
+ switch (activeAccountType) {
476
+ case KeyringAccountType.Trezor:
477
+ return await signPersonalMessageWithTrezor();
478
+ case KeyringAccountType.Ledger:
479
+ return await signPersonalMessageWithLedger();
480
+ default:
481
+ return signPersonalMessageWithDefaultWallet();
482
+ }
483
+ };
484
+ this.parsePersonalMessage = (hexMsg) => {
485
+ try {
486
+ return parseLocalPersonalMessage(hexMsg);
487
+ }
488
+ catch (error) {
489
+ throw error;
490
+ }
491
+ };
492
+ this.verifyPersonalMessage = (message, sign) => {
493
+ try {
494
+ const msgParams = {
495
+ data: message,
496
+ signature: sign,
497
+ };
498
+ return recoverPersonalSignature(msgParams);
499
+ }
500
+ catch (error) {
501
+ throw error;
502
+ }
503
+ };
504
+ this.getEncryptedPubKey = () => {
505
+ const { activeAccountType } = this.getState();
506
+ // Hardware wallets don't support encryption public key generation
507
+ if (activeAccountType === KeyringAccountType.Trezor ||
508
+ activeAccountType === KeyringAccountType.Ledger) {
509
+ throw new Error('Hardware wallets do not support eth_getEncryptionPublicKey');
510
+ }
511
+ const { decryptedPrivateKey } = this.getDecryptedPrivateKey();
512
+ try {
513
+ return getEncryptionPublicKey(stripHexPrefix(decryptedPrivateKey));
514
+ }
515
+ catch (error) {
516
+ throw error;
517
+ }
518
+ };
519
+ // eth_decryptMessage
520
+ this.decryptMessage = (msgParams) => {
521
+ const { activeAccountType } = this.getState();
522
+ // Hardware wallets don't support message decryption
523
+ if (activeAccountType === KeyringAccountType.Trezor ||
524
+ activeAccountType === KeyringAccountType.Ledger) {
525
+ throw new Error('Hardware wallets do not support eth_decrypt');
526
+ }
527
+ const { address, decryptedPrivateKey } = this.getDecryptedPrivateKey();
528
+ let encryptedData = '';
529
+ if (msgParams[0].toLowerCase() === address.toLowerCase()) {
530
+ encryptedData = msgParams[1];
531
+ }
532
+ else if (msgParams[1].toLowerCase() === address.toLowerCase()) {
533
+ encryptedData = msgParams[0];
534
+ }
535
+ else {
536
+ throw new Error('Decrypting for wrong receiver');
537
+ }
538
+ encryptedData = stripHexPrefix(encryptedData);
539
+ try {
540
+ const buff = Buffer.from(encryptedData, 'hex');
541
+ const cleanData = JSON.parse(buff.toString('utf8'));
542
+ const sig = decrypt({
543
+ encryptedData: cleanData,
544
+ privateKey: stripHexPrefix(decryptedPrivateKey),
545
+ });
546
+ return sig;
547
+ }
548
+ catch (error) {
549
+ throw error;
550
+ }
551
+ };
552
+ this.toBigNumber = (aBigNumberish) => BigNumber.from(String(aBigNumberish));
553
+ this.toHex0x = (value, fieldName) => {
554
+ if (value === undefined || value === null)
555
+ return '0x0';
556
+ if (typeof value === 'string') {
557
+ const v = value.trim();
558
+ if (v === '')
559
+ return '0x0';
560
+ if (v.startsWith('0x'))
561
+ return v;
562
+ return BigNumber.from(v).toHexString();
563
+ }
564
+ if (typeof value === 'number')
565
+ return BigNumber.from(value).toHexString();
566
+ if (BigNumber.isBigNumber(value))
567
+ return value.toHexString();
568
+ const maybeHex = value?._hex ?? value?.hex ?? value;
569
+ try {
570
+ return BigNumber.from(maybeHex).toHexString();
571
+ }
572
+ catch (_e) {
573
+ throw new Error(`Invalid numeric field "${fieldName}" for EVM tx: ${String(value)}`);
574
+ }
575
+ };
576
+ this.captureReviewedTokenApproval = (approval, standard) => {
577
+ if (!approval ||
578
+ typeof approval.assertCurrent !== 'function' ||
579
+ !(approval.signal instanceof AbortSignal))
580
+ throw new Error(`${standard} transaction approval required`);
581
+ const { assertCurrent, signal } = approval;
582
+ const assertApproval = () => {
583
+ if (signal.aborted)
584
+ throw Object.assign(new Error(`${standard} transaction approval expired`), {
585
+ transactionNotBroadcast: true,
586
+ });
587
+ assertSynchronousOriginalValidation(assertCurrent(), `${standard} transaction approval must be synchronous`);
588
+ };
589
+ assertApproval();
590
+ return { assertApproval, signal };
591
+ };
592
+ this.reviewedTokenFees = (request, chainId, standard) => {
593
+ const { isLegacy } = request;
594
+ if (typeof isLegacy !== 'boolean' ||
595
+ (isLegacy
596
+ ? request.maxFeePerGas !== undefined ||
597
+ request.maxPriorityFeePerGas !== undefined
598
+ : request.gasPrice !== undefined))
599
+ throw new Error(`Invalid ${standard} transaction fee model`);
600
+ const gasLimit = BigNumber.from(assertEvmGasLimit(parseEvmFeeQuantity(request.gasLimit, `${standard} gas limit`), BigInt(chainId)));
601
+ const gasPrice = isLegacy
602
+ ? BigNumber.from(parseEvmFeeQuantity(request.gasPrice, `${standard} gas price`))
603
+ : undefined;
604
+ const maxFeePerGas = !isLegacy
605
+ ? BigNumber.from(parseEvmFeeQuantity(request.maxFeePerGas, `${standard} maximum fee`))
606
+ : undefined;
607
+ const maxPriorityFeePerGas = !isLegacy
608
+ ? BigNumber.from(parseEvmFeeQuantity(request.maxPriorityFeePerGas, `${standard} priority fee`))
609
+ : undefined;
610
+ if (gasPrice?.isZero() ||
611
+ (maxFeePerGas &&
612
+ (maxFeePerGas.isZero() || maxPriorityFeePerGas.gt(maxFeePerGas))))
613
+ throw new Error(`Invalid ${standard} fees`);
614
+ return { gasLimit, isLegacy, gasPrice, maxFeePerGas, maxPriorityFeePerGas };
615
+ };
616
+ this.getData = ({ contractAddress, receivingAddress, value, }) => {
617
+ const abi = getErc20Abi();
618
+ try {
619
+ const contract = createContractUsingAbi(abi, contractAddress, this.web3Provider);
620
+ const data = contract.interface.encodeFunctionData('transfer', [
621
+ receivingAddress,
622
+ value,
623
+ ]);
624
+ return data;
625
+ }
626
+ catch (error) {
627
+ throw error;
628
+ }
629
+ };
630
+ this.getFeeDataWithDynamicMaxPriorityFeePerGas = async (provider = this.web3Provider) => {
631
+ const fee = await getEvmFeeData(provider, {
632
+ signal: this.abortController.signal,
633
+ minPriorityFee: this.gasOverrides.minPriorityFee,
634
+ feeMultiplier: this.gasOverrides.feeMultiplier,
635
+ });
636
+ return {
637
+ gasPrice: BigNumber.from(fee.gasPrice),
638
+ ...(fee.baseFeePerGas === undefined
639
+ ? {}
640
+ : {
641
+ baseFeePerGas: BigNumber.from(fee.baseFeePerGas),
642
+ }),
643
+ maxFeePerGas: BigNumber.from(fee.maxFeePerGas),
644
+ maxPriorityFeePerGas: BigNumber.from(fee.maxPriorityFeePerGas),
645
+ };
646
+ };
647
+ this.calculateNewGasValues = (oldTxsParams, isForCancel, isLegacy) => {
648
+ const newGasValues = {
649
+ maxFeePerGas: undefined,
650
+ maxPriorityFeePerGas: undefined,
651
+ gasPrice: undefined,
652
+ gasLimit: undefined,
653
+ };
654
+ const { maxFeePerGas, maxPriorityFeePerGas, gasLimit, gasPrice } = oldTxsParams;
655
+ const calculateAndConvertNewValue = (feeValue) => {
656
+ // Apply multiplier for replacement transaction (cancel or speedup)
657
+ const newValue = feeValue * multiplierToUse;
658
+ const calculateValue = String(newValue);
659
+ const convertValueToHex = '0x' + parseInt(calculateValue, 10).toString(16);
660
+ return BigNumber.from(convertValueToHex);
661
+ };
662
+ const maxFeePerGasToNumber = maxFeePerGas?.toNumber();
663
+ const maxPriorityFeePerGasToNumber = maxPriorityFeePerGas?.toNumber();
664
+ const gasLimitToNumber = gasLimit?.toNumber();
665
+ const gasPriceToNumber = gasPrice?.toNumber();
666
+ const multiplierToUse = 1.2; //The same calculation we used in the edit fee modal, always using the 0.2 multiplier
667
+ if (!isLegacy) {
668
+ // For EIP-1559 transactions
669
+ newGasValues.maxFeePerGas = calculateAndConvertNewValue(maxFeePerGasToNumber);
670
+ newGasValues.maxPriorityFeePerGas = calculateAndConvertNewValue(maxPriorityFeePerGasToNumber);
671
+ }
672
+ if (isLegacy) {
673
+ newGasValues.gasPrice = calculateAndConvertNewValue(gasPriceToNumber);
674
+ }
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
+ }
683
+ return newGasValues;
684
+ };
685
+ this.cancelSentTransaction = async (txHash, isLegacy, fallbackNonce, options) => {
686
+ // Cancellation is a signing operation too. Keep its account, network and
687
+ // provider fixed across RPC requests and hardware approval.
688
+ if (typeof options?.assertCurrentContext !== 'function') {
689
+ throw new Error('Cancellation context required');
690
+ }
691
+ if (typeof options.validateOriginal !== 'function') {
692
+ throw new Error('Cancellation original validator required');
693
+ }
694
+ if (fallbackNonce !== undefined &&
695
+ (!Number.isSafeInteger(fallbackNonce) || fallbackNonce < 0)) {
696
+ throw new Error('Invalid cancellation fallback nonce');
697
+ }
698
+ const previewOnly = options.previewOnly === true;
699
+ const approvedMaximumFee = options.approvedMaximumFee;
700
+ if (!previewOnly &&
701
+ (typeof approvedMaximumFee !== 'string' ||
702
+ !/^(0|[1-9][0-9]{0,77})$/.test(approvedMaximumFee))) {
703
+ throw new Error('Cancellation fee approval required');
704
+ }
705
+ const { assertCurrentContext, validateOriginal } = options;
706
+ const assertHostContext = () => assertSynchronousOriginalValidation(assertCurrentContext(), 'Transaction context guard must be synchronous');
707
+ assertHostContext();
708
+ const state = this.getState();
709
+ const selectedType = state.activeAccountType;
710
+ const selectedId = state.activeAccountId;
711
+ const selectedAddress = state.accounts?.[selectedType]?.[selectedId]?.address;
712
+ const activeAccountType = options.targetAccount?.type ?? selectedType;
713
+ const activeAccountId = options.targetAccount?.id ?? selectedId;
714
+ const targetAccount = options.targetAccount
715
+ ? { type: activeAccountType, id: activeAccountId }
716
+ : undefined;
717
+ const activeAccount = {
718
+ ...state.accounts?.[activeAccountType]?.[activeAccountId],
719
+ };
720
+ const activeNetwork = { ...state.activeNetwork };
721
+ if (!selectedAddress ||
722
+ !activeAccount.address ||
723
+ (targetAccount && activeAccountType === KeyringAccountType.SmartAccount)) {
724
+ throw new Error('Cancellation signing account is unavailable');
725
+ }
726
+ if (!previewOnly)
727
+ this.assertHardwareSignerAvailable(targetAccount);
728
+ const provider = this.web3Provider;
729
+ const assertSession = this.captureSessionGuard();
730
+ const assertKeyringContext = () => {
731
+ assertSession();
732
+ const current = this.getState();
733
+ if (current.activeAccountType !== selectedType ||
734
+ current.activeAccountId !== selectedId ||
735
+ current.accounts?.[selectedType]?.[selectedId]?.address?.toLowerCase() !== selectedAddress.toLowerCase() ||
736
+ current.accounts?.[activeAccountType]?.[activeAccountId]?.address?.toLowerCase() !== activeAccount.address.toLowerCase() ||
737
+ [current.activeNetwork, this.getNetwork()].some((selected) => selected.kind !== activeNetwork.kind ||
738
+ selected.chainId !== activeNetwork.chainId ||
739
+ selected.slip44 !== activeNetwork.slip44 ||
740
+ selected.url !== activeNetwork.url) ||
741
+ this.web3Provider !== provider) {
742
+ throw new Error('Wallet signing context changed');
743
+ }
744
+ };
745
+ const assertSigningCurrent = () => {
746
+ assertHostContext();
747
+ assertKeyringContext();
748
+ };
749
+ assertSigningCurrent();
750
+ const submission = previewOnly
751
+ ? undefined
752
+ : this.captureSubmission('cancel', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertSigningCurrent, txHash);
753
+ const assertCurrent = submission?.assertCurrent ?? assertSigningCurrent;
754
+ const currentEip1559Fees = async () => {
755
+ const feeData = await this.getFeeDataWithDynamicMaxPriorityFeePerGas(provider);
756
+ assertCurrent();
757
+ const maxFeePerGas = nonNegativeFeeQuantity(feeData.maxFeePerGas, 'cancellation maximum fee');
758
+ const maxPriorityFeePerGas = nonNegativeFeeQuantity(feeData.maxPriorityFeePerGas, 'cancellation priority fee');
759
+ if (maxFeePerGas.isZero() || maxPriorityFeePerGas.gt(maxFeePerGas)) {
760
+ throw new Error('Invalid cancellation EIP-1559 fees');
761
+ }
762
+ return { maxFeePerGas, maxPriorityFeePerGas };
763
+ };
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
+ };
800
+ }
801
+ 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
+ };
812
+ }
813
+ }
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;
822
+ let feePlan;
823
+ if (isLegacy) {
824
+ const currentGasPrice = parseEvmFeeQuantity(await provider.getGasPrice(), 'cancellation gas price');
825
+ assertCurrent();
826
+ feePlan = planEvmCancellationFees({
827
+ type: 0,
828
+ currentGasPrice,
829
+ originalGasPrice: originalGasPrice == null
830
+ ? undefined
831
+ : parseEvmFeeQuantity(originalGasPrice, 'original gas price'),
832
+ });
833
+ }
834
+ else {
835
+ const currentFees = await currentEip1559Fees();
836
+ feePlan = planEvmCancellationFees({
837
+ type: 2,
838
+ currentMaxFeePerGas: currentFees.maxFeePerGas.toBigInt(),
839
+ currentMaxPriorityFeePerGas: currentFees.maxPriorityFeePerGas.toBigInt(),
840
+ originalMaxFeePerGas: originalMaxFeePerGas == null
841
+ ? undefined
842
+ : parseEvmFeeQuantity(originalMaxFeePerGas, 'original maximum fee'),
843
+ originalMaxPriorityFeePerGas: originalMaxPriorityFeePerGas == null
844
+ ? undefined
845
+ : parseEvmFeeQuantity(originalMaxPriorityFeePerGas, 'original priority fee'),
846
+ });
847
+ }
848
+ const maximumFee = BigNumber.from(feePlan.maximumFee);
849
+ assertCurrent();
850
+ if (!submission)
851
+ return {
852
+ isCanceled: false,
853
+ error: false,
854
+ maximumFee: maximumFee.toString(),
855
+ };
856
+ if (maximumFee.gt(approvedMaximumFee))
857
+ throw new Error('Cancellation fee exceeds approval');
858
+ const assertApprovedEnvelope = (transaction) => {
859
+ const gas = nonNegativeFeeQuantity(transaction.gasLimit, 'cancellation gas limit');
860
+ const cap = nonNegativeFeeQuantity(isLegacy ? transaction.gasPrice : transaction.maxFeePerGas, 'cancellation maximum fee');
861
+ if (!gas.eq(feePlan.gasLimit) || gas.mul(cap).gt(approvedMaximumFee))
862
+ throw new Error('Cancellation fee exceeds approval');
863
+ };
864
+ // Base cancel transaction parameters (same for all wallet types)
865
+ const baseCancelTx = {
866
+ nonce: software ? softwareNonce : tx.nonce,
867
+ from: activeAccount.address,
868
+ to: activeAccount.address,
869
+ value: Zero,
870
+ gasLimit: BigNumber.from(feePlan.gasLimit),
871
+ };
872
+ const changedTxToCancel = feePlan.type === 0
873
+ ? {
874
+ ...baseCancelTx,
875
+ gasPrice: BigNumber.from(feePlan.gasPrice),
876
+ type: 0, // Force Type 0 for legacy cancel
877
+ }
878
+ : {
879
+ ...baseCancelTx,
880
+ maxFeePerGas: BigNumber.from(feePlan.maxFeePerGas),
881
+ maxPriorityFeePerGas: BigNumber.from(feePlan.maxPriorityFeePerGas),
882
+ // Don't set type - let ethers auto-detect
883
+ };
884
+ // Ledger cancel handler
885
+ const cancelWithLedger = async () => {
886
+ try {
887
+ assertCurrent();
888
+ const resolvedParams = await resolveProperties(omit(changedTxToCancel, 'from'));
889
+ assertCurrent();
890
+ const formatParams = {
891
+ ...resolvedParams,
892
+ nonce: resolvedParams.nonce
893
+ ? Number(resolvedParams.nonce.toString())
894
+ : undefined,
895
+ };
896
+ const txFormattedForEthers = isLegacy
897
+ ? {
898
+ ...formatParams,
899
+ chainId: activeNetwork.chainId,
900
+ type: 0, // Need explicit type for hardware wallet serialization
901
+ }
902
+ : {
903
+ ...formatParams,
904
+ chainId: activeNetwork.chainId,
905
+ type: 2, // Need explicit type for hardware wallet serialization
906
+ };
907
+ assertApprovedEnvelope(txFormattedForEthers);
908
+ const rawTx = serializeTransaction(txFormattedForEthers);
909
+ const signature = await this.requireLedgerSigner().evm.signEVMTransaction({
910
+ rawTx: rawTx.replace('0x', ''),
911
+ accountIndex: activeAccountId,
912
+ expectedAddress: activeAccount.address,
913
+ }, await this.authorizeHardwareTransaction(provider, assertCurrent));
914
+ assertCurrent();
915
+ const formattedSignature = {
916
+ r: `0x${signature.r}`,
917
+ s: `0x${signature.s}`,
918
+ v: parseInt(signature.v, 16),
919
+ };
920
+ if (signature) {
921
+ const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
922
+ assertCurrent();
923
+ assertHardwareTransactionSender(signedTx, activeAccount.address);
924
+ const transactionResponse = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, rawTx);
925
+ return {
926
+ isCanceled: true,
927
+ transaction: transactionResponse,
928
+ };
929
+ }
930
+ else {
931
+ return {
932
+ isCanceled: false,
933
+ error: true,
934
+ };
935
+ }
936
+ }
937
+ catch (error) {
938
+ return {
939
+ ...evmSubmissionFailure(error),
940
+ isCanceled: false,
941
+ error: true,
942
+ };
943
+ }
944
+ };
945
+ // Trezor cancel handler
946
+ const cancelWithTrezor = async () => {
947
+ try {
948
+ assertCurrent();
949
+ const trezorCoin = activeNetwork.slip44 === 60 ? 'eth' : activeNetwork.currency;
950
+ const formattedTx = await resolveProperties(omit(changedTxToCancel, 'from'));
951
+ assertCurrent();
952
+ const txFormattedForTrezor = {
953
+ ...formattedTx,
954
+ gasLimit: typeof formattedTx.gasLimit === 'string'
955
+ ? formattedTx.gasLimit
956
+ : this.toHex0x(formattedTx.gasLimit, 'gasLimit'),
957
+ value: '0x0',
958
+ nonce: this.toHex0x(formattedTx.nonce, 'nonce'),
959
+ chainId: activeNetwork.chainId,
960
+ type: isLegacy ? 0 : 2, // Need explicit type for hardware wallet serialization
961
+ };
962
+ if (isLegacy) {
963
+ txFormattedForTrezor.gasPrice =
964
+ typeof formattedTx.gasPrice === 'string'
965
+ ? formattedTx.gasPrice
966
+ : this.toHex0x(formattedTx.gasPrice, 'gasPrice');
967
+ }
968
+ else {
969
+ txFormattedForTrezor.maxFeePerGas =
970
+ typeof formattedTx.maxFeePerGas === 'string'
971
+ ? formattedTx.maxFeePerGas
972
+ : this.toHex0x(formattedTx.maxFeePerGas, 'maxFeePerGas');
973
+ txFormattedForTrezor.maxPriorityFeePerGas =
974
+ typeof formattedTx.maxPriorityFeePerGas === 'string'
975
+ ? formattedTx.maxPriorityFeePerGas
976
+ : this.toHex0x(formattedTx.maxPriorityFeePerGas, 'maxPriorityFeePerGas');
977
+ }
978
+ assertApprovedEnvelope(txFormattedForTrezor);
979
+ assertTransactionGasLimit(txFormattedForTrezor);
980
+ const expectedUnsignedTransaction = serializeTransaction(txFormattedForTrezor);
981
+ const signature = await this.requireTrezorSigner().signEthTransaction({
982
+ coin: trezorCoin,
983
+ tx: txFormattedForTrezor,
984
+ index: activeAccountId.toString(),
985
+ slip44: activeNetwork.slip44,
986
+ }, await this.authorizeHardwareTransaction(provider, assertCurrent));
987
+ assertCurrent();
988
+ if (signature.success) {
989
+ const signedTx = serializeTransaction(txFormattedForTrezor, trezorSignature(signature.payload));
990
+ assertCurrent();
991
+ assertHardwareTransactionSender(signedTx, activeAccount.address);
992
+ const transactionResponse = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, expectedUnsignedTransaction);
993
+ return {
994
+ isCanceled: true,
995
+ transaction: transactionResponse,
996
+ };
997
+ }
998
+ else {
999
+ return {
1000
+ isCanceled: false,
1001
+ error: true,
1002
+ };
1003
+ }
1004
+ }
1005
+ catch (error) {
1006
+ return {
1007
+ ...evmSubmissionFailure(error),
1008
+ isCanceled: false,
1009
+ error: true,
1010
+ };
1011
+ }
1012
+ };
1013
+ // Regular wallet cancel handler
1014
+ const cancelWithPrivateKey = async () => {
1015
+ try {
1016
+ assertCurrent();
1017
+ const { address, decryptedPrivateKey } = this.getDecryptedPrivateKey(targetAccount);
1018
+ if (address?.toLowerCase() !== activeAccount.address.toLowerCase()) {
1019
+ throw new Error('Wallet signing context changed');
1020
+ }
1021
+ const transactionResponse = await sendLocalEvmTransaction(provider, decryptedPrivateKey, changedTxToCancel, assertCurrent, assertCurrent, (transaction) => {
1022
+ assertApprovedEnvelope(transaction);
1023
+ assertEvmCancellationEnvelope(serializeTransaction(transaction), {
1024
+ payer: activeAccount.address,
1025
+ chainId: activeNetwork.chainId,
1026
+ nonce: softwareNonce,
1027
+ feePlan,
1028
+ });
1029
+ }, submission);
1030
+ if (transactionResponse) {
1031
+ return {
1032
+ isCanceled: true,
1033
+ transaction: transactionResponse,
1034
+ };
1035
+ }
1036
+ else {
1037
+ return {
1038
+ isCanceled: false,
1039
+ };
1040
+ }
1041
+ }
1042
+ catch (error) {
1043
+ return {
1044
+ ...evmSubmissionFailure(error),
1045
+ isCanceled: false,
1046
+ error: true,
1047
+ };
1048
+ }
1049
+ };
1050
+ // Route based on account type
1051
+ switch (activeAccountType) {
1052
+ case KeyringAccountType.Trezor:
1053
+ return await cancelWithTrezor();
1054
+ case KeyringAccountType.Ledger:
1055
+ return await cancelWithLedger();
1056
+ default:
1057
+ return await cancelWithPrivateKey();
1058
+ }
1059
+ };
1060
+ //TODO: This function needs to be refactored
1061
+ this.sendFormattedTransaction = async (...args) => {
1062
+ const [params, isLegacy, options] = args;
1063
+ let broadcastAttempted = false;
1064
+ try {
1065
+ if (!options ||
1066
+ typeof options !== 'object' ||
1067
+ Array.isArray(options) ||
1068
+ (options.mode !== 'reviewed-native' &&
1069
+ options.mode !== 'specialized') ||
1070
+ typeof options.assertCurrent !== 'function')
1071
+ throw new Error('Explicit transaction approval is required');
1072
+ // Capture once; later caller mutations cannot replace this invocation's
1073
+ // admission decision while chain checks or device approval are pending.
1074
+ const { assertCurrent: assertHostCurrent, mode } = options;
1075
+ const assertApproved = () => {
1076
+ const result = assertHostCurrent();
1077
+ if (result !== undefined) {
1078
+ assertSynchronousOriginalValidation(result, 'Transaction context guard must be synchronous');
1079
+ throw new Error('Transaction context guard must return undefined');
1080
+ }
1081
+ };
1082
+ assertApproved();
1083
+ return await this.sendFormattedTransactionInternal(params, isLegacy, () => {
1084
+ broadcastAttempted = true;
1085
+ }, assertApproved, mode);
1086
+ }
1087
+ catch (error) {
1088
+ // A broadcast transport error may mean the node accepted the signed
1089
+ // transaction. Derive this marker from this invocation, never RPC data.
1090
+ const submissionError = Object.assign(new Error(error instanceof Error ? error.message : String(error)), error, {
1091
+ transactionNotBroadcast: !broadcastAttempted ||
1092
+ evmSubmissionFailure(error).transactionNotBroadcast,
1093
+ });
1094
+ if (error instanceof Error) {
1095
+ submissionError.name = error.name;
1096
+ submissionError.stack = error.stack;
1097
+ }
1098
+ copyEvmSubmissionFailure(error, submissionError);
1099
+ throw submissionError;
1100
+ }
1101
+ };
1102
+ this.sendFormattedTransactionInternal = async (input, isLegacy, beforeBroadcast, assertHostCurrent, mode) => {
1103
+ // Hardware approval may outlive the selected account, network or unlocked
1104
+ // session. Keep immutable identities and one provider for this invocation.
1105
+ const { activeAccount, activeAccountType, activeAccountId, activeNetwork, provider, assertCurrent: assertKeyringCurrent, } = this.captureEvmSigningContext();
1106
+ const assertSigningCurrent = () => {
1107
+ assertHostCurrent();
1108
+ assertKeyringCurrent();
1109
+ };
1110
+ const assertBeforeBroadcast = () => {
1111
+ assertCurrent();
1112
+ beforeBroadcast();
1113
+ };
1114
+ const submission = this.captureSubmission('send', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertSigningCurrent);
1115
+ const assertCurrent = submission.assertCurrent;
1116
+ if (mode === 'reviewed-native') {
1117
+ const { transaction } = snapshotPreparedEvmRequest({
1118
+ account: {
1119
+ id: activeAccountId,
1120
+ type: activeAccountType,
1121
+ expectedAddress: activeAccount.address,
1122
+ },
1123
+ network: {
1124
+ kind: activeNetwork.kind,
1125
+ chainId: String(activeNetwork.chainId),
1126
+ slip44: activeNetwork.slip44,
1127
+ url: activeNetwork.url,
1128
+ },
1129
+ transaction: input,
1130
+ });
1131
+ const minimum = this.gasOverrides.minGasLimit;
1132
+ if (transaction.data !== '0x' ||
1133
+ BigInt(transaction.value) === 0n ||
1134
+ BigInt(transaction.gasLimit) < 21000n ||
1135
+ (transaction.type === 2 && BigInt(transaction.maxFeePerGas) === 0n) ||
1136
+ (minimum && BigInt(transaction.gasLimit) < minimum.toBigInt()) ||
1137
+ (isLegacy !== undefined && isLegacy !== (transaction.type === 0)))
1138
+ throw new Error('Invalid reviewed native transfer approval');
1139
+ assertCurrent();
1140
+ await provider.verifyConfiguredChainId();
1141
+ assertCurrent();
1142
+ if (this.gasOverrides.minGasLimit !== minimum)
1143
+ throw new Error('Reviewed native gas policy changed');
1144
+ const { address, decryptedPrivateKey } = this.getDecryptedPrivateKey();
1145
+ if (address.toLowerCase() !== transaction.from.toLowerCase())
1146
+ throw new Error('Reviewed native signer does not match the approved account');
1147
+ assertCurrent();
1148
+ const { signed, expectedUnsigned } = signPreparedEvmEnvelope(transaction, decryptedPrivateKey);
1149
+ return submitSignedEvmTransaction(signed.rawTransaction, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, expectedUnsigned);
1150
+ }
1151
+ // Detach supplied values before any RPC/device await. Autofill updates only
1152
+ // this invocation; caller mutations cannot replace already supplied fields.
1153
+ let params = input;
1154
+ if (!params || typeof params !== 'object' || Array.isArray(params))
1155
+ throw new TypeError('Invalid EVM transaction request');
1156
+ const supplied = { ...params };
1157
+ // Validate original scalar spellings before conversion. Normalization's
1158
+ // permissive zero coercion must not bypass the bounded wire parser.
1159
+ for (const field of [
1160
+ 'chainId',
1161
+ 'nonce',
1162
+ 'gasLimit',
1163
+ 'gasPrice',
1164
+ 'maxFeePerGas',
1165
+ 'maxPriorityFeePerGas',
1166
+ 'value',
1167
+ 'type',
1168
+ ]) {
1169
+ if (supplied[field] != null) {
1170
+ const value = parseEvmFeeQuantity(supplied[field], field);
1171
+ if (field === 'nonce' || field === 'type') {
1172
+ if (value > BigInt(Number.MAX_SAFE_INTEGER))
1173
+ throw new Error(`Invalid transaction ${field}`);
1174
+ supplied[field] = Number(value);
1175
+ }
1176
+ else
1177
+ supplied[field] = value;
1178
+ }
1179
+ }
1180
+ if (supplied.data != null)
1181
+ supplied.data = hexlify(supplied.data);
1182
+ if (supplied.accessList != null)
1183
+ supplied.accessList = accessListify(supplied.accessList);
1184
+ params = supplied;
1185
+ assertCurrent();
1186
+ // Capture the reviewed nonce before any fee/RPC await. Hardware and the
1187
+ // final serializer must use the same immutable integer, including zero.
1188
+ let explicitHardwareNonce;
1189
+ if (activeAccountType === KeyringAccountType.Ledger ||
1190
+ activeAccountType === KeyringAccountType.Trezor) {
1191
+ const suppliedNonce = params.nonce;
1192
+ if (suppliedNonce != null) {
1193
+ const nonce = parseEvmFeeQuantity(suppliedNonce, 'transaction nonce');
1194
+ if (nonce > BigInt(Number.MAX_SAFE_INTEGER))
1195
+ throw new Error('Invalid transaction nonce');
1196
+ explicitHardwareNonce = Number(nonce);
1197
+ }
1198
+ }
1199
+ const explicitEip1559 = params.type != null && BigNumber.from(params.type).eq(2);
1200
+ const requestedEip1559 = explicitEip1559 ||
1201
+ params.maxFeePerGas != null ||
1202
+ params.maxPriorityFeePerGas != null;
1203
+ if (isLegacy && requestedEip1559)
1204
+ throw new Error('EIP-1559 fees conflict with legacy pricing');
1205
+ const validateEip1559Fees = () => {
1206
+ const maximum = nonNegativeFeeQuantity(params.maxFeePerGas, 'maximum fee');
1207
+ const priority = nonNegativeFeeQuantity(params.maxPriorityFeePerGas, 'priority fee');
1208
+ if (maximum.isZero() || priority.gt(maximum))
1209
+ throw new Error('Invalid EIP-1559 fees');
1210
+ params.maxFeePerGas = maximum;
1211
+ params.maxPriorityFeePerGas = priority;
1212
+ };
1213
+ if (!isLegacy &&
1214
+ params.maxFeePerGas != null &&
1215
+ params.maxPriorityFeePerGas != null)
1216
+ validateEip1559Fees();
1217
+ // Only apply a minimum gas limit when a network-specific override is set.
1218
+ // Otherwise keep the user/dapp-approved gas limit unchanged.
1219
+ if (params.gasLimit != null && this.gasOverrides.minGasLimit) {
1220
+ const minGasLimit = this.gasOverrides.minGasLimit;
1221
+ const currentGasLimit = BigNumber.from(params.gasLimit);
1222
+ if (currentGasLimit.lt(minGasLimit)) {
1223
+ params.gasLimit = minGasLimit;
1224
+ }
1225
+ }
1226
+ const txForEstimation = isLegacy
1227
+ ? omit({
1228
+ ...params,
1229
+ from: params.from || activeAccount.address,
1230
+ }, ['maxFeePerGas', 'maxPriorityFeePerGas', 'type'])
1231
+ : omit({
1232
+ ...params,
1233
+ from: params.from || activeAccount.address,
1234
+ }, ['gasPrice']);
1235
+ if (params.gasLimit == null) {
1236
+ params.gasLimit = await provider.estimateGas(txForEstimation);
1237
+ assertCurrent();
1238
+ }
1239
+ assertTransactionGasLimit({
1240
+ gasLimit: params.gasLimit,
1241
+ chainId: activeNetwork.chainId,
1242
+ });
1243
+ if (isLegacy) {
1244
+ if (params.gasPrice == null) {
1245
+ params.gasPrice = await provider.getGasPrice();
1246
+ assertCurrent();
1247
+ }
1248
+ }
1249
+ else if (params.maxFeePerGas == null ||
1250
+ params.maxPriorityFeePerGas == null) {
1251
+ const feeData = await provider.getFeeData();
1252
+ assertCurrent();
1253
+ params.maxFeePerGas = params.maxFeePerGas ?? feeData.maxFeePerGas;
1254
+ params.maxPriorityFeePerGas =
1255
+ params.maxPriorityFeePerGas ?? feeData.maxPriorityFeePerGas;
1256
+ if (params.maxFeePerGas == null || params.maxPriorityFeePerGas == null) {
1257
+ if (requestedEip1559)
1258
+ throw new Error('EIP-1559 fee data unavailable');
1259
+ isLegacy = true;
1260
+ params.gasPrice = feeData.gasPrice ?? (await provider.getGasPrice());
1261
+ assertCurrent();
1262
+ delete params.maxFeePerGas;
1263
+ delete params.maxPriorityFeePerGas;
1264
+ }
1265
+ }
1266
+ if (!isLegacy)
1267
+ validateEip1559Fees();
1268
+ const sendEVMLedgerTransaction = async () => {
1269
+ assertCurrent();
1270
+ const transactionNonce = explicitHardwareNonce ??
1271
+ (await provider.getTransactionCount(activeAccount.address, 'pending'));
1272
+ assertCurrent();
1273
+ const formatParams = isLegacy
1274
+ ? omit(params, 'from')
1275
+ : omit(params, ['from', 'gasPrice']); // Strip gasPrice for EIP-1559
1276
+ const txFormattedForEthers = isLegacy
1277
+ ? {
1278
+ ...formatParams,
1279
+ nonce: transactionNonce,
1280
+ chainId: activeNetwork.chainId,
1281
+ type: 0, // Force Type 0 for legacy
1282
+ }
1283
+ : {
1284
+ ...formatParams,
1285
+ nonce: transactionNonce,
1286
+ chainId: activeNetwork.chainId,
1287
+ type: 2, // Need explicit type for hardware wallet serialization
1288
+ };
1289
+ const rawTx = serializeTransaction(txFormattedForEthers);
1290
+ const signature = await this.requireLedgerSigner().evm.signEVMTransaction({
1291
+ rawTx: rawTx.replace('0x', ''),
1292
+ accountIndex: activeAccountId,
1293
+ expectedAddress: activeAccount.address,
1294
+ }, await this.authorizeHardwareTransaction(provider, assertCurrent));
1295
+ assertCurrent();
1296
+ const formattedSignature = {
1297
+ r: `0x${signature.r}`,
1298
+ s: `0x${signature.s}`,
1299
+ v: parseInt(signature.v, 16),
1300
+ };
1301
+ if (signature) {
1302
+ try {
1303
+ const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
1304
+ assertHardwareTransactionSender(signedTx, activeAccount.address);
1305
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, rawTx);
1306
+ return finalTx;
1307
+ }
1308
+ catch (error) {
1309
+ throw error;
1310
+ }
1311
+ }
1312
+ else {
1313
+ throw new Error(`Transaction Signature Failed. Error: ${signature}`);
1314
+ }
1315
+ };
1316
+ const sendEVMTrezorTransaction = async () => {
1317
+ assertCurrent();
1318
+ const transactionNonce = explicitHardwareNonce ??
1319
+ (await provider.getTransactionCount(activeAccount.address, 'pending'));
1320
+ assertCurrent();
1321
+ let txFormattedForTrezor = {};
1322
+ const formatParams = isLegacy
1323
+ ? omit(params, 'from')
1324
+ : omit(params, ['from', 'gasPrice']); // Strip gasPrice for EIP-1559
1325
+ switch (isLegacy) {
1326
+ case true:
1327
+ txFormattedForTrezor = {
1328
+ ...formatParams,
1329
+ gasPrice: this.toHex0x(params.gasPrice, 'gasPrice'),
1330
+ gasLimit: this.toHex0x(formatParams.gasLimit ?? params.gasLimit, 'gasLimit'),
1331
+ value: this.toHex0x(formatParams.value ?? params.value, 'value'),
1332
+ nonce: this.toBigNumber(transactionNonce)._hex,
1333
+ chainId: activeNetwork.chainId,
1334
+ data: formatParams.data ?? '0x',
1335
+ };
1336
+ break;
1337
+ case false:
1338
+ txFormattedForTrezor = {
1339
+ ...formatParams,
1340
+ gasLimit: this.toHex0x(formatParams.gasLimit ?? params.gasLimit, 'gasLimit'),
1341
+ maxFeePerGas: this.toHex0x(formatParams.maxFeePerGas ??
1342
+ params.maxFeePerGas, 'maxFeePerGas'),
1343
+ maxPriorityFeePerGas: this.toHex0x(formatParams.maxPriorityFeePerGas ??
1344
+ params.maxPriorityFeePerGas, 'maxPriorityFeePerGas'),
1345
+ value: this.toHex0x(formatParams.value ?? params.value, 'value'),
1346
+ nonce: this.toBigNumber(transactionNonce)._hex,
1347
+ chainId: activeNetwork.chainId,
1348
+ data: formatParams.data ?? '0x',
1349
+ };
1350
+ break;
1351
+ default:
1352
+ txFormattedForTrezor = {
1353
+ ...formatParams,
1354
+ gasLimit: this.toHex0x(formatParams.gasLimit ?? params.gasLimit, 'gasLimit'),
1355
+ maxFeePerGas: this.toHex0x(formatParams.maxFeePerGas ??
1356
+ params.maxFeePerGas, 'maxFeePerGas'),
1357
+ maxPriorityFeePerGas: this.toHex0x(formatParams.maxPriorityFeePerGas ??
1358
+ params.maxPriorityFeePerGas, 'maxPriorityFeePerGas'),
1359
+ value: this.toHex0x(formatParams.value ?? params.value, 'value'),
1360
+ nonce: this.toBigNumber(transactionNonce)._hex,
1361
+ chainId: activeNetwork.chainId,
1362
+ data: formatParams.data ?? '0x',
1363
+ };
1364
+ break;
1365
+ }
1366
+ assertTransactionGasLimit(txFormattedForTrezor);
1367
+ const expectedUnsignedTransaction = serializeTransaction({
1368
+ ...txFormattedForTrezor,
1369
+ type: isLegacy ? 0 : 2,
1370
+ });
1371
+ const signature = await this.requireTrezorSigner().signEthTransaction({
1372
+ index: `${activeAccountId}`,
1373
+ tx: txFormattedForTrezor,
1374
+ coin: activeNetwork.currency,
1375
+ slip44: activeNetwork.slip44,
1376
+ }, await this.authorizeHardwareTransaction(provider, assertCurrent));
1377
+ assertCurrent();
1378
+ if (signature.success) {
1379
+ try {
1380
+ const txFormattedForEthers = isLegacy
1381
+ ? {
1382
+ ...formatParams,
1383
+ nonce: transactionNonce,
1384
+ chainId: activeNetwork.chainId,
1385
+ type: 0, // Force Type 0 for legacy
1386
+ }
1387
+ : {
1388
+ ...formatParams,
1389
+ nonce: transactionNonce,
1390
+ chainId: activeNetwork.chainId,
1391
+ type: 2, // Need explicit type for hardware wallet serialization
1392
+ };
1393
+ const signedTx = serializeTransaction(txFormattedForEthers, trezorSignature(signature.payload));
1394
+ assertHardwareTransactionSender(signedTx, activeAccount.address);
1395
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, expectedUnsignedTransaction);
1396
+ return finalTx;
1397
+ }
1398
+ catch (error) {
1399
+ throw error;
1400
+ }
1401
+ }
1402
+ else {
1403
+ throw new Error(`Transaction Signature Failed. Error: ${signature}`);
1404
+ }
1405
+ };
1406
+ const sendEVMTransaction = async () => {
1407
+ assertCurrent();
1408
+ const { address, decryptedPrivateKey } = this.getDecryptedPrivateKey();
1409
+ // Validate that we have the correct private key for the active account to prevent race conditions
1410
+ // This is critical for transaction security during account switches
1411
+ if (address.toLowerCase() !== activeAccount.address.toLowerCase()) {
1412
+ throw new Error(`Account state mismatch detected during transaction. Expected ${activeAccount.address} but got ${address}. Please wait for account switching to complete and try again.`);
1413
+ }
1414
+ // Explicitly set transaction type based on isLegacy flag
1415
+ // For EIP-1559, ensure gasPrice is not present to avoid ethers throwing
1416
+ const tx = isLegacy
1417
+ ? { ...params, type: 0 } // Force Type 0 for legacy transactions
1418
+ : omit(params, ['gasPrice']); // Strip gasPrice for EIP-1559
1419
+ try {
1420
+ const transaction = await sendLocalEvmTransaction(provider, decryptedPrivateKey, tx, assertBeforeBroadcast, assertCurrent, undefined, submission);
1421
+ // The broadcast response already has the hash. Pending transactions
1422
+ // have no block number, so a block lookup can fail after a successful
1423
+ // broadcast and incorrectly report the send as failed.
1424
+ return transaction;
1425
+ }
1426
+ catch (error) {
1427
+ throw error;
1428
+ }
1429
+ };
1430
+ switch (activeAccountType) {
1431
+ case KeyringAccountType.Trezor:
1432
+ return await sendEVMTrezorTransaction();
1433
+ case KeyringAccountType.Ledger:
1434
+ return await sendEVMLedgerTransaction();
1435
+ default:
1436
+ return await sendEVMTransaction();
1437
+ }
1438
+ };
1439
+ this.sendTransactionWithEditedFee = async (txHash, isLegacy, options) => {
1440
+ const previewOnly = options?.previewOnly === true;
1441
+ const approvedMaximumFee = options?.approvedMaximumFee;
1442
+ if (!previewOnly &&
1443
+ (typeof approvedMaximumFee !== 'string' ||
1444
+ !/^(0|[1-9][0-9]{0,77})$/.test(approvedMaximumFee)))
1445
+ throw new Error('Replacement fee approval required');
1446
+ if (typeof options?.assertCurrentContext !== 'function')
1447
+ throw new Error('Replacement context required');
1448
+ if (typeof options.validateOriginal !== 'function')
1449
+ throw new Error('Replacement original validator required');
1450
+ const { assertCurrentContext, validateOriginal, beforeBroadcast } = options;
1451
+ if (!previewOnly)
1452
+ this.assertHardwareSignerAvailable(options.targetAccount);
1453
+ isLegacy ?? (isLegacy = false);
1454
+ let assertCapturedSignerCurrent;
1455
+ const assertSigningCurrent = () => {
1456
+ assertSynchronousOriginalValidation(assertCurrentContext(), 'Transaction context guard must be synchronous');
1457
+ assertCapturedSignerCurrent?.();
1458
+ };
1459
+ const assertBeforeBroadcast = () => {
1460
+ assertSigningCurrent();
1461
+ assertSynchronousOriginalValidation(beforeBroadcast?.(), 'Transaction broadcast guard must be synchronous');
1462
+ assertSigningCurrent();
1463
+ };
1464
+ const assertSignedSender = (signedTx) => {
1465
+ if (Transaction.from(signedTx).from?.toLowerCase() !== expectedSender) {
1466
+ throw new Error('Replacement signature does not match account');
1467
+ }
1468
+ };
1469
+ assertSigningCurrent();
1470
+ const state = this.getState();
1471
+ const activeAccountType = options.targetAccount?.type ?? state.activeAccountType;
1472
+ const activeAccountId = options.targetAccount?.id ?? state.activeAccountId;
1473
+ const targetAccount = options.targetAccount
1474
+ ? { type: activeAccountType, id: activeAccountId }
1475
+ : undefined;
1476
+ const activeAccount = {
1477
+ ...state.accounts?.[activeAccountType]?.[activeAccountId],
1478
+ };
1479
+ const activeNetwork = { ...state.activeNetwork };
1480
+ if (!activeAccount.address ||
1481
+ (targetAccount && activeAccountType === KeyringAccountType.SmartAccount)) {
1482
+ throw new Error('Replacement signing account is unavailable');
1483
+ }
1484
+ if (!previewOnly)
1485
+ assertCapturedSignerCurrent =
1486
+ this.captureEvmSigningContext(targetAccount).assertCurrent;
1487
+ const expectedSender = activeAccount.address.toLowerCase();
1488
+ const requestedHash = txHash.toLowerCase();
1489
+ const provider = this.web3Provider;
1490
+ assertSigningCurrent();
1491
+ const submission = previewOnly
1492
+ ? undefined
1493
+ : this.captureSubmission('speed-up', activeAccount, activeAccountType, activeAccountId, activeNetwork, assertSigningCurrent, txHash);
1494
+ const assertCurrent = submission?.assertCurrent ?? assertSigningCurrent;
1495
+ const validateLookup = (transaction) => {
1496
+ assertCurrent();
1497
+ if (!transaction)
1498
+ return null;
1499
+ const authenticated = authenticateReplacementOriginal(transaction, requestedHash, expectedSender, activeNetwork.chainId);
1500
+ const type = authenticated.type;
1501
+ if (type !== 1)
1502
+ isLegacy = type === 0;
1503
+ if (isLegacy &&
1504
+ type === 1 &&
1505
+ activeAccountType === KeyringAccountType.Trezor) {
1506
+ throw new Error('Trezor does not support type 1 replacement signing');
1507
+ }
1508
+ assertSynchronousOriginalValidation(validateOriginal(authenticated));
1509
+ assertCurrent();
1510
+ return authenticated;
1511
+ };
1512
+ let tx = validateLookup(await provider.getTransaction(txHash));
1513
+ if (!tx) {
1514
+ // Retry a couple of times in case the node hasn't indexed the pending tx yet
1515
+ for (let attempt = 0; attempt < 2 && !tx; attempt++) {
1516
+ await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
1517
+ assertCurrent();
1518
+ tx = validateLookup(await provider.getTransaction(txHash));
1519
+ }
1520
+ if (!tx) {
1521
+ return {
1522
+ isSpeedUp: false,
1523
+ error: true,
1524
+ code: 'TX_NOT_FOUND',
1525
+ message: 'Original transaction not yet available from RPC provider',
1526
+ };
1527
+ }
1528
+ }
1529
+ // Check if this might be a max send transaction by comparing total cost to balance
1530
+ const currentBalance = await provider.getBalance(activeAccount.address);
1531
+ assertCurrent();
1532
+ // Ensure all transaction values are resolved from promises
1533
+ const gasLimit = await Promise.resolve(tx.gasLimit);
1534
+ const gasPrice = await Promise.resolve(tx.gasPrice || 0);
1535
+ const maxFeePerGas = await Promise.resolve(tx.type === 1 ? tx.gasPrice : tx.maxFeePerGas);
1536
+ const maxPriorityFeePerGas = await Promise.resolve(tx.type === 1 ? tx.gasPrice : tx.maxPriorityFeePerGas);
1537
+ const txValue = await Promise.resolve(tx.value);
1538
+ const txData = await Promise.resolve(tx.data || '0x');
1539
+ 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
+ const oldTxsGasValues = {
1550
+ maxFeePerGas: maxFeePerGas,
1551
+ maxPriorityFeePerGas: maxPriorityFeePerGas,
1552
+ gasPrice: gasPrice,
1553
+ gasLimit: gasLimit,
1554
+ };
1555
+ // A zero-fee original may need a current estimate; the preview shows it
1556
+ // and the final candidate must stay within the user's approved ceiling.
1557
+ if (isLegacy && oldTxsGasValues.gasPrice.isZero()) {
1558
+ oldTxsGasValues.gasPrice = await provider.getGasPrice();
1559
+ assertCurrent();
1560
+ }
1561
+ else if (!isLegacy && oldTxsGasValues.maxFeePerGas.isZero()) {
1562
+ const feeData = await this.getFeeDataWithDynamicMaxPriorityFeePerGas(provider);
1563
+ assertCurrent();
1564
+ oldTxsGasValues.maxFeePerGas = BigNumber.from(feeData.maxFeePerGas || 0);
1565
+ oldTxsGasValues.maxPriorityFeePerGas = BigNumber.from(feeData.maxPriorityFeePerGas || 0);
1566
+ }
1567
+ const newGasValues = this.calculateNewGasValues(oldTxsGasValues, false, isLegacy);
1568
+ if (!newGasValues.gasLimit.gt(0))
1569
+ throw new Error('Invalid replacement gas limit');
1570
+ if (!isLegacy &&
1571
+ newGasValues.maxPriorityFeePerGas.gt(newGasValues.maxFeePerGas))
1572
+ throw new Error('Invalid replacement fee');
1573
+ 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');
1577
+ return {
1578
+ isSpeedUp: false,
1579
+ error: true,
1580
+ code: 'CONTRACT_CALL_MAX_SEND',
1581
+ message: 'Cannot speed up a likely max-send contract call; value cannot be adjusted to fit new gas',
1582
+ };
1583
+ }
1584
+ return {
1585
+ isSpeedUp: false,
1586
+ error: true,
1587
+ code: 'REPLACEMENT_VALUE_WOULD_CHANGE',
1588
+ message: 'Add funds before speeding up this transaction; the original amount will not be changed',
1589
+ };
1590
+ }
1591
+ const txWithEditedFee = {
1592
+ chainId: activeNetwork.chainId,
1593
+ from: tx.from,
1594
+ to: tx.to,
1595
+ nonce: tx.nonce,
1596
+ value: txValue,
1597
+ data: txData,
1598
+ gasLimit: newGasValues.gasLimit,
1599
+ ...(tx.accessList != null ? { accessList: tx.accessList } : {}),
1600
+ ...(isLegacy
1601
+ ? { gasPrice: newGasValues.gasPrice, type: tx.type === 1 ? 1 : 0 }
1602
+ : {
1603
+ maxFeePerGas: newGasValues.maxFeePerGas,
1604
+ maxPriorityFeePerGas: newGasValues.maxPriorityFeePerGas,
1605
+ }),
1606
+ };
1607
+ if (!submission)
1608
+ return { isSpeedUp: false, maximumFee: maximumFee.toString() };
1609
+ if (maximumFee.gt(approvedMaximumFee))
1610
+ throw new Error('Replacement fee exceeds approval');
1611
+ // Ledger speedup handler
1612
+ const speedUpWithLedger = async () => {
1613
+ try {
1614
+ const resolvedParams = await resolveProperties(omit(txWithEditedFee, 'from'));
1615
+ const formatParams = {
1616
+ ...resolvedParams,
1617
+ nonce: resolvedParams.nonce != null
1618
+ ? Number(resolvedParams.nonce.toString())
1619
+ : undefined,
1620
+ };
1621
+ const txFormattedForEthers = isLegacy
1622
+ ? {
1623
+ ...formatParams,
1624
+ chainId: activeNetwork.chainId,
1625
+ type: txWithEditedFee.type,
1626
+ }
1627
+ : {
1628
+ ...formatParams,
1629
+ chainId: activeNetwork.chainId,
1630
+ type: 2, // Need explicit type for hardware wallet serialization
1631
+ };
1632
+ const rawTx = serializeTransaction(txFormattedForEthers);
1633
+ assertCurrent();
1634
+ const signature = await this.requireLedgerSigner().evm.signEVMTransaction({
1635
+ rawTx: rawTx.replace('0x', ''),
1636
+ accountIndex: activeAccountId,
1637
+ expectedAddress: activeAccount.address,
1638
+ }, await this.authorizeHardwareTransaction(provider, assertCurrent));
1639
+ const formattedSignature = {
1640
+ r: `0x${signature.r}`,
1641
+ s: `0x${signature.s}`,
1642
+ v: parseInt(signature.v, 16),
1643
+ };
1644
+ if (signature) {
1645
+ const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
1646
+ assertSignedSender(signedTx);
1647
+ assertHardwareTransactionSender(signedTx, activeAccount.address);
1648
+ const transactionResponse = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, rawTx);
1649
+ return {
1650
+ isSpeedUp: true,
1651
+ transaction: transactionResponse,
1652
+ };
1653
+ }
1654
+ else {
1655
+ return {
1656
+ isSpeedUp: false,
1657
+ error: true,
1658
+ };
1659
+ }
1660
+ }
1661
+ catch (error) {
1662
+ console.error('[SpeedUp] Failed to send replacement transaction with Ledger:', error);
1663
+ const message = error?.message || String(error);
1664
+ const lower = message.toLowerCase();
1665
+ const code = lower.includes('underpriced')
1666
+ ? 'REPLACEMENT_UNDERPRICED'
1667
+ : lower.includes('known transaction') ||
1668
+ lower.includes('already known')
1669
+ ? 'REPLACEMENT_ALREADY_KNOWN'
1670
+ : 'REPLACEMENT_SEND_FAILED';
1671
+ return {
1672
+ ...evmSubmissionFailure(error),
1673
+ isSpeedUp: false,
1674
+ error: true,
1675
+ code,
1676
+ message,
1677
+ };
1678
+ }
1679
+ };
1680
+ // Trezor speedup handler
1681
+ const speedUpWithTrezor = async () => {
1682
+ try {
1683
+ const trezorCoin = activeNetwork.slip44 === 60 ? 'eth' : activeNetwork.currency;
1684
+ const formattedTx = await resolveProperties(omit(txWithEditedFee, 'from'));
1685
+ const txFormattedForTrezor = {
1686
+ ...formattedTx,
1687
+ gasLimit: typeof formattedTx.gasLimit === 'string'
1688
+ ? formattedTx.gasLimit
1689
+ : this.toHex0x(formattedTx.gasLimit, 'gasLimit'),
1690
+ value: typeof formattedTx.value === 'string'
1691
+ ? formattedTx.value
1692
+ : this.toHex0x(formattedTx.value, 'value'),
1693
+ nonce: this.toHex0x(formattedTx.nonce, 'nonce'),
1694
+ chainId: activeNetwork.chainId,
1695
+ type: isLegacy ? txWithEditedFee.type : 2,
1696
+ };
1697
+ if (formattedTx.data && formattedTx.data !== '0x') {
1698
+ txFormattedForTrezor.data = formattedTx.data;
1699
+ }
1700
+ if (isLegacy) {
1701
+ txFormattedForTrezor.gasPrice =
1702
+ typeof formattedTx.gasPrice === 'string'
1703
+ ? formattedTx.gasPrice
1704
+ : this.toHex0x(formattedTx.gasPrice, 'gasPrice');
1705
+ }
1706
+ else {
1707
+ txFormattedForTrezor.maxFeePerGas =
1708
+ typeof formattedTx.maxFeePerGas === 'string'
1709
+ ? formattedTx.maxFeePerGas
1710
+ : this.toHex0x(formattedTx.maxFeePerGas, 'maxFeePerGas');
1711
+ txFormattedForTrezor.maxPriorityFeePerGas =
1712
+ typeof formattedTx.maxPriorityFeePerGas === 'string'
1713
+ ? formattedTx.maxPriorityFeePerGas
1714
+ : this.toHex0x(formattedTx.maxPriorityFeePerGas, 'maxPriorityFeePerGas');
1715
+ }
1716
+ assertCurrent();
1717
+ assertTransactionGasLimit(txFormattedForTrezor);
1718
+ const expectedUnsignedTransaction = serializeTransaction(txFormattedForTrezor);
1719
+ const signature = await this.requireTrezorSigner().signEthTransaction({
1720
+ coin: trezorCoin,
1721
+ tx: txFormattedForTrezor,
1722
+ index: activeAccountId.toString(),
1723
+ slip44: activeNetwork.slip44,
1724
+ }, await this.authorizeHardwareTransaction(provider, assertCurrent));
1725
+ if (signature.success) {
1726
+ const signedTx = serializeTransaction(txFormattedForTrezor, trezorSignature(signature.payload));
1727
+ assertSignedSender(signedTx);
1728
+ assertHardwareTransactionSender(signedTx, activeAccount.address);
1729
+ const transactionResponse = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, assertBeforeBroadcast, expectedUnsignedTransaction);
1730
+ return {
1731
+ isSpeedUp: true,
1732
+ transaction: transactionResponse,
1733
+ };
1734
+ }
1735
+ else {
1736
+ return {
1737
+ isSpeedUp: false,
1738
+ error: true,
1739
+ };
1740
+ }
1741
+ }
1742
+ catch (error) {
1743
+ console.error('[SpeedUp] Failed to send replacement transaction with Trezor:', error);
1744
+ const message = error?.message || String(error);
1745
+ const lower = message.toLowerCase();
1746
+ const code = lower.includes('underpriced')
1747
+ ? 'REPLACEMENT_UNDERPRICED'
1748
+ : lower.includes('known transaction') ||
1749
+ lower.includes('already known')
1750
+ ? 'REPLACEMENT_ALREADY_KNOWN'
1751
+ : 'REPLACEMENT_SEND_FAILED';
1752
+ return {
1753
+ ...evmSubmissionFailure(error),
1754
+ isSpeedUp: false,
1755
+ error: true,
1756
+ code,
1757
+ message,
1758
+ };
1759
+ }
1760
+ };
1761
+ // Regular wallet speedup handler
1762
+ const speedUpWithPrivateKey = async () => {
1763
+ try {
1764
+ assertCurrent();
1765
+ const { address, decryptedPrivateKey } = this.getDecryptedPrivateKey(targetAccount);
1766
+ assertCurrent();
1767
+ if (address.toLowerCase() !== expectedSender) {
1768
+ throw new Error('Replacement signing account changed');
1769
+ }
1770
+ const transactionResponse = await sendLocalEvmTransaction(provider, decryptedPrivateKey, txWithEditedFee, assertBeforeBroadcast, assertCurrent, undefined, submission);
1771
+ if (transactionResponse) {
1772
+ return {
1773
+ isSpeedUp: true,
1774
+ transaction: transactionResponse,
1775
+ };
1776
+ }
1777
+ else {
1778
+ return {
1779
+ isSpeedUp: false,
1780
+ };
1781
+ }
1782
+ }
1783
+ catch (error) {
1784
+ console.error('[SpeedUp] Failed to send replacement transaction:', error);
1785
+ const message = error?.message || String(error);
1786
+ const lower = message.toLowerCase();
1787
+ const code = lower.includes('underpriced')
1788
+ ? 'REPLACEMENT_UNDERPRICED'
1789
+ : lower.includes('known transaction') ||
1790
+ lower.includes('already known')
1791
+ ? 'REPLACEMENT_ALREADY_KNOWN'
1792
+ : lower.includes('insufficient funds')
1793
+ ? 'INSUFFICIENT_FUNDS_REPLACEMENT'
1794
+ : 'REPLACEMENT_SEND_FAILED';
1795
+ return {
1796
+ ...evmSubmissionFailure(error),
1797
+ isSpeedUp: false,
1798
+ error: true,
1799
+ code,
1800
+ message,
1801
+ };
1802
+ }
1803
+ };
1804
+ // Route based on account type
1805
+ switch (activeAccountType) {
1806
+ case KeyringAccountType.Trezor:
1807
+ return await speedUpWithTrezor();
1808
+ case KeyringAccountType.Ledger:
1809
+ return await speedUpWithLedger();
1810
+ default:
1811
+ return await speedUpWithPrivateKey();
1812
+ }
1813
+ };
1814
+ this.sendSignedErc20Transaction = async (request, approval) => {
1815
+ const { assertApproval, signal } = this.captureReviewedTokenApproval(approval, 'ERC20');
1816
+ const { receiver, tokenAddress, tokenAmountUnits, saveTrezorTx, networkUrl, } = request;
1817
+ const { activeAccount, activeAccountType, activeAccountId, activeNetwork, provider, assertCurrent: assertSigningCurrent, } = this.captureEvmSigningContext();
1818
+ const activeAccountAddress = activeAccount.address;
1819
+ if (networkUrl !== activeNetwork.url)
1820
+ throw new Error('ERC20 transaction approval network changed');
1821
+ if ('tokenAmount' in request ||
1822
+ 'decimals' in request ||
1823
+ 'tokenId' in request ||
1824
+ typeof tokenAmountUnits !== 'string' ||
1825
+ !/^(0|[1-9][0-9]{0,77})$/.test(tokenAmountUnits))
1826
+ throw new Error('Invalid ERC20 amount');
1827
+ const exactTokenAmount = parseEvmFeeQuantity(tokenAmountUnits, 'ERC20 amount');
1828
+ const { gasLimit, isLegacy, gasPrice, maxFeePerGas, maxPriorityFeePerGas } = this.reviewedTokenFees(request, activeNetwork.chainId, 'ERC20');
1829
+ const submission = this.captureSubmission('erc20', activeAccount, activeAccountType, activeAccountId, activeNetwork, () => {
1830
+ assertApproval();
1831
+ assertSigningCurrent();
1832
+ });
1833
+ const assertCurrent = submission.assertCurrent;
1834
+ const sendERC20Token = async () => {
1835
+ assertCurrent();
1836
+ const { decryptedPrivateKey } = this.getDecryptedPrivateKey();
1837
+ try {
1838
+ const _contract = new Contract(tokenAddress, getErc20Abi(), provider);
1839
+ let transferMethod;
1840
+ if (isLegacy) {
1841
+ const overrides = {
1842
+ nonce: await provider.getTransactionCount(activeAccountAddress, 'pending'),
1843
+ gasPrice,
1844
+ gasLimit,
1845
+ type: 0, // Explicitly set Type 0 for legacy token transfers
1846
+ };
1847
+ assertCurrent();
1848
+ transferMethod = await _contract.transfer.populateTransaction(receiver, exactTokenAmount, normalizeEthersOverrides(overrides));
1849
+ }
1850
+ else {
1851
+ const overrides = {
1852
+ nonce: await provider.getTransactionCount(activeAccountAddress, 'pending'),
1853
+ maxPriorityFeePerGas,
1854
+ maxFeePerGas,
1855
+ gasLimit,
1856
+ };
1857
+ assertCurrent();
1858
+ transferMethod = await _contract.transfer.populateTransaction(receiver, exactTokenAmount, normalizeEthersOverrides(overrides));
1859
+ }
1860
+ return await sendLocalEvmTransaction(provider, decryptedPrivateKey, {
1861
+ ...transferMethod,
1862
+ chainId: activeNetwork.chainId,
1863
+ from: activeAccountAddress,
1864
+ value: '0x0',
1865
+ }, assertCurrent, assertCurrent, undefined, submission);
1866
+ }
1867
+ catch (error) {
1868
+ throw error;
1869
+ }
1870
+ };
1871
+ const sendERC20TokenOnLedger = async () => {
1872
+ assertCurrent();
1873
+ const transactionNonce = await provider.getTransactionCount(activeAccountAddress, 'pending');
1874
+ assertCurrent();
1875
+ try {
1876
+ const _contract = new Contract(tokenAddress, getErc20Abi(), provider);
1877
+ const txData = _contract.interface.encodeFunctionData('transfer', [
1878
+ receiver,
1879
+ exactTokenAmount,
1880
+ ]);
1881
+ let txFormattedForEthers;
1882
+ if (isLegacy) {
1883
+ txFormattedForEthers = {
1884
+ to: tokenAddress,
1885
+ value: '0x0',
1886
+ gasLimit: gasLimit,
1887
+ gasPrice,
1888
+ data: txData,
1889
+ nonce: transactionNonce,
1890
+ chainId: activeNetwork.chainId,
1891
+ type: 0,
1892
+ };
1893
+ }
1894
+ else {
1895
+ txFormattedForEthers = {
1896
+ to: tokenAddress,
1897
+ value: '0x0',
1898
+ gasLimit: gasLimit,
1899
+ maxFeePerGas,
1900
+ maxPriorityFeePerGas,
1901
+ data: txData,
1902
+ nonce: transactionNonce,
1903
+ chainId: activeNetwork.chainId,
1904
+ type: 2, // Need explicit type for hardware wallet serialization
1905
+ };
1906
+ }
1907
+ const rawTx = serializeTransaction(txFormattedForEthers);
1908
+ const signature = await this.requireLedgerSigner().evm.signEVMTransaction({
1909
+ rawTx: rawTx.replace('0x', ''),
1910
+ accountIndex: activeAccountId,
1911
+ expectedAddress: activeAccount.address,
1912
+ }, await this.authorizeHardwareTransaction(provider, assertCurrent, signal));
1913
+ assertCurrent();
1914
+ const formattedSignature = {
1915
+ r: `0x${signature.r}`,
1916
+ s: `0x${signature.s}`,
1917
+ v: parseInt(signature.v, 16),
1918
+ };
1919
+ if (signature) {
1920
+ try {
1921
+ const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
1922
+ assertCurrent();
1923
+ assertHardwareTransactionSender(signedTx, activeAccount.address);
1924
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, async (raw) => {
1925
+ const response = await provider.sendTransaction(raw);
1926
+ saveTrezorTx && saveTrezorTx(response);
1927
+ return response;
1928
+ }, submission, undefined, rawTx);
1929
+ return finalTx;
1930
+ }
1931
+ catch (error) {
1932
+ throw error;
1933
+ }
1934
+ }
1935
+ else {
1936
+ throw new Error(`Transaction Signature Failed. Error: ${signature}`);
1937
+ }
1938
+ }
1939
+ catch (error) {
1940
+ throw error;
1941
+ }
1942
+ };
1943
+ const sendERC20TokenOnTrezor = async () => {
1944
+ assertCurrent();
1945
+ const transactionNonce = await provider.getTransactionCount(activeAccountAddress, 'pending');
1946
+ assertCurrent();
1947
+ try {
1948
+ const _contract = new Contract(tokenAddress, getErc20Abi(), provider);
1949
+ const txData = _contract.interface.encodeFunctionData('transfer', [
1950
+ receiver,
1951
+ exactTokenAmount,
1952
+ ]);
1953
+ let txToBeSignedByTrezor;
1954
+ if (isLegacy) {
1955
+ txToBeSignedByTrezor = {
1956
+ to: tokenAddress,
1957
+ value: '0x0',
1958
+ gasLimit: this.toHex0x(gasLimit, 'gasLimit'),
1959
+ gasPrice: this.toHex0x(gasPrice, 'gasPrice'),
1960
+ nonce: this.toBigNumber(transactionNonce)._hex,
1961
+ chainId: activeNetwork.chainId,
1962
+ data: txData,
1963
+ };
1964
+ }
1965
+ else {
1966
+ txToBeSignedByTrezor = {
1967
+ to: tokenAddress,
1968
+ value: '0x0',
1969
+ gasLimit: this.toHex0x(gasLimit, 'gasLimit'),
1970
+ maxFeePerGas: this.toHex0x(maxFeePerGas, 'maxFeePerGas'),
1971
+ maxPriorityFeePerGas: this.toHex0x(maxPriorityFeePerGas, 'maxPriorityFeePerGas'),
1972
+ nonce: this.toBigNumber(transactionNonce)._hex,
1973
+ chainId: activeNetwork.chainId,
1974
+ data: txData,
1975
+ };
1976
+ }
1977
+ assertTransactionGasLimit(txToBeSignedByTrezor);
1978
+ const expectedUnsignedTransaction = serializeTransaction({
1979
+ ...txToBeSignedByTrezor,
1980
+ type: isLegacy ? 0 : 2,
1981
+ });
1982
+ const signature = await this.requireTrezorSigner().signEthTransaction({
1983
+ index: `${activeAccountId}`,
1984
+ tx: txToBeSignedByTrezor,
1985
+ coin: activeNetwork.currency,
1986
+ slip44: activeNetwork.slip44,
1987
+ }, await this.authorizeHardwareTransaction(provider, assertCurrent, signal));
1988
+ assertCurrent();
1989
+ if (signature.success) {
1990
+ try {
1991
+ let txFormattedForEthers;
1992
+ if (isLegacy) {
1993
+ txFormattedForEthers = {
1994
+ to: tokenAddress,
1995
+ value: '0x0',
1996
+ gasLimit: gasLimit,
1997
+ gasPrice,
1998
+ data: txData,
1999
+ nonce: transactionNonce,
2000
+ chainId: activeNetwork.chainId,
2001
+ type: 0,
2002
+ };
2003
+ }
2004
+ else {
2005
+ txFormattedForEthers = {
2006
+ to: tokenAddress,
2007
+ value: '0x0',
2008
+ gasLimit: gasLimit,
2009
+ maxFeePerGas,
2010
+ maxPriorityFeePerGas,
2011
+ data: txData,
2012
+ nonce: transactionNonce,
2013
+ chainId: activeNetwork.chainId,
2014
+ type: 2, // Need explicit type for hardware wallet serialization
2015
+ };
2016
+ }
2017
+ const signedTx = serializeTransaction(txFormattedForEthers, trezorSignature(signature.payload));
2018
+ assertCurrent();
2019
+ assertHardwareTransactionSender(signedTx, activeAccount.address);
2020
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, async (raw) => {
2021
+ const response = await provider.sendTransaction(raw);
2022
+ saveTrezorTx && saveTrezorTx(response);
2023
+ return response;
2024
+ }, submission, undefined, expectedUnsignedTransaction);
2025
+ return finalTx;
2026
+ }
2027
+ catch (error) {
2028
+ throw error;
2029
+ }
2030
+ }
2031
+ else {
2032
+ throw new Error(`Transaction Signature Failed. Error: ${signature}`);
2033
+ }
2034
+ }
2035
+ catch (error) {
2036
+ throw error;
2037
+ }
2038
+ };
2039
+ switch (activeAccountType) {
2040
+ case KeyringAccountType.Trezor:
2041
+ return await sendERC20TokenOnTrezor();
2042
+ case KeyringAccountType.Ledger:
2043
+ return await sendERC20TokenOnLedger();
2044
+ default:
2045
+ return (await sendERC20Token());
2046
+ }
2047
+ };
2048
+ this.sendSignedErc721Transaction = async (request, approval) => {
2049
+ const { assertApproval, signal } = this.captureReviewedTokenApproval(approval, 'ERC721');
2050
+ const { receiver, tokenAddress, tokenId, networkUrl } = request;
2051
+ const { activeAccount, activeAccountType, activeAccountId, activeNetwork, provider, assertCurrent: assertSigningCurrent, } = this.captureEvmSigningContext();
2052
+ if (networkUrl !== activeNetwork.url)
2053
+ throw new Error('ERC721 transaction approval network changed');
2054
+ if ('tokenAmountUnits' in request || 'decimals' in request)
2055
+ throw new Error('ERC20 amount is not allowed for NFT transfers');
2056
+ if (typeof tokenId !== 'string' || !/^(0|[1-9][0-9]{0,77})$/.test(tokenId))
2057
+ throw new Error('Invalid ERC721 token ID');
2058
+ const normalizedTokenId = parseEvmFeeQuantity(tokenId, 'ERC721 token ID');
2059
+ if ('tokenAmount' in request)
2060
+ throw new Error('ERC721 transfer must contain one token');
2061
+ const { gasLimit, isLegacy, gasPrice, maxFeePerGas, maxPriorityFeePerGas } = this.reviewedTokenFees(request, activeNetwork.chainId, 'ERC721');
2062
+ const activeAccountAddress = activeAccount.address;
2063
+ const submission = this.captureSubmission('erc721', activeAccount, activeAccountType, activeAccountId, activeNetwork, () => {
2064
+ assertApproval();
2065
+ assertSigningCurrent();
2066
+ });
2067
+ const assertCurrent = submission.assertCurrent;
2068
+ const sendERC721Token = async () => {
2069
+ assertCurrent();
2070
+ const { decryptedPrivateKey } = this.getDecryptedPrivateKey();
2071
+ let transferMethod;
2072
+ try {
2073
+ const _contract = new Contract(tokenAddress, getErc21Abi(), provider);
2074
+ if (isLegacy) {
2075
+ const overrides = {
2076
+ nonce: await provider.getTransactionCount(activeAccountAddress, 'pending'),
2077
+ gasPrice,
2078
+ gasLimit,
2079
+ type: 0, // Explicitly set Type 0 for legacy NFT transfers
2080
+ };
2081
+ transferMethod = await _contract.transferFrom.populateTransaction(activeAccountAddress, receiver, normalizedTokenId, normalizeEthersOverrides(overrides));
2082
+ }
2083
+ else {
2084
+ const overrides = {
2085
+ nonce: await provider.getTransactionCount(activeAccountAddress, 'pending'),
2086
+ maxPriorityFeePerGas,
2087
+ maxFeePerGas,
2088
+ gasLimit,
2089
+ };
2090
+ transferMethod = await _contract.transferFrom.populateTransaction(activeAccountAddress, receiver, normalizedTokenId, normalizeEthersOverrides(overrides));
2091
+ }
2092
+ return await sendLocalEvmTransaction(provider, decryptedPrivateKey, {
2093
+ ...transferMethod,
2094
+ chainId: activeNetwork.chainId,
2095
+ from: activeAccountAddress,
2096
+ value: '0x0',
2097
+ }, assertCurrent, assertCurrent, undefined, submission);
2098
+ }
2099
+ catch (error) {
2100
+ throw error;
2101
+ }
2102
+ };
2103
+ const sendERC721TokenOnLedger = async () => {
2104
+ assertCurrent();
2105
+ const transactionNonce = await provider.getTransactionCount(activeAccountAddress, 'pending');
2106
+ assertCurrent();
2107
+ try {
2108
+ const _contract = new Contract(tokenAddress, getErc21Abi(), provider);
2109
+ const txData = _contract.interface.encodeFunctionData('transferFrom', [
2110
+ activeAccountAddress,
2111
+ receiver,
2112
+ normalizedTokenId,
2113
+ ]);
2114
+ let txFormattedForEthers;
2115
+ if (isLegacy) {
2116
+ txFormattedForEthers = {
2117
+ to: tokenAddress,
2118
+ value: '0x0',
2119
+ gasLimit,
2120
+ gasPrice,
2121
+ data: txData,
2122
+ nonce: transactionNonce,
2123
+ chainId: activeNetwork.chainId,
2124
+ type: 0,
2125
+ };
2126
+ }
2127
+ else {
2128
+ txFormattedForEthers = {
2129
+ to: tokenAddress,
2130
+ value: '0x0',
2131
+ gasLimit,
2132
+ maxFeePerGas,
2133
+ maxPriorityFeePerGas,
2134
+ data: txData,
2135
+ nonce: transactionNonce,
2136
+ chainId: activeNetwork.chainId,
2137
+ type: 2, // Need explicit type for hardware wallet serialization
2138
+ };
2139
+ }
2140
+ const rawTx = serializeTransaction(txFormattedForEthers);
2141
+ const signature = await this.requireLedgerSigner().evm.signEVMTransaction({
2142
+ rawTx: rawTx.replace('0x', ''),
2143
+ accountIndex: activeAccountId,
2144
+ expectedAddress: activeAccount.address,
2145
+ }, await this.authorizeHardwareTransaction(provider, assertCurrent, signal));
2146
+ assertCurrent();
2147
+ const formattedSignature = {
2148
+ r: `0x${signature.r}`,
2149
+ s: `0x${signature.s}`,
2150
+ v: parseInt(signature.v, 16),
2151
+ };
2152
+ if (signature) {
2153
+ try {
2154
+ const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
2155
+ assertCurrent();
2156
+ assertHardwareTransactionSender(signedTx, activeAccount.address);
2157
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, rawTx);
2158
+ return finalTx;
2159
+ }
2160
+ catch (error) {
2161
+ throw error;
2162
+ }
2163
+ }
2164
+ else {
2165
+ throw new Error(`Transaction Signature Failed. Error: ${signature}`);
2166
+ }
2167
+ }
2168
+ catch (error) {
2169
+ throw error;
2170
+ }
2171
+ };
2172
+ const sendERC721TokenOnTrezor = async () => {
2173
+ assertCurrent();
2174
+ const transactionNonce = await provider.getTransactionCount(activeAccountAddress, 'pending');
2175
+ assertCurrent();
2176
+ try {
2177
+ const _contract = new Contract(tokenAddress, getErc21Abi(), provider);
2178
+ const txData = _contract.interface.encodeFunctionData('transferFrom', [
2179
+ activeAccountAddress,
2180
+ receiver,
2181
+ normalizedTokenId,
2182
+ ]);
2183
+ let txToBeSignedByTrezor;
2184
+ if (isLegacy) {
2185
+ txToBeSignedByTrezor = {
2186
+ to: tokenAddress,
2187
+ value: '0x0',
2188
+ gasLimit: this.toHex0x(gasLimit, 'gasLimit'),
2189
+ gasPrice: this.toHex0x(gasPrice, 'gasPrice'),
2190
+ nonce: this.toBigNumber(transactionNonce)._hex,
2191
+ chainId: activeNetwork.chainId,
2192
+ data: txData,
2193
+ };
2194
+ console.log({ txToBeSignedByTrezor });
2195
+ }
2196
+ else {
2197
+ txToBeSignedByTrezor = {
2198
+ to: tokenAddress,
2199
+ value: '0x0',
2200
+ gasLimit: this.toHex0x(gasLimit, 'gasLimit'),
2201
+ maxFeePerGas: this.toHex0x(maxFeePerGas, 'maxFeePerGas'),
2202
+ maxPriorityFeePerGas: this.toHex0x(maxPriorityFeePerGas, 'maxPriorityFeePerGas'),
2203
+ nonce: this.toBigNumber(transactionNonce)._hex,
2204
+ chainId: activeNetwork.chainId,
2205
+ data: txData,
2206
+ };
2207
+ }
2208
+ // For EVM networks, Trezor expects 'eth' regardless of the network's currency
2209
+ const trezorCoin = activeNetwork.slip44 === 60 ? 'eth' : activeNetwork.currency;
2210
+ assertTransactionGasLimit(txToBeSignedByTrezor);
2211
+ const expectedUnsignedTransaction = serializeTransaction({
2212
+ ...txToBeSignedByTrezor,
2213
+ type: isLegacy ? 0 : 2,
2214
+ });
2215
+ const signature = await this.requireTrezorSigner().signEthTransaction({
2216
+ index: `${activeAccountId}`,
2217
+ tx: txToBeSignedByTrezor,
2218
+ coin: trezorCoin,
2219
+ slip44: activeNetwork.slip44,
2220
+ }, await this.authorizeHardwareTransaction(provider, assertCurrent, signal));
2221
+ assertCurrent();
2222
+ if (signature.success) {
2223
+ try {
2224
+ let txFormattedForEthers;
2225
+ if (isLegacy) {
2226
+ txFormattedForEthers = {
2227
+ to: tokenAddress,
2228
+ value: '0x0',
2229
+ gasLimit,
2230
+ gasPrice,
2231
+ data: txData,
2232
+ nonce: transactionNonce,
2233
+ chainId: activeNetwork.chainId,
2234
+ type: 0,
2235
+ };
2236
+ }
2237
+ else {
2238
+ txFormattedForEthers = {
2239
+ to: tokenAddress,
2240
+ value: '0x0',
2241
+ gasLimit,
2242
+ maxFeePerGas,
2243
+ maxPriorityFeePerGas,
2244
+ data: txData,
2245
+ nonce: transactionNonce,
2246
+ chainId: activeNetwork.chainId,
2247
+ type: 2, // Need explicit type for hardware wallet serialization
2248
+ };
2249
+ }
2250
+ const signedTx = serializeTransaction(txFormattedForEthers, trezorSignature(signature.payload));
2251
+ assertCurrent();
2252
+ assertHardwareTransactionSender(signedTx, activeAccount.address);
2253
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, expectedUnsignedTransaction);
2254
+ return finalTx;
2255
+ }
2256
+ catch (error) {
2257
+ console.log({ error });
2258
+ throw error;
2259
+ }
2260
+ }
2261
+ else {
2262
+ throw new Error(`Transaction Signature Failed. Error: ${signature}`);
2263
+ }
2264
+ }
2265
+ catch (error) {
2266
+ console.log({ errorDois: error });
2267
+ throw error;
2268
+ }
2269
+ };
2270
+ switch (activeAccountType) {
2271
+ case KeyringAccountType.Trezor:
2272
+ return await sendERC721TokenOnTrezor();
2273
+ case KeyringAccountType.Ledger:
2274
+ return await sendERC721TokenOnLedger();
2275
+ default:
2276
+ return (await sendERC721Token());
2277
+ }
2278
+ };
2279
+ this.sendSignedErc1155Transaction = async (request, approval) => {
2280
+ const { assertApproval, signal } = this.captureReviewedTokenApproval(approval, 'ERC1155');
2281
+ const { receiver, tokenAddress, tokenId, networkUrl } = request;
2282
+ const { activeAccount, activeAccountType, activeAccountId, activeNetwork, provider, assertCurrent: assertSigningCurrent, } = this.captureEvmSigningContext();
2283
+ if (networkUrl !== activeNetwork.url)
2284
+ throw new Error('ERC1155 transaction approval network changed');
2285
+ if ('tokenAmountUnits' in request || 'decimals' in request)
2286
+ throw new Error('ERC20 amount is not allowed for NFT transfers');
2287
+ if (typeof tokenId !== 'string' || !/^(0|[1-9][0-9]{0,77})$/.test(tokenId))
2288
+ throw new Error('Invalid ERC1155 token ID');
2289
+ const normalizedTokenId = parseEvmFeeQuantity(tokenId, 'ERC1155 token ID');
2290
+ const { tokenAmount } = request;
2291
+ if (typeof tokenAmount !== 'string' ||
2292
+ !/^[1-9][0-9]{0,77}$/.test(tokenAmount))
2293
+ throw new Error('Invalid ERC1155 amount');
2294
+ const amount = parseEvmFeeQuantity(tokenAmount, 'ERC1155 amount');
2295
+ const { gasLimit, isLegacy, gasPrice, maxFeePerGas, maxPriorityFeePerGas } = this.reviewedTokenFees(request, activeNetwork.chainId, 'ERC1155');
2296
+ const activeAccountAddress = activeAccount.address;
2297
+ const submission = this.captureSubmission('erc1155', activeAccount, activeAccountType, activeAccountId, activeNetwork, () => {
2298
+ assertApproval();
2299
+ assertSigningCurrent();
2300
+ });
2301
+ const assertCurrent = submission.assertCurrent;
2302
+ const sendERC1155Token = async () => {
2303
+ assertCurrent();
2304
+ const { decryptedPrivateKey } = this.getDecryptedPrivateKey();
2305
+ let transferMethod;
2306
+ try {
2307
+ const _contract = new Contract(tokenAddress, getErc55Abi(), provider);
2308
+ // Use BigNumber to avoid JS number overflow/precision loss
2309
+ let overrides;
2310
+ if (isLegacy) {
2311
+ overrides = {
2312
+ nonce: await provider.getTransactionCount(activeAccountAddress, 'pending'),
2313
+ gasPrice,
2314
+ gasLimit,
2315
+ type: 0, // Explicitly set Type 0 for legacy ERC1155 transfers
2316
+ };
2317
+ }
2318
+ else {
2319
+ overrides = {
2320
+ nonce: await provider.getTransactionCount(activeAccountAddress, 'pending'),
2321
+ maxPriorityFeePerGas,
2322
+ maxFeePerGas,
2323
+ gasLimit,
2324
+ };
2325
+ }
2326
+ transferMethod = await _contract.safeTransferFrom.populateTransaction(activeAccountAddress, receiver, normalizedTokenId, amount, '0x', normalizeEthersOverrides(overrides));
2327
+ return await sendLocalEvmTransaction(provider, decryptedPrivateKey, {
2328
+ ...transferMethod,
2329
+ chainId: activeNetwork.chainId,
2330
+ from: activeAccountAddress,
2331
+ value: '0x0',
2332
+ }, assertCurrent, assertCurrent, undefined, submission);
2333
+ }
2334
+ catch (error) {
2335
+ throw error;
2336
+ }
2337
+ };
2338
+ const sendERC1155TokenOnLedger = async () => {
2339
+ assertCurrent();
2340
+ const transactionNonce = await provider.getTransactionCount(activeAccountAddress, 'pending');
2341
+ assertCurrent();
2342
+ try {
2343
+ const _contract = new Contract(tokenAddress, getErc55Abi(), provider);
2344
+ const txData = _contract.interface.encodeFunctionData('safeTransferFrom', [activeAccountAddress, receiver, normalizedTokenId, amount, '0x']);
2345
+ let txFormattedForEthers;
2346
+ if (isLegacy) {
2347
+ txFormattedForEthers = {
2348
+ to: tokenAddress,
2349
+ value: '0x0',
2350
+ gasLimit,
2351
+ gasPrice,
2352
+ data: txData,
2353
+ nonce: transactionNonce,
2354
+ chainId: activeNetwork.chainId,
2355
+ type: 0,
2356
+ };
2357
+ }
2358
+ else {
2359
+ txFormattedForEthers = {
2360
+ to: tokenAddress,
2361
+ value: '0x0',
2362
+ gasLimit,
2363
+ maxFeePerGas,
2364
+ maxPriorityFeePerGas,
2365
+ data: txData,
2366
+ nonce: transactionNonce,
2367
+ chainId: activeNetwork.chainId,
2368
+ type: 2, // Need explicit type for hardware wallet serialization
2369
+ };
2370
+ }
2371
+ const rawTx = serializeTransaction(txFormattedForEthers);
2372
+ const signature = await this.requireLedgerSigner().evm.signEVMTransaction({
2373
+ rawTx: rawTx.replace('0x', ''),
2374
+ accountIndex: activeAccountId,
2375
+ expectedAddress: activeAccount.address,
2376
+ }, await this.authorizeHardwareTransaction(provider, assertCurrent, signal));
2377
+ assertCurrent();
2378
+ const formattedSignature = {
2379
+ r: `0x${signature.r}`,
2380
+ s: `0x${signature.s}`,
2381
+ v: parseInt(signature.v, 16),
2382
+ };
2383
+ if (signature) {
2384
+ try {
2385
+ const signedTx = serializeTransaction(txFormattedForEthers, formattedSignature);
2386
+ assertCurrent();
2387
+ assertHardwareTransactionSender(signedTx, activeAccount.address);
2388
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, rawTx);
2389
+ return finalTx;
2390
+ }
2391
+ catch (error) {
2392
+ throw error;
2393
+ }
2394
+ }
2395
+ else {
2396
+ throw new Error(`Transaction Signature Failed. Error: ${signature}`);
2397
+ }
2398
+ }
2399
+ catch (error) {
2400
+ throw error;
2401
+ }
2402
+ };
2403
+ const sendERC1155TokenOnTrezor = async () => {
2404
+ assertCurrent();
2405
+ const transactionNonce = await provider.getTransactionCount(activeAccountAddress, 'pending');
2406
+ assertCurrent();
2407
+ try {
2408
+ const _contract = new Contract(tokenAddress, getErc55Abi(), provider);
2409
+ const txData = _contract.interface.encodeFunctionData('safeTransferFrom', [activeAccountAddress, receiver, normalizedTokenId, amount, '0x']);
2410
+ let txToBeSignedByTrezor;
2411
+ if (isLegacy) {
2412
+ txToBeSignedByTrezor = {
2413
+ to: tokenAddress,
2414
+ value: '0x0',
2415
+ gasLimit: this.toHex0x(gasLimit, 'gasLimit'),
2416
+ gasPrice: this.toHex0x(gasPrice, 'gasPrice'),
2417
+ nonce: this.toBigNumber(transactionNonce)._hex,
2418
+ chainId: activeNetwork.chainId,
2419
+ data: txData,
2420
+ };
2421
+ }
2422
+ else {
2423
+ txToBeSignedByTrezor = {
2424
+ to: tokenAddress,
2425
+ value: '0x0',
2426
+ gasLimit: this.toHex0x(gasLimit, 'gasLimit'),
2427
+ maxFeePerGas: this.toHex0x(maxFeePerGas, 'maxFeePerGas'),
2428
+ maxPriorityFeePerGas: this.toHex0x(maxPriorityFeePerGas, 'maxPriorityFeePerGas'),
2429
+ nonce: this.toBigNumber(transactionNonce)._hex,
2430
+ chainId: activeNetwork.chainId,
2431
+ data: txData,
2432
+ };
2433
+ }
2434
+ assertTransactionGasLimit(txToBeSignedByTrezor);
2435
+ const expectedUnsignedTransaction = serializeTransaction({
2436
+ ...txToBeSignedByTrezor,
2437
+ type: isLegacy ? 0 : 2,
2438
+ });
2439
+ const signature = await this.requireTrezorSigner().signEthTransaction({
2440
+ index: `${activeAccountId}`,
2441
+ tx: txToBeSignedByTrezor,
2442
+ coin: activeNetwork.currency,
2443
+ slip44: activeNetwork.slip44,
2444
+ }, await this.authorizeHardwareTransaction(provider, assertCurrent, signal));
2445
+ assertCurrent();
2446
+ if (signature.success) {
2447
+ try {
2448
+ let txFormattedForEthers;
2449
+ if (isLegacy) {
2450
+ txFormattedForEthers = {
2451
+ to: tokenAddress,
2452
+ value: '0x0',
2453
+ gasLimit,
2454
+ gasPrice,
2455
+ data: txData,
2456
+ nonce: transactionNonce,
2457
+ chainId: activeNetwork.chainId,
2458
+ type: 0,
2459
+ };
2460
+ }
2461
+ else {
2462
+ txFormattedForEthers = {
2463
+ to: tokenAddress,
2464
+ value: '0x0',
2465
+ gasLimit,
2466
+ maxFeePerGas,
2467
+ maxPriorityFeePerGas,
2468
+ data: txData,
2469
+ nonce: transactionNonce,
2470
+ chainId: activeNetwork.chainId,
2471
+ type: 2, // Need explicit type for hardware wallet serialization
2472
+ };
2473
+ }
2474
+ const signedTx = serializeTransaction(txFormattedForEthers, trezorSignature(signature.payload));
2475
+ assertCurrent();
2476
+ assertHardwareTransactionSender(signedTx, activeAccount.address);
2477
+ const finalTx = await this.submitSignedHardwareTransaction(provider, signedTx, (raw) => provider.sendTransaction(raw), submission, undefined, expectedUnsignedTransaction);
2478
+ return finalTx;
2479
+ }
2480
+ catch (error) {
2481
+ console.log({ error });
2482
+ throw error;
2483
+ }
2484
+ }
2485
+ else {
2486
+ throw new Error(`Transaction Signature Failed. Error: ${signature}`);
2487
+ }
2488
+ }
2489
+ catch (error) {
2490
+ console.log({ error });
2491
+ throw error;
2492
+ }
2493
+ };
2494
+ switch (activeAccountType) {
2495
+ case KeyringAccountType.Trezor:
2496
+ return await sendERC1155TokenOnTrezor();
2497
+ case KeyringAccountType.Ledger:
2498
+ return await sendERC1155TokenOnLedger();
2499
+ default:
2500
+ return (await sendERC1155Token());
2501
+ }
2502
+ };
2503
+ this.getRecommendedNonce = async (address) => {
2504
+ try {
2505
+ return await this.web3Provider.getTransactionCount(address, 'pending');
2506
+ }
2507
+ catch (error) {
2508
+ throw error;
2509
+ }
2510
+ };
2511
+ this.getFeeByType = async (type) => {
2512
+ const gasPrice = (await this.getRecommendedGasPrice(false));
2513
+ const low = this.toBigNumber(gasPrice)
2514
+ .mul(BigNumber.from('8'))
2515
+ .div(BigNumber.from('10'))
2516
+ .toString();
2517
+ const high = this.toBigNumber(gasPrice)
2518
+ .mul(BigNumber.from('11'))
2519
+ .div(BigNumber.from('10'))
2520
+ .toString();
2521
+ if (type === 'low')
2522
+ return low;
2523
+ if (type === 'high')
2524
+ return high;
2525
+ return gasPrice;
2526
+ };
2527
+ this.getGasLimit = async (toAddress) => {
2528
+ try {
2529
+ const estimated = await this.web3Provider.estimateGas({
2530
+ to: toAddress,
2531
+ });
2532
+ return Number(BigNumber.from(estimated).toString());
2533
+ }
2534
+ catch (error) {
2535
+ throw error;
2536
+ }
2537
+ };
2538
+ this.getTxGasLimit = async (tx) => {
2539
+ const request = { ...tx };
2540
+ const network = this.getNetwork();
2541
+ const identity = {
2542
+ kind: network.kind,
2543
+ chainId: network.chainId,
2544
+ slip44: network.slip44,
2545
+ url: network.url,
2546
+ };
2547
+ const chainId = parseEvmFeeQuantity(identity.chainId, 'chain ID');
2548
+ const provider = this.web3Provider;
2549
+ const controller = this.abortController;
2550
+ const minimum = this.gasOverrides.minGasLimit;
2551
+ const assertCurrent = () => {
2552
+ const current = this.getNetwork();
2553
+ if (controller.signal.aborted ||
2554
+ controller !== this.abortController ||
2555
+ current.kind !== identity.kind ||
2556
+ current.chainId !== identity.chainId ||
2557
+ current.slip44 !== identity.slip44 ||
2558
+ current.url !== identity.url ||
2559
+ this.web3Provider !== provider) {
2560
+ const error = new Error('EVM gas estimate context changed');
2561
+ error.name = 'AbortError';
2562
+ throw error;
2563
+ }
2564
+ };
2565
+ const checked = (limit) => {
2566
+ assertCurrent();
2567
+ return BigNumber.from(assertEvmGasLimit(typeof limit === 'bigint' ? limit : limit.toBigInt(), chainId));
2568
+ };
2569
+ const hasCalldata = Boolean(request.data && request.data !== '0x' && request.data !== '0x0');
2570
+ try {
2571
+ // First attempt: standard estimation
2572
+ assertCurrent();
2573
+ const estimated = await provider.estimateGas(request);
2574
+ return checked(bufferedEvmGasLimit(estimated.toBigInt(), minimum?.toBigInt()));
2575
+ }
2576
+ catch (error) {
2577
+ assertCurrent();
2578
+ if (error instanceof EvmGasLimitError ||
2579
+ error?.code === 'SYSWEB3_RPC_RESPONSE_INVALID' ||
2580
+ error?.name === 'AbortError' ||
2581
+ this.abortController.signal.aborted)
2582
+ throw error;
2583
+ console.warn('Gas estimation failed:', error);
2584
+ if (hasCalldata) {
2585
+ const calldataFallback = BigNumber.from('100000');
2586
+ if (minimum && calldataFallback.lt(minimum)) {
2587
+ return checked(minimum);
2588
+ }
2589
+ return checked(calldataFallback);
2590
+ }
2591
+ // Try a simpler estimation for basic transfers
2592
+ try {
2593
+ const simpleEstimate = await provider.estimateGas({
2594
+ to: request.to,
2595
+ from: request.from,
2596
+ value: request.value || '0x0',
2597
+ });
2598
+ const withBuffer = simpleEstimate.mul(150).div(100); // 50% buffer for failed estimations
2599
+ if (minimum && withBuffer.lt(minimum)) {
2600
+ return checked(minimum);
2601
+ }
2602
+ return checked(withBuffer);
2603
+ }
2604
+ catch (secondError) {
2605
+ assertCurrent();
2606
+ if (secondError instanceof EvmGasLimitError ||
2607
+ secondError?.code === 'SYSWEB3_RPC_RESPONSE_INVALID' ||
2608
+ secondError?.name === 'AbortError' ||
2609
+ this.abortController.signal.aborted) {
2610
+ throw secondError;
2611
+ }
2612
+ // Ultimate fallback
2613
+ console.warn('Simple estimation also failed, using default', secondError);
2614
+ return checked(minimum || BigNumber.from('100000'));
2615
+ }
2616
+ }
2617
+ };
2618
+ this.getRecommendedGasPrice = async (formatted) => {
2619
+ try {
2620
+ const gasPriceBN = await this.web3Provider.getGasPrice();
2621
+ if (formatted) {
2622
+ return {
2623
+ gwei: Number(formatUnits(gasPriceBN, 'gwei')).toFixed(2),
2624
+ ethers: formatEther(gasPriceBN),
2625
+ };
2626
+ }
2627
+ return gasPriceBN.toString();
2628
+ }
2629
+ catch (error) {
2630
+ throw error;
2631
+ }
2632
+ };
2633
+ this.getBalance = async (address) => {
2634
+ try {
2635
+ const balance = await this.web3Provider.getBalance(address);
2636
+ const formattedBalance = formatEther(balance);
2637
+ return parseFloat(formattedBalance); // Return full precision
2638
+ }
2639
+ catch (error) {
2640
+ throw error;
2641
+ }
2642
+ };
2643
+ this.importAccount = (mnemonicOrPrivKey) => {
2644
+ if (isHexString(mnemonicOrPrivKey)) {
2645
+ return privateKeyToAccount(mnemonicOrPrivKey);
2646
+ }
2647
+ return deriveEvmAccountFromMnemonic(mnemonicOrPrivKey, getAddressDerivationPath('eth', 60, 0, false, 0));
2648
+ };
2649
+ // Method to get safe fee data that works around network-specific validation issues
2650
+ this.getSafeFeeData = async (forceLegacy = false) => {
2651
+ if (forceLegacy) {
2652
+ // For legacy transactions, just return gas price
2653
+ const gasPrice = await this.web3Provider.getGasPrice();
2654
+ const bufferedGasPrice = gasPrice.mul(110).div(100); // 10% buffer
2655
+ return {
2656
+ gasPrice: bufferedGasPrice,
2657
+ maxFeePerGas: undefined,
2658
+ maxPriorityFeePerGas: undefined,
2659
+ };
2660
+ }
2661
+ const feeData = await this.getFeeDataWithDynamicMaxPriorityFeePerGas();
2662
+ if (feeData.baseFeePerGas === undefined) {
2663
+ // Only an actual pre-London header authorizes legacy selection.
2664
+ const bufferedGasPrice = feeData.gasPrice.mul(110).div(100);
2665
+ return {
2666
+ gasPrice: bufferedGasPrice,
2667
+ maxFeePerGas: undefined,
2668
+ maxPriorityFeePerGas: undefined,
2669
+ };
2670
+ }
2671
+ return {
2672
+ gasPrice: undefined,
2673
+ maxFeePerGas: feeData.maxFeePerGas,
2674
+ maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
2675
+ };
2676
+ };
2677
+ if (typeof captureSigningContext !== 'function')
2678
+ throw new TypeError('Ethereum transactions require a signing context');
2679
+ if (captureSubmissionLifecycle !== null &&
2680
+ typeof captureSubmissionLifecycle !== 'function')
2681
+ throw new TypeError('Ethereum transactions require an explicit submission capability');
2682
+ this.getNetwork = getNetwork;
2683
+ this.getDecryptedPrivateKey = (targetAccount) => {
2684
+ this.assertHardwareSignerAvailable(targetAccount);
2685
+ return getDecryptedPrivateKey(targetAccount);
2686
+ };
2687
+ this.abortController = new AbortController();
2688
+ // NOTE: Defer network access until vault state getter is initialized
2689
+ // The web3Provider will be created lazily when first accessed via getters
2690
+ this.getState = getState;
2691
+ this.trezorSigner = trezorSigner;
2692
+ this.ledgerSigner = ledgerSigner;
2693
+ this.captureSigningContext = captureSigningContext;
2694
+ this.captureSubmissionLifecycle = captureSubmissionLifecycle;
2695
+ }
2696
+ // Getter that automatically ensures providers are initialized when accessed
2697
+ get web3Provider() {
2698
+ this.ensureProvidersInitialized();
2699
+ return this._web3Provider;
2700
+ }
2701
+ // Helper method to ensure providers are initialized when first needed
2702
+ ensureProvidersInitialized() {
2703
+ // Keep provider in sync with current network selection.
2704
+ // Hardware-wallet signing (e.g., Trezor) commits to a chainId; if the provider points at a different
2705
+ // RPC/network than `activeNetwork`, nodes will reject broadcasts with chain-id mismatch errors.
2706
+ try {
2707
+ const currentNetwork = this.getNetwork();
2708
+ if (currentNetwork?.kind !== INetworkType.Ethereum) {
2709
+ if (this._web3Provider)
2710
+ this.setWeb3Provider(currentNetwork);
2711
+ return;
2712
+ }
2713
+ const currentKey = `${currentNetwork.chainId}|${currentNetwork.url ?? ''}`;
2714
+ const needsInit = !this._web3Provider || this._web3ProviderKey !== currentKey;
2715
+ if (needsInit) {
2716
+ this.setWeb3Provider(currentNetwork);
2717
+ }
2718
+ }
2719
+ catch (error) {
2720
+ // If vault state not available yet, providers will be initialized later
2721
+ // when setWeb3Provider is called explicitly
2722
+ console.log('[EthereumTransactions] Deferring provider initialization:', error?.message || error);
2723
+ }
2724
+ }
2725
+ setWeb3Provider(network) {
2726
+ this.abortController.abort();
2727
+ this.abortController = new AbortController();
2728
+ if (network?.kind !== INetworkType.Ethereum) {
2729
+ this._web3Provider = undefined;
2730
+ this._web3ProviderKey = undefined;
2731
+ return;
2732
+ }
2733
+ this._web3Provider = new CustomJsonRpcProvider(this.abortController.signal, network.url, network.chainId);
2734
+ this._web3ProviderKey = `${network.chainId}|${network.url ?? ''}`;
2735
+ }
2736
+ // Method to configure gas overrides for networks with validation issues
2737
+ setGasOverrides(overrides) {
2738
+ this.gasOverrides = {
2739
+ minGasLimit: overrides.minGasLimit
2740
+ ? BigNumber.from(overrides.minGasLimit)
2741
+ : undefined,
2742
+ minPriorityFee: overrides.minPriorityFee
2743
+ ? BigNumber.from(overrides.minPriorityFee)
2744
+ : undefined,
2745
+ feeMultiplier: overrides.feeMultiplier,
2746
+ };
2747
+ }
2748
+ // Get the current gas overrides
2749
+ getGasOverrides() {
2750
+ return this.gasOverrides;
2751
+ }
2752
+ }
2753
+ //# sourceMappingURL=ethereum.js.map