@sidhujag/sysweb3-keyring 2.1.2 → 2.1.4
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.
- package/CHANGELOG.md +6 -0
- package/README.md +55 -47
- package/cjs/current-vault-refusal.js +6 -0
- package/cjs/current-vault-refusal.js.map +1 -0
- package/cjs/evm-fees.js +29 -1
- package/cjs/evm-fees.js.map +1 -1
- package/cjs/hardware-transaction-authority.js +36 -0
- package/cjs/hardware-transaction-authority.js.map +1 -0
- package/cjs/index.js +5 -5
- package/cjs/index.js.map +1 -1
- package/cjs/keyring-core.js +466 -250
- package/cjs/keyring-core.js.map +1 -1
- package/cjs/keyring-host.js +113 -0
- package/cjs/keyring-host.js.map +1 -0
- package/cjs/keyring-manager.js +15 -55
- package/cjs/keyring-manager.js.map +1 -1
- package/cjs/ledger/host-types.js.map +1 -1
- package/cjs/ledger/index.js +44 -19
- package/cjs/ledger/index.js.map +1 -1
- package/cjs/providers.js +3 -1
- package/cjs/providers.js.map +1 -1
- package/cjs/software.js +10 -43
- package/cjs/software.js.map +1 -1
- package/cjs/storage-core.js +44 -4
- package/cjs/storage-core.js.map +1 -1
- package/cjs/transactions/ethereum.js +358 -294
- package/cjs/transactions/ethereum.js.map +1 -1
- package/cjs/transactions/evm-cancellation.js +43 -0
- package/cjs/transactions/evm-cancellation.js.map +1 -0
- package/cjs/transactions/evm-local-signer.js +15 -0
- package/cjs/transactions/evm-local-signer.js.map +1 -1
- package/cjs/transactions/evm-submission.js +61 -65
- package/cjs/transactions/evm-submission.js.map +1 -1
- package/cjs/transactions/hardware-psbt.js +11 -8
- package/cjs/transactions/hardware-psbt.js.map +1 -1
- package/cjs/transactions/index.js +10 -2
- package/cjs/transactions/index.js.map +1 -1
- package/cjs/transactions/prepared-evm.js +10 -7
- package/cjs/transactions/prepared-evm.js.map +1 -1
- package/cjs/transactions/syscoin-core.js +282 -202
- package/cjs/transactions/syscoin-core.js.map +1 -1
- package/cjs/trezor/connect.js +19 -2
- package/cjs/trezor/connect.js.map +1 -1
- package/cjs/trezor/index.js +38 -16
- package/cjs/trezor/index.js.map +1 -1
- package/cjs/types.js.map +1 -1
- package/cjs/utils/owned-blockbook-account.js +45 -0
- package/cjs/utils/owned-blockbook-account.js.map +1 -0
- package/esm/current-vault-refusal.js +3 -0
- package/esm/current-vault-refusal.js.map +1 -0
- package/esm/evm-fees.js +242 -0
- package/esm/evm-fees.js.map +1 -0
- package/esm/hardware-transaction-authority.js +33 -0
- package/esm/hardware-transaction-authority.js.map +1 -0
- package/esm/initial-state.js +102 -0
- package/esm/initial-state.js.map +1 -0
- package/esm/keyring-core.js +2781 -0
- package/esm/keyring-core.js.map +1 -0
- package/esm/keyring-host.js +108 -0
- package/esm/keyring-host.js.map +1 -0
- package/esm/ledger/dmk-host.js +31 -0
- package/esm/ledger/dmk-host.js.map +1 -0
- package/esm/ledger/dmk-selected-host.js +6 -0
- package/esm/ledger/dmk-selected-host.js.map +1 -0
- package/esm/ledger/dmk-session.js +154 -34
- package/esm/ledger/dmk-session.js.map +1 -1
- package/esm/ledger/dmk-utxo.js +9 -3
- package/esm/ledger/dmk-utxo.js.map +1 -1
- package/esm/ledger/host-types.js.map +1 -1
- package/esm/ledger/index.js +44 -19
- package/esm/ledger/index.js.map +1 -1
- package/esm/ledger-host.js +3 -27
- package/esm/ledger-host.js.map +1 -1
- package/esm/network-utils.js +70 -0
- package/esm/network-utils.js.map +1 -0
- package/esm/providers.js +3 -1
- package/esm/providers.js.map +1 -1
- package/esm/software-hd-account.js +20 -0
- package/esm/software-hd-account.js.map +1 -0
- package/esm/storage-core.js +258 -0
- package/esm/storage-core.js.map +1 -0
- package/esm/transactions/ethereum.js +2753 -0
- package/esm/transactions/ethereum.js.map +1 -0
- package/esm/transactions/evm-cancellation.js +40 -0
- package/esm/transactions/evm-cancellation.js.map +1 -0
- package/esm/transactions/evm-local-signer.js +202 -0
- package/esm/transactions/evm-local-signer.js.map +1 -0
- package/esm/transactions/evm-submission.js +187 -0
- package/esm/transactions/evm-submission.js.map +1 -0
- package/esm/transactions/prepared-evm.js +194 -0
- package/esm/transactions/prepared-evm.js.map +1 -0
- package/esm/transactions/syscoin-core.js +691 -0
- package/esm/transactions/syscoin-core.js.map +1 -0
- package/esm/trezor/connect.js +19 -2
- package/esm/trezor/connect.js.map +1 -1
- package/esm/trezor/index.js +38 -16
- package/esm/trezor/index.js.map +1 -1
- package/esm/types.js.map +1 -1
- package/esm/utils/excluded-utxos.js +53 -0
- package/esm/utils/excluded-utxos.js.map +1 -0
- package/esm/utils/owned-blockbook-account.js +9 -0
- package/esm/utils/owned-blockbook-account.js.map +1 -0
- package/esm/utils/psbt.js +32 -0
- package/esm/utils/psbt.js.map +1 -0
- package/esm/utils/watch-descriptor.js +138 -0
- package/esm/utils/watch-descriptor.js.map +1 -0
- package/package.json +17 -11
- package/types/current-vault-refusal.d.ts +2 -0
- package/types/evm-fees.d.ts +25 -0
- package/types/hardware-transaction-authority.d.ts +5 -0
- package/types/index.d.ts +3 -3
- package/types/keyring-core.d.ts +48 -44
- package/types/keyring-host.d.ts +46 -0
- package/types/keyring-manager.d.ts +14 -7
- package/types/ledger/dmk-host.d.ts +7 -0
- package/types/ledger/dmk-selected-host.d.ts +7 -0
- package/types/ledger/dmk-session.d.ts +10 -2
- package/types/ledger/host-types.d.ts +4 -1
- package/types/ledger/index.d.ts +1 -1
- package/types/ledger/types.d.ts +3 -2
- package/types/ledger-host.d.ts +6 -2
- package/types/providers.d.ts +1 -1
- package/types/software.d.ts +7 -19
- package/types/storage-core.d.ts +3 -1
- package/types/transactions/ethereum.d.ts +14 -11
- package/types/transactions/evm-cancellation.d.ts +10 -0
- package/types/transactions/evm-local-signer.d.ts +2 -2
- package/types/transactions/evm-submission.d.ts +11 -4
- package/types/transactions/hardware-psbt.d.ts +1 -1
- package/types/transactions/index.d.ts +3 -2
- package/types/transactions/prepared-evm.d.ts +4 -1
- package/types/transactions/syscoin-core.d.ts +48 -21
- package/types/trezor/connect.d.ts +6 -3
- package/types/trezor/index.d.ts +4 -3
- package/types/types.d.ts +95 -63
- package/types/utils/owned-blockbook-account.d.ts +3 -0
- package/cjs/hardware-wallet-manager-singleton.js +0 -34
- package/cjs/hardware-wallet-manager-singleton.js.map +0 -1
- package/cjs/storage.js +0 -20
- package/cjs/storage.js.map +0 -1
- package/cjs/transactions/syscoin.js +0 -16
- package/cjs/transactions/syscoin.js.map +0 -1
- package/cjs/utils/blockbook-cache.js +0 -76
- package/cjs/utils/blockbook-cache.js.map +0 -1
- package/types/hardware-wallet-manager-singleton.d.ts +0 -19
- package/types/storage.d.ts +0 -5
- package/types/transactions/syscoin.d.ts +0 -9
- 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
|