@sidhujag/sysweb3-keyring 2.1.3 → 2.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +47 -47
  3. package/cjs/hardware-transaction-authority.js +36 -0
  4. package/cjs/hardware-transaction-authority.js.map +1 -0
  5. package/cjs/index.js +5 -5
  6. package/cjs/index.js.map +1 -1
  7. package/cjs/keyring-core.js +171 -220
  8. package/cjs/keyring-core.js.map +1 -1
  9. package/cjs/keyring-host.js +113 -0
  10. package/cjs/keyring-host.js.map +1 -0
  11. package/cjs/keyring-manager.js +15 -151
  12. package/cjs/keyring-manager.js.map +1 -1
  13. package/cjs/ledger/host-types.js.map +1 -1
  14. package/cjs/ledger/index.js +44 -19
  15. package/cjs/ledger/index.js.map +1 -1
  16. package/cjs/providers.js +3 -1
  17. package/cjs/providers.js.map +1 -1
  18. package/cjs/software.js +10 -56
  19. package/cjs/software.js.map +1 -1
  20. package/cjs/transactions/ethereum.js +306 -229
  21. package/cjs/transactions/ethereum.js.map +1 -1
  22. package/cjs/transactions/evm-local-signer.js +15 -0
  23. package/cjs/transactions/evm-local-signer.js.map +1 -1
  24. package/cjs/transactions/evm-submission.js +61 -65
  25. package/cjs/transactions/evm-submission.js.map +1 -1
  26. package/cjs/transactions/hardware-psbt.js +11 -8
  27. package/cjs/transactions/hardware-psbt.js.map +1 -1
  28. package/cjs/transactions/index.js +10 -2
  29. package/cjs/transactions/index.js.map +1 -1
  30. package/cjs/transactions/prepared-evm.js +10 -7
  31. package/cjs/transactions/prepared-evm.js.map +1 -1
  32. package/cjs/transactions/syscoin-core.js +308 -205
  33. package/cjs/transactions/syscoin-core.js.map +1 -1
  34. package/cjs/trezor/connect.js +19 -2
  35. package/cjs/trezor/connect.js.map +1 -1
  36. package/cjs/trezor/index.js +38 -16
  37. package/cjs/trezor/index.js.map +1 -1
  38. package/cjs/types.js.map +1 -1
  39. package/cjs/utils/excluded-utxos.js +6 -2
  40. package/cjs/utils/excluded-utxos.js.map +1 -1
  41. package/cjs/utils/owned-blockbook-account.js +45 -0
  42. package/cjs/utils/owned-blockbook-account.js.map +1 -0
  43. package/esm/current-vault-refusal.js +3 -0
  44. package/esm/current-vault-refusal.js.map +1 -0
  45. package/esm/evm-fees.js +242 -0
  46. package/esm/evm-fees.js.map +1 -0
  47. package/esm/hardware-transaction-authority.js +33 -0
  48. package/esm/hardware-transaction-authority.js.map +1 -0
  49. package/esm/initial-state.js +102 -0
  50. package/esm/initial-state.js.map +1 -0
  51. package/esm/keyring-core.js +2781 -0
  52. package/esm/keyring-core.js.map +1 -0
  53. package/esm/keyring-host.js +108 -0
  54. package/esm/keyring-host.js.map +1 -0
  55. package/esm/ledger/dmk-session.js +1 -0
  56. package/esm/ledger/dmk-session.js.map +1 -1
  57. package/esm/ledger/dmk-utxo.js +9 -3
  58. package/esm/ledger/dmk-utxo.js.map +1 -1
  59. package/esm/ledger/host-types.js.map +1 -1
  60. package/esm/ledger/index.js +44 -19
  61. package/esm/ledger/index.js.map +1 -1
  62. package/esm/network-utils.js +70 -0
  63. package/esm/network-utils.js.map +1 -0
  64. package/esm/providers.js +3 -1
  65. package/esm/providers.js.map +1 -1
  66. package/esm/software-hd-account.js +20 -0
  67. package/esm/software-hd-account.js.map +1 -0
  68. package/esm/storage-core.js +258 -0
  69. package/esm/storage-core.js.map +1 -0
  70. package/esm/transactions/ethereum.js +2753 -0
  71. package/esm/transactions/ethereum.js.map +1 -0
  72. package/esm/transactions/evm-cancellation.js +40 -0
  73. package/esm/transactions/evm-cancellation.js.map +1 -0
  74. package/esm/transactions/evm-local-signer.js +202 -0
  75. package/esm/transactions/evm-local-signer.js.map +1 -0
  76. package/esm/transactions/evm-submission.js +187 -0
  77. package/esm/transactions/evm-submission.js.map +1 -0
  78. package/esm/transactions/prepared-evm.js +194 -0
  79. package/esm/transactions/prepared-evm.js.map +1 -0
  80. package/esm/transactions/syscoin-core.js +714 -0
  81. package/esm/transactions/syscoin-core.js.map +1 -0
  82. package/esm/trezor/connect.js +19 -2
  83. package/esm/trezor/connect.js.map +1 -1
  84. package/esm/trezor/index.js +38 -16
  85. package/esm/trezor/index.js.map +1 -1
  86. package/esm/types.js.map +1 -1
  87. package/esm/utils/excluded-utxos.js +57 -0
  88. package/esm/utils/excluded-utxos.js.map +1 -0
  89. package/esm/utils/owned-blockbook-account.js +9 -0
  90. package/esm/utils/owned-blockbook-account.js.map +1 -0
  91. package/esm/utils/psbt.js +32 -0
  92. package/esm/utils/psbt.js.map +1 -0
  93. package/esm/utils/watch-descriptor.js +138 -0
  94. package/esm/utils/watch-descriptor.js.map +1 -0
  95. package/package.json +5 -11
  96. package/types/hardware-transaction-authority.d.ts +5 -0
  97. package/types/index.d.ts +3 -3
  98. package/types/keyring-core.d.ts +22 -51
  99. package/types/keyring-host.d.ts +46 -0
  100. package/types/keyring-manager.d.ts +13 -18
  101. package/types/ledger/host-types.d.ts +4 -1
  102. package/types/ledger/index.d.ts +1 -1
  103. package/types/ledger/types.d.ts +3 -2
  104. package/types/providers.d.ts +1 -1
  105. package/types/software.d.ts +6 -19
  106. package/types/transactions/ethereum.d.ts +14 -11
  107. package/types/transactions/evm-local-signer.d.ts +2 -2
  108. package/types/transactions/evm-submission.d.ts +11 -4
  109. package/types/transactions/hardware-psbt.d.ts +1 -1
  110. package/types/transactions/index.d.ts +3 -2
  111. package/types/transactions/prepared-evm.d.ts +4 -1
  112. package/types/transactions/syscoin-core.d.ts +48 -21
  113. package/types/trezor/connect.d.ts +6 -3
  114. package/types/trezor/index.d.ts +4 -3
  115. package/types/types.d.ts +95 -63
  116. package/types/utils/excluded-utxos.d.ts +1 -1
  117. package/types/utils/owned-blockbook-account.d.ts +3 -0
  118. package/cjs/hardware-wallet-manager-singleton.js +0 -34
  119. package/cjs/hardware-wallet-manager-singleton.js.map +0 -1
  120. package/cjs/storage.js +0 -20
  121. package/cjs/storage.js.map +0 -1
  122. package/cjs/transactions/syscoin.js +0 -16
  123. package/cjs/transactions/syscoin.js.map +0 -1
  124. package/cjs/utils/blockbook-cache.js +0 -76
  125. package/cjs/utils/blockbook-cache.js.map +0 -1
  126. package/types/hardware-wallet-manager-singleton.d.ts +0 -19
  127. package/types/storage.d.ts +0 -5
  128. package/types/transactions/syscoin.d.ts +0 -9
  129. package/types/utils/blockbook-cache.d.ts +0 -6
@@ -0,0 +1,2781 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _KeyringCore_instances, _KeyringCore_deviceUnlockRetired, _KeyringCore_sessionOwnership, _KeyringCore_createDeviceUnlockControls, _KeyringCore_acceptSessionFrom, _KeyringCore_assertSessionOwner, _KeyringCore_captureSessionGuard, _KeyringCore_replaceSessionBuffers, _KeyringCore_finishUnlock, _KeyringCore_installVaultSession, _KeyringCore_recreateSessionFromVault;
13
+ import ecc from '@bitcoinerlab/secp256k1';
14
+ import { INetworkType, getNetworkConfig, } from '@sidhujag/sysweb3-network';
15
+ import { BIP32Factory } from 'bip32';
16
+ import bs58check from 'bs58check';
17
+ import crypto from 'crypto';
18
+ import CryptoJS from 'crypto-js';
19
+ import mapValues from 'lodash/mapValues';
20
+ import omit from 'lodash/omit';
21
+ import * as syscoinjs from 'syscoinjs-lib';
22
+ import * as BIP84 from 'syscoinjs-lib/bip84-replacement';
23
+ // Reference embedded bitcoinjs from syscoinjs-lib
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ const bjs = syscoinjs.utils.bitcoinjs;
26
+ const LEGACY_ENCRYPTION_KDF = 'pbkdf2-sha512-20000';
27
+ const SLH_DSA_DERIVATION_VERSION = 1;
28
+ // Preserve Pali v1 SLH derivation: first 32 bytes of the account-scoped HMAC.
29
+ // Pali expands this setup secret into the 48-byte SLH skSeed/skPrf/pkSeed tuple.
30
+ const SLH_DSA_SETUP_SECRET_HEX_LENGTH = 64;
31
+ const SLH_DSA_PARAMETER_SET = 'SLH-DSA-SHA2-128-24';
32
+ import { currentVaultRefusals } from './current-vault-refusal';
33
+ import { getAddress, isHexString } from './ethers-v6';
34
+ import { initialActiveImportedAccountState, initialActiveLedgerAccountState, initialActiveTrezorAccountState, } from './initial-state';
35
+ import { takeKeyringHostBinding } from './keyring-host';
36
+ import { decryptPrivateRecordBytes, encryptPrivateRecordBytes, privateRecordProfileId, } from './private-records';
37
+ import { getSyscoinSigners } from './signers';
38
+ import { snapshotEvmHdIndex } from './software-hd-account';
39
+ import { decryptCurrentVault, VaultAuthenticationError } from './storage-core';
40
+ import { EthereumTransactions } from './transactions/ethereum';
41
+ import { deriveEvmAccountFromMnemonic, privateKeyToAccount, } from './transactions/evm-local-signer';
42
+ import { preparedEvmGuard, signPreparedEvmEnvelope, snapshotPreparedEvmRequest, } from './transactions/prepared-evm';
43
+ import { SyscoinTransactionsCore, captureOwnedSyscoinRead, createHardwareSignerUnavailableError, } from './transactions/syscoin-core';
44
+ import { KeyringAccountType, } from './types';
45
+ import { getAddressDerivationPath, isEvmCoin, convertExtendedKeyVersion, } from './utils/derivation-paths';
46
+ import { fetchOwnedBlockbookAccount } from './utils/owned-blockbook-account';
47
+ import { assertPsbtAccountScope, createPsbtDerivationGuard, } from './utils/psbt-account-scope';
48
+ import { deriveDescriptorAddress } from './utils/watch-descriptor';
49
+ const currentVaultSalt = (keys) => {
50
+ const salt = keys && typeof keys === 'object' && Reflect.ownKeys(keys).length === 1
51
+ ? Object.getOwnPropertyDescriptor(keys, 'salt')?.value
52
+ : undefined;
53
+ return typeof salt === 'string' && /^[0-9a-f]{32}$/.test(salt)
54
+ ? salt
55
+ : undefined;
56
+ };
57
+ const readCurrentVaultPair = async (readPair) => {
58
+ const pair = await readPair();
59
+ if (pair === null)
60
+ return null;
61
+ const { vault, keys, pairFingerprint } = pair;
62
+ const salt = currentVaultSalt(keys);
63
+ if (typeof vault !== 'string' ||
64
+ salt === undefined ||
65
+ typeof pairFingerprint !== 'string' ||
66
+ !/^[0-9a-f]{64}$/.test(pairFingerprint)) {
67
+ throw new Error('Invalid device unlock vault pair');
68
+ }
69
+ return { vault, salt, pairFingerprint };
70
+ };
71
+ // Dynamic ETH HD path generation - will be computed as needed
72
+ /**
73
+ * Secure Buffer implementation for sensitive data
74
+ * Provides explicit memory clearing capability
75
+ */
76
+ class SecureBuffer {
77
+ constructor(data) {
78
+ this._isCleared = false;
79
+ if (typeof data === 'string') {
80
+ this.buffer = Buffer.from(data, 'utf8');
81
+ }
82
+ else {
83
+ this.buffer = Buffer.from(data);
84
+ }
85
+ }
86
+ get() {
87
+ if (this._isCleared || !this.buffer) {
88
+ throw new Error('SecureBuffer has been cleared');
89
+ }
90
+ return Buffer.from(this.buffer); // Return copy
91
+ }
92
+ toString() {
93
+ if (this._isCleared || !this.buffer) {
94
+ throw new Error('SecureBuffer has been cleared');
95
+ }
96
+ return this.buffer.toString('utf8');
97
+ }
98
+ clear() {
99
+ if (!this._isCleared && this.buffer) {
100
+ // Zeroing must not depend on a working platform random source.
101
+ this.buffer.fill(0);
102
+ this.buffer = null;
103
+ this._isCleared = true;
104
+ }
105
+ }
106
+ isCleared() {
107
+ return this._isCleared;
108
+ }
109
+ }
110
+ export class KeyringCore {
111
+ constructor(binding, hardware) {
112
+ _KeyringCore_instances.add(this);
113
+ // Store getter function for accessing Redux state
114
+ this.getVaultState = null;
115
+ // Method to inject store getter from Pali side
116
+ this.setVaultStateGetter = (getter) => {
117
+ this.getVaultState = getter;
118
+ };
119
+ // Helper method to get current vault state
120
+ this.getVault = () => {
121
+ if (!this.getVaultState) {
122
+ throw new Error('Vault state getter not configured. Call setVaultStateGetter() first.');
123
+ }
124
+ const vault = this.getVaultState();
125
+ // DEFENSIVE CHECK: Ensure vault state is properly structured
126
+ if (!vault) {
127
+ throw new Error('Vault state is undefined. Ensure Redux store is properly initialized with vault state.');
128
+ }
129
+ if (!vault.activeNetwork) {
130
+ throw new Error('Vault state is missing activeNetwork. Ensure vault state is properly initialized before keyring operations.');
131
+ }
132
+ if (!vault.activeAccount) {
133
+ throw new Error('Vault state is missing activeAccount. Ensure vault state is properly initialized before keyring operations.');
134
+ }
135
+ return vault;
136
+ };
137
+ // Helper to get active chain from vault state (replaces this.activeChain)
138
+ this.getActiveChain = () => {
139
+ return this.getVault().activeNetwork.kind;
140
+ };
141
+ this.accountAddressExists = (accounts, address, caseInsensitive = false) => {
142
+ const expectedAddress = caseInsensitive ? address.toLowerCase() : address;
143
+ return Object.values(accounts || {}).some((accountsByType) => Object.values(accountsByType || {}).some((account) => {
144
+ if (!account?.address) {
145
+ return false;
146
+ }
147
+ const accountAddress = caseInsensitive
148
+ ? account.address.toLowerCase()
149
+ : account.address;
150
+ return accountAddress === expectedAddress;
151
+ }));
152
+ };
153
+ // Secure session data - using Buffers that can be explicitly cleared
154
+ this.sessionPassword = null;
155
+ this.sessionMnemonic = null; // can be a mnemonic or a zprv, can be changed to a zprv when using an imported wallet
156
+ this.privateRecordSessionRevision = 0;
157
+ _KeyringCore_deviceUnlockRetired.set(this, false);
158
+ _KeyringCore_sessionOwnership.set(this, void 0);
159
+ this.createSyscoinTransactions = () => new SyscoinTransactionsCore(this.getSigner, this.getReadOnlySigner, this.getAccountsState, this.syscoinHardwareSigners, this.capturePsbtSigningContext);
160
+ this.requireLedgerSigner = () => {
161
+ if (!this.ledgerSigner)
162
+ throw createHardwareSignerUnavailableError('Ledger');
163
+ return this.ledgerSigner;
164
+ };
165
+ this.requireTrezorSigner = () => {
166
+ if (!this.trezorSigner)
167
+ throw createHardwareSignerUnavailableError('Trezor');
168
+ return this.trezorSigner;
169
+ };
170
+ // ===================================== PUBLIC METHODS - KEYRING MANAGER FOR HD - SYS ALL ===================================== //
171
+ this.validateAccountType = (account) => {
172
+ return account.isImported === true
173
+ ? KeyringAccountType.Imported
174
+ : KeyringAccountType.HDAccount;
175
+ };
176
+ this.isUnlocked = () => !!this.sessionPassword && !this.sessionPassword.isCleared();
177
+ this.lockWallet = async () => {
178
+ this.privateRecordSessionRevision++;
179
+ // Clear secure session data
180
+ if (this.sessionPassword) {
181
+ this.sessionPassword.clear();
182
+ this.sessionPassword = null;
183
+ }
184
+ if (this.sessionMnemonic) {
185
+ this.sessionMnemonic.clear();
186
+ this.sessionMnemonic = null;
187
+ }
188
+ const ownership = __classPrivateFieldGet(this, _KeyringCore_sessionOwnership, "f");
189
+ if (ownership.tracker.current === ownership.token) {
190
+ ownership.tracker.current = undefined;
191
+ ownership.tracker.epoch++;
192
+ }
193
+ // Clear transaction handlers that may hold HD signers
194
+ if (this.syscoinTransaction) {
195
+ // Replace with empty object to clear references
196
+ this.syscoinTransaction = {};
197
+ }
198
+ // NOTE: We intentionally don't clear ethereumTransaction here because
199
+ // polling needs the web3Provider even when the wallet is locked.
200
+ // Clean up hardware wallet connections (await to ensure HID is released)
201
+ if (this.ledgerSigner) {
202
+ try {
203
+ await this.ledgerSigner.destroy();
204
+ }
205
+ catch (_) {
206
+ // ignore
207
+ }
208
+ }
209
+ if (this.trezorSigner) {
210
+ try {
211
+ await this.trezorSigner.destroy();
212
+ }
213
+ catch (_) {
214
+ // ignore
215
+ }
216
+ }
217
+ };
218
+ // Direct secure transfer of session data to another keyring
219
+ this.transferSessionTo = (targetKeyring) => {
220
+ if (!this.isUnlocked()) {
221
+ throw new Error('Source keyring must be unlocked to transfer session');
222
+ }
223
+ // Cross-wallet transfers must fail before either keyring's buffers change.
224
+ if (!(targetKeyring instanceof KeyringCore) ||
225
+ targetKeyring.authority !== this.authority ||
226
+ targetKeyring.storage !== this.storage ||
227
+ targetKeyring.vaultPersistence !== this.vaultPersistence) {
228
+ throw new Error('Cannot transfer session between different wallet authorities');
229
+ }
230
+ if (targetKeyring === this)
231
+ return;
232
+ // Transfer ownership of our buffers to the target
233
+ if (!this.sessionPassword || !this.sessionMnemonic) {
234
+ throw new Error('Session data is missing during transfer');
235
+ }
236
+ __classPrivateFieldGet(targetKeyring, _KeyringCore_instances, "m", _KeyringCore_acceptSessionFrom).call(targetKeyring, this);
237
+ this.privateRecordSessionRevision++;
238
+ __classPrivateFieldSet(this, _KeyringCore_deviceUnlockRetired, true, "f");
239
+ // Null out our references (do NOT clear buffers - target owns them now)
240
+ this.sessionPassword = null;
241
+ this.sessionMnemonic = null;
242
+ };
243
+ this.addNewAccount = async (label) => {
244
+ // Check if wallet is unlocked
245
+ if (!this.isUnlocked()) {
246
+ throw new Error('Wallet must be unlocked to add new accounts');
247
+ }
248
+ // addNewAccount should only create accounts from the main seed
249
+ // For importing accounts (including zprvs), use importAccount
250
+ if (this.getActiveChain() === INetworkType.Syscoin) {
251
+ return await this.addNewAccountToSyscoinChain(label);
252
+ }
253
+ else {
254
+ // EVM chainType
255
+ return await this.addNewAccountToEth(label);
256
+ }
257
+ };
258
+ this.createSmartAccount = async ({ address, deriveAccount, label, metadata, }) => {
259
+ const vault = this.getVault();
260
+ const { accounts } = vault;
261
+ const smartAccounts = accounts[KeyringAccountType.SmartAccount] || {};
262
+ const nextId = this.getNextAccountId(smartAccounts);
263
+ const derived = deriveAccount
264
+ ? await deriveAccount(nextId)
265
+ : { address, metadata };
266
+ if (!derived.address) {
267
+ throw new Error('Smart account address is required');
268
+ }
269
+ if (this.accountAddressExists(accounts, derived.address, true)) {
270
+ throw new Error('Account already exists on your Wallet.');
271
+ }
272
+ return {
273
+ address: derived.address,
274
+ balances: {
275
+ [INetworkType.Syscoin]: 0,
276
+ [INetworkType.Ethereum]: 0,
277
+ },
278
+ id: nextId,
279
+ isImported: false,
280
+ isLedgerWallet: false,
281
+ isSmartAccount: true,
282
+ isTrezorWallet: false,
283
+ label: label || `Smart Account ${nextId + 1}`,
284
+ smartAccount: derived.metadata,
285
+ xprv: '',
286
+ xpub: derived.address,
287
+ };
288
+ };
289
+ this.getActiveUtxoAddressAccount = (id, ownedRead) => {
290
+ const captured = captureOwnedSyscoinRead(ownedRead, this.getAccountsState, this.capturePsbtSigningContext);
291
+ if (id !== captured.accountId)
292
+ throw new Error('UTXO account is not active');
293
+ const account = this.getVault().accounts[captured.accountType]?.[id];
294
+ captured.read.assertCurrent();
295
+ return { account, read: captured.read };
296
+ };
297
+ this.getNewChangeAddress = (ownedRead) => this.getChangeAddress(this.getVault().activeAccount.id, ownedRead);
298
+ this.getChangeAddress = async (id, ownedRead) => {
299
+ const { account, read } = this.getActiveUtxoAddressAccount(id, ownedRead);
300
+ return this.getAddressForUtxoAccount(account, true, read);
301
+ };
302
+ this.getPubkey = async (id, isChangeAddress, ownedRead) => {
303
+ const { account, read } = this.getActiveUtxoAddressAccount(id, ownedRead);
304
+ this.assertUtxoDerivationAvailable(account, 'Public key');
305
+ return this.getAddressPubkey(isChangeAddress, read);
306
+ };
307
+ this.getBip32Path = async (id, isChangeAddress, ownedRead) => {
308
+ const { account, read } = this.getActiveUtxoAddressAccount(id, ownedRead);
309
+ this.assertUtxoDerivationAvailable(account, 'BIP32 path');
310
+ return this.getAddressBip32Path(id, isChangeAddress, read);
311
+ };
312
+ this.updateReceivingAddress = async (ownedRead) => {
313
+ const { account, read } = this.getActiveUtxoAddressAccount(this.getVault().activeAccount.id, ownedRead);
314
+ return this.getAddressForUtxoAccount(account, false, read);
315
+ };
316
+ this.getAddressForUtxoAccount = (account, change, read) => {
317
+ const { xpub, isImported, address } = account;
318
+ if (isImported &&
319
+ xpub === address &&
320
+ !this.isDescriptor(xpub) &&
321
+ !this.isXpubLike(xpub))
322
+ return Promise.resolve(address);
323
+ return this.discoverUtxoAddress(change, read);
324
+ };
325
+ this.getAccountById = (id, accountType) => {
326
+ const vault = this.getVault();
327
+ const accounts = vault.accounts[accountType];
328
+ const account = accounts[id];
329
+ if (!account) {
330
+ throw new Error('Account not found');
331
+ }
332
+ return omit(account, 'xprv');
333
+ };
334
+ this.getPrivateKeyByAccountId = async (id, accountType, pwd) => {
335
+ try {
336
+ // Validate password using vault salt (same pattern as getSeed)
337
+ if (!this.sessionPassword) {
338
+ throw new Error('Unlock wallet first');
339
+ }
340
+ // Get vault salt for password validation
341
+ const vaultKeys = await this.storage.get('vault-keys');
342
+ if (!vaultKeys || !vaultKeys.salt) {
343
+ throw new Error('Vault keys not found');
344
+ }
345
+ const genPwd = await this.deriveStoredSessionKey(pwd, vaultKeys);
346
+ if (this.getSessionPasswordString() !== genPwd) {
347
+ throw new Error('Invalid password');
348
+ }
349
+ const vault = this.getVault();
350
+ const account = vault.accounts[accountType][id];
351
+ if (!account) {
352
+ throw new Error('Account not found');
353
+ }
354
+ // Decrypt the stored private key using secure method
355
+ const decryptedPrivateKey = this.withSecureData((sessionPwd) => {
356
+ const decrypted = CryptoJS.AES.decrypt(account.xprv, sessionPwd).toString(CryptoJS.enc.Utf8);
357
+ if (!decrypted) {
358
+ throw new Error('Failed to decrypt private key. Invalid password or corrupted data.');
359
+ }
360
+ return decrypted;
361
+ });
362
+ // NOTE: Returning decrypted private key as string is necessary for compatibility
363
+ // Callers should handle this sensitive data carefully
364
+ return decryptedPrivateKey;
365
+ }
366
+ catch (error) {
367
+ this.validateAndHandleErrorByMessage(error.message);
368
+ throw error;
369
+ }
370
+ };
371
+ this.getActiveAccount = () => {
372
+ const vault = this.getVault();
373
+ const { accounts, activeAccount } = vault;
374
+ const activeAccountId = activeAccount.id;
375
+ const activeAccountType = activeAccount.type;
376
+ return {
377
+ activeAccount: omit(accounts[activeAccountType][activeAccountId], 'xprv'),
378
+ activeAccountType,
379
+ };
380
+ };
381
+ this.isDescriptor = (s) => /^(addr|pkh|wpkh|sh|wsh|tr|combo|multi|sortedmulti)\s*\(/i.test(s || '');
382
+ this.isXpubLike = (s) => /^(xpub|tpub|zpub|vpub)/i.test(s || '');
383
+ this.getEncryptedXprv = (hd) => {
384
+ return this.withSecureData((sessionPwd) => {
385
+ return CryptoJS.AES.encrypt(this.getSysActivePrivateKey(hd), sessionPwd).toString();
386
+ });
387
+ };
388
+ this.getSeed = async (pwd) => {
389
+ if (!this.sessionPassword) {
390
+ throw new Error('Unlock wallet first');
391
+ }
392
+ // Get vault salt for password validation (consistent with getPrivateKeyByAccountId)
393
+ const vaultKeys = await this.storage.get('vault-keys');
394
+ if (!vaultKeys || !vaultKeys.salt) {
395
+ throw new Error('Vault keys not found');
396
+ }
397
+ const genPwd = await this.deriveStoredSessionKey(pwd, vaultKeys);
398
+ if (this.getSessionPasswordString() !== genPwd) {
399
+ throw new Error('Invalid password');
400
+ }
401
+ // v4 vault is encrypted with the derived session password key (PBKDF2 output)
402
+ let { mnemonic } = await this.vaultPersistence.getDecryptedVault(genPwd);
403
+ if (!mnemonic) {
404
+ throw new Error('Mnemonic not found in vault or is empty');
405
+ }
406
+ // Try to detect if mnemonic is encrypted or plain text
407
+ const isLikelyPlainMnemonic = mnemonic.includes(' ') &&
408
+ (mnemonic.split(' ').length === 12 || mnemonic.split(' ').length === 24);
409
+ if (!isLikelyPlainMnemonic) {
410
+ try {
411
+ mnemonic = CryptoJS.AES.decrypt(mnemonic, pwd).toString(CryptoJS.enc.Utf8);
412
+ }
413
+ catch (decryptError) {
414
+ // If decryption fails, assume mnemonic is already decrypted
415
+ console.warn('Mnemonic decryption failed in getSeed, using as-is:', decryptError.message);
416
+ }
417
+ }
418
+ if (!mnemonic) {
419
+ throw new Error('Failed to decrypt mnemonic or mnemonic is empty after decryption');
420
+ }
421
+ return mnemonic;
422
+ };
423
+ this.deriveSLHDSASetupSecretForAccount = async ({ accountIndex, }) => {
424
+ if (!this.sessionPassword) {
425
+ throw new Error('Unlock wallet first');
426
+ }
427
+ const mnemonic = this.getDecryptedMnemonic();
428
+ const normalizedMnemonic = mnemonic.normalize('NFKD');
429
+ const seed = CryptoJS.PBKDF2(normalizedMnemonic, `mnemonic`.normalize('NFKD'), {
430
+ hasher: CryptoJS.algo.SHA512,
431
+ iterations: 2048,
432
+ keySize: 512 / 32,
433
+ });
434
+ const derivationLabel = `PALI/${SLH_DSA_PARAMETER_SET}/v${SLH_DSA_DERIVATION_VERSION}/account/${accountIndex}`;
435
+ const digest = CryptoJS.HmacSHA512(derivationLabel, CryptoJS.enc.Hex.parse(seed.toString(CryptoJS.enc.Hex)));
436
+ return {
437
+ derivationLabel,
438
+ setupSecretHex: digest
439
+ .toString(CryptoJS.enc.Hex)
440
+ .slice(0, SLH_DSA_SETUP_SECRET_HEX_LENGTH),
441
+ };
442
+ };
443
+ this.encryptSLHDSASessionState = (plainText) => {
444
+ return this.withSecureData((sessionPwd) => CryptoJS.AES.encrypt(plainText, sessionPwd).toString());
445
+ };
446
+ this.decryptSLHDSASessionState = (cipherText) => {
447
+ return this.withSecureData((sessionPwd) => {
448
+ const plainText = CryptoJS.AES.decrypt(cipherText, sessionPwd).toString(CryptoJS.enc.Utf8);
449
+ if (!plainText) {
450
+ throw new Error('Failed to decrypt SLH-DSA session state');
451
+ }
452
+ return plainText;
453
+ });
454
+ };
455
+ this.setSignerNetwork = async (network) => {
456
+ // With multi-keyring architecture, each keyring is dedicated to specific slip44
457
+ if (INetworkType.Ethereum !== network.kind &&
458
+ INetworkType.Syscoin !== network.kind) {
459
+ throw new Error('Unsupported chain');
460
+ }
461
+ // Validate network/chain type compatibility
462
+ if (network.kind === INetworkType.Ethereum &&
463
+ this.getActiveChain() === INetworkType.Syscoin) {
464
+ throw new Error('Cannot use Ethereum chain type with Syscoin network');
465
+ }
466
+ if (network.kind === INetworkType.Syscoin &&
467
+ this.getActiveChain() === INetworkType.Ethereum) {
468
+ throw new Error('Cannot use Syscoin chain type with Ethereum network');
469
+ }
470
+ // CRITICAL: Prevent UTXO-to-UTXO network switching within same keyring
471
+ // Each UTXO network should have its own KeyringManager instance based on slip44
472
+ const vault = this.getVault();
473
+ if (this.getActiveChain() === INetworkType.Syscoin && vault.activeNetwork) {
474
+ const currentSlip44 = vault.activeNetwork.slip44;
475
+ const newSlip44 = network.slip44;
476
+ if (currentSlip44 !== newSlip44) {
477
+ throw new Error(`Cannot switch between different UTXO networks within the same keyring. ` +
478
+ `Current network uses slip44=${currentSlip44}, target network uses slip44=${newSlip44}. ` +
479
+ `Each UTXO network requires a separate KeyringManager instance.`);
480
+ }
481
+ }
482
+ try {
483
+ // With multi-keyring architecture:
484
+ // - UTXO: Each keyring is dedicated to one network (slip44), so this is only called during initialization
485
+ // - EVM: All EVM networks share slip44=60, so network can change within the same keyring
486
+ if (network.kind === INetworkType.Syscoin) {
487
+ // For UTXO networks: validate that active account exists (accounts should be created via addNewAccount/initialize)
488
+ const accountId = vault.activeAccount.id || 0;
489
+ const accountType = vault.activeAccount.type || KeyringAccountType.HDAccount;
490
+ const accounts = vault.accounts[accountType];
491
+ if (!accounts[accountId] || !accounts[accountId].xpub) {
492
+ throw new Error(`Active account ${accountType}:${accountId} does not exist. Create accounts using addNewAccount() or initializeWalletSecurely() first.`);
493
+ }
494
+ // No additional setup needed - on-demand signers will be created when needed
495
+ }
496
+ else if (network.kind === INetworkType.Ethereum) {
497
+ // For EVM networks: we primarily need a correct provider for the active network.
498
+ // Active account validation can be temporarily inconsistent during network switching
499
+ // (e.g., placeholder activeAccount like Trezor:0 before accounts hydrate), and provider
500
+ // setup does not depend on it. So treat missing active account as a warning, not a hard error.
501
+ const accountId = vault.activeAccount.id || 0;
502
+ const accountType = vault.activeAccount.type || KeyringAccountType.HDAccount;
503
+ const accounts = vault.accounts[accountType];
504
+ if (!accounts?.[accountId]) {
505
+ console.warn(`[KeyringManager] Active account ${accountType}:${accountId} not found while setting EVM network; continuing to set provider`);
506
+ }
507
+ // Set up EVM provider for network switching
508
+ await this.setSignerEVM(network);
509
+ }
510
+ return {
511
+ success: true,
512
+ };
513
+ }
514
+ catch (err) {
515
+ console.log('ERROR setSignerNetwork', {
516
+ err,
517
+ });
518
+ this.validateAndHandleErrorByMessage(err.message);
519
+ //Rollback to previous values
520
+ console.error('Set Signer Network failed with', err);
521
+ return { success: false };
522
+ }
523
+ };
524
+ this.forgetMainWallet = async (pwd) => {
525
+ const vaultKeys = await this.storage.get('vault-keys');
526
+ if (!vaultKeys || !vaultKeys.salt) {
527
+ throw new Error('Vault keys not found');
528
+ }
529
+ const genPwd = await this.deriveStoredSessionKey(pwd, vaultKeys);
530
+ if (!this.sessionPassword) {
531
+ throw new Error('Unlock wallet first');
532
+ }
533
+ else if (this.getSessionPasswordString() !== genPwd) {
534
+ throw new Error('Invalid password');
535
+ }
536
+ await this.clearTemporaryLocalKeys();
537
+ };
538
+ this.importWeb3Account = (mnemonicOrPrivKey) => {
539
+ // Check if it's a hex string (Ethereum private key)
540
+ if (isHexString(mnemonicOrPrivKey)) {
541
+ return privateKeyToAccount(mnemonicOrPrivKey);
542
+ }
543
+ // Check if it's a zprv/tprv (Syscoin private key)
544
+ const zprvPrefixes = ['zprv', 'tprv', 'vprv', 'xprv'];
545
+ if (zprvPrefixes.some((prefix) => mnemonicOrPrivKey.startsWith(prefix))) {
546
+ throw new Error('Syscoin extended private keys (zprv/tprv) should be imported using importAccount, not importWeb3Account');
547
+ }
548
+ // Otherwise, assume it's a mnemonic
549
+ return deriveEvmAccountFromMnemonic(mnemonicOrPrivKey, getAddressDerivationPath('eth', 60, 0, false, 0));
550
+ };
551
+ this.getAccountXpub = () => {
552
+ const vault = this.getVault();
553
+ const { activeAccount } = vault;
554
+ const account = vault.accounts[activeAccount.type]?.[activeAccount.id];
555
+ if (!account) {
556
+ throw new Error('Active account not found');
557
+ }
558
+ return account.xpub;
559
+ };
560
+ this.isSeedValid = (seedPhrase) => BIP84.validateMnemonic(seedPhrase);
561
+ this.createNewSeed = (wordCount) => {
562
+ // Map BIP39 word counts to entropy strength
563
+ const wordCountToStrength = {
564
+ 12: 128,
565
+ 15: 160,
566
+ 18: 192,
567
+ 21: 224,
568
+ 24: 256,
569
+ };
570
+ const strength = wordCount ? wordCountToStrength[wordCount] : 128;
571
+ return strength
572
+ ? BIP84.generateMnemonic(strength)
573
+ : BIP84.generateMnemonic();
574
+ };
575
+ this.getUTXOState = () => {
576
+ const vault = this.getVault();
577
+ if (vault.activeNetwork.kind !== INetworkType.Syscoin) {
578
+ throw new Error('Cannot get state in a ethereum network');
579
+ }
580
+ const utxOAccounts = mapValues(vault.accounts.HDAccount, (value) => omit(value, 'xprv'));
581
+ return {
582
+ ...vault,
583
+ accounts: {
584
+ [KeyringAccountType.HDAccount]: utxOAccounts,
585
+ [KeyringAccountType.Imported]: {},
586
+ [KeyringAccountType.Trezor]: {},
587
+ [KeyringAccountType.Ledger]: {},
588
+ },
589
+ };
590
+ };
591
+ this.getActiveUTXOAccountState = () => {
592
+ const vault = this.getVault();
593
+ const { activeAccount } = vault;
594
+ return {
595
+ ...vault.accounts.HDAccount[activeAccount.id],
596
+ xprv: undefined,
597
+ };
598
+ };
599
+ this.getNetwork = () => this.getVault().activeNetwork;
600
+ this.createEthAccount = (privateKey) => privateKeyToAccount(privateKey);
601
+ this.fetchCurrentAccountData = async (isChangeAddress, read) => {
602
+ read.assertCurrent();
603
+ const { tokens } = await fetchOwnedBlockbookAccount(read.network.url, read.xpub, read.transport);
604
+ read.assertCurrent();
605
+ const { receivingIndex, changeIndex } = this.setLatestIndexesFromXPubTokens(tokens);
606
+ return {
607
+ currentAccount: this.loadBip84AddressAccount(read.xpub, read.network),
608
+ addressIndex: isChangeAddress ? changeIndex : receivingIndex,
609
+ };
610
+ };
611
+ this.discoverUtxoAddress = async (isChangeAddress, read) => {
612
+ read.assertCurrent();
613
+ if (this.isDescriptor(read.xpub))
614
+ throw new Error('Address rotation is not supported for watch-only descriptors');
615
+ const { currentAccount, addressIndex } = await this.fetchCurrentAccountData(isChangeAddress, read);
616
+ read.assertCurrent();
617
+ return currentAccount.getAddress(addressIndex, isChangeAddress, 84);
618
+ };
619
+ this.getAddress = async (xpub, isChangeAddress, ownedRead) => {
620
+ const id = this.getVault().activeAccount.id;
621
+ const { account, read } = this.getActiveUtxoAddressAccount(id, ownedRead);
622
+ if (xpub !== read.xpub)
623
+ throw new Error('UTXO account is not active');
624
+ return this.getAddressForUtxoAccount(account, isChangeAddress, read);
625
+ };
626
+ this.getAddressPubkey = async (isChangeAddress, read) => {
627
+ const { currentAccount, addressIndex } = await this.fetchCurrentAccountData(isChangeAddress, read);
628
+ read.assertCurrent();
629
+ return currentAccount.getPublicKey(addressIndex, isChangeAddress);
630
+ };
631
+ this.getCurrentAddressPubkey = async (xpub, isChangeAddress, ownedRead) => {
632
+ const id = this.getVault().activeAccount.id;
633
+ const { account, read } = this.getActiveUtxoAddressAccount(id, ownedRead);
634
+ if (xpub !== read.xpub)
635
+ throw new Error('UTXO account is not active');
636
+ this.assertUtxoDerivationAvailable(account, 'Public key');
637
+ return this.getAddressPubkey(isChangeAddress, read);
638
+ };
639
+ this.getAddressBip32Path = async (id, isChangeAddress, read) => {
640
+ const { addressIndex } = await this.fetchCurrentAccountData(isChangeAddress, read);
641
+ read.assertCurrent();
642
+ return getAddressDerivationPath(read.network.currency.toLowerCase(), read.network.slip44, id, isChangeAddress, addressIndex);
643
+ };
644
+ this.getCurrentAddressBip32Path = async (xpub, isChangeAddress, ownedRead) => {
645
+ const id = this.getVault().activeAccount.id;
646
+ const { account, read } = this.getActiveUtxoAddressAccount(id, ownedRead);
647
+ if (xpub !== read.xpub)
648
+ throw new Error('UTXO account is not active');
649
+ this.assertUtxoDerivationAvailable(account, 'BIP32 path');
650
+ return this.getAddressBip32Path(id, isChangeAddress, read);
651
+ };
652
+ this.logout = () => {
653
+ this.lockWallet();
654
+ };
655
+ /**
656
+ * PRIVATE METHODS
657
+ */
658
+ // ===================================== AUXILIARY METHOD - FOR TRANSACTIONS CLASSES ===================================== //
659
+ this.getDecryptedPrivateKey = (targetAccount) => {
660
+ try {
661
+ const vault = this.getVault();
662
+ const { accounts } = vault;
663
+ const activeAccount = targetAccount || vault.activeAccount;
664
+ const activeAccountId = activeAccount.id;
665
+ const activeAccountType = activeAccount.type;
666
+ const isLedger = activeAccountType === KeyringAccountType.Ledger;
667
+ const isTrezor = activeAccountType === KeyringAccountType.Trezor;
668
+ const isHardwareWallet = isTrezor || isLedger;
669
+ if (!this.sessionPassword)
670
+ throw new Error('Wallet is locked cant proceed with transaction');
671
+ const activeAccountData = accounts[activeAccountType][activeAccountId];
672
+ if (!activeAccountData) {
673
+ throw new Error(`Active account (${activeAccountType}:${activeAccountId}) not found. Account switching may be in progress.`);
674
+ }
675
+ const { xprv, address } = activeAccountData;
676
+ if (isLedger || activeAccountData.isLedgerWallet)
677
+ this.requireLedgerSigner();
678
+ if (isTrezor || activeAccountData.isTrezorWallet)
679
+ this.requireTrezorSigner();
680
+ if (isHardwareWallet) {
681
+ return {
682
+ address,
683
+ decryptedPrivateKey: '',
684
+ };
685
+ }
686
+ if (!xprv) {
687
+ throw new Error(`Private key not found for account ${activeAccountType}:${activeAccountId}. Account may not be fully initialized.`);
688
+ }
689
+ let decryptedPrivateKey;
690
+ try {
691
+ decryptedPrivateKey = this.withSecureData((sessionPwd) => {
692
+ return CryptoJS.AES.decrypt(xprv, sessionPwd).toString(CryptoJS.enc.Utf8);
693
+ });
694
+ }
695
+ catch (decryptError) {
696
+ throw new Error(`Failed to decrypt private key for account ${activeAccountType}:${activeAccountId}. The wallet may be locked or corrupted.`);
697
+ }
698
+ if (!decryptedPrivateKey) {
699
+ throw new Error(`Decrypted private key is empty for account ${activeAccountType}:${activeAccountId}. Invalid password or corrupted data.`);
700
+ }
701
+ // For EVM accounts, validate that the derived address matches the stored address
702
+ // This helps catch account switching race conditions early
703
+ if (this.getActiveChain() === INetworkType.Ethereum) {
704
+ try {
705
+ const derivedAccount = privateKeyToAccount(decryptedPrivateKey);
706
+ if (derivedAccount.address.toLowerCase() !== address.toLowerCase()) {
707
+ throw new Error(`Address mismatch for account ${activeAccountType}:${activeAccountId}. Expected ${address} but derived ${derivedAccount.address}. Account switching may be in progress.`);
708
+ }
709
+ }
710
+ catch (addressError) {
711
+ throw new Error(`Failed to validate EVM address for account ${activeAccountType}:${activeAccountId}: ${addressError.message}`);
712
+ }
713
+ }
714
+ return {
715
+ address,
716
+ decryptedPrivateKey,
717
+ };
718
+ }
719
+ catch (error) {
720
+ const vaultForLogging = this.getVault();
721
+ const isTestEnv = typeof process !== 'undefined' && process?.env?.NODE_ENV === 'test';
722
+ if (!isTestEnv) {
723
+ console.error('ERROR getDecryptedPrivateKey', {
724
+ error: error.message,
725
+ activeChain: this.getActiveChain(),
726
+ vault: {
727
+ activeAccountId: vaultForLogging?.activeAccount?.id,
728
+ activeAccountType: vaultForLogging?.activeAccount?.type,
729
+ },
730
+ });
731
+ }
732
+ this.validateAndHandleErrorByMessage(error.message);
733
+ throw error;
734
+ }
735
+ };
736
+ this.capturePsbtSigningContext = (bindSessionRevision = true) => {
737
+ const session = this.sessionPassword;
738
+ const revision = this.privateRecordSessionRevision;
739
+ const vault = this.getVault();
740
+ const selected = { ...vault.activeAccount };
741
+ const account = vault.accounts[selected.type]?.[selected.id];
742
+ const identity = { address: account?.address, xpub: account?.xpub };
743
+ const network = { ...vault.activeNetwork };
744
+ const assertCurrent = () => {
745
+ const current = this.getVault();
746
+ const currentAccount = current.accounts[selected.type]?.[selected.id];
747
+ if (!session ||
748
+ session.isCleared() ||
749
+ this.sessionPassword !== session ||
750
+ (bindSessionRevision &&
751
+ this.privateRecordSessionRevision !== revision) ||
752
+ current.activeAccount.id !== selected.id ||
753
+ current.activeAccount.type !== selected.type ||
754
+ currentAccount?.address !== identity.address ||
755
+ currentAccount?.xpub !== identity.xpub ||
756
+ current.activeNetwork.kind !== network.kind ||
757
+ current.activeNetwork.chainId !== network.chainId ||
758
+ current.activeNetwork.slip44 !== network.slip44 ||
759
+ current.activeNetwork.url !== network.url) {
760
+ throw new Error('Wallet signing context changed');
761
+ }
762
+ };
763
+ assertCurrent();
764
+ return assertCurrent;
765
+ };
766
+ this.getSigner = () => {
767
+ if (!this.sessionPassword) {
768
+ throw new Error('Wallet is locked cant proceed with transaction');
769
+ }
770
+ if (this.getActiveChain() !== INetworkType.Syscoin) {
771
+ throw new Error('Switch to UTXO chain');
772
+ }
773
+ const vault = this.getVault();
774
+ const { activeAccount } = vault;
775
+ const accountId = activeAccount.id;
776
+ const accountType = activeAccount.type;
777
+ const assertCurrent = this.capturePsbtSigningContext();
778
+ // Determine signer type: HD or WIF single-address
779
+ let signerForUse;
780
+ if (accountType === KeyringAccountType.HDAccount) {
781
+ signerForUse = this.createOnDemandUTXOSigner(accountId);
782
+ }
783
+ else if (accountType === KeyringAccountType.Imported) {
784
+ // Decrypt stored key material
785
+ const decrypted = this.withSecureData((sessionPwd) => {
786
+ const account = vault.accounts[KeyringAccountType.Imported][accountId];
787
+ const res = CryptoJS.AES.decrypt(account.xprv, sessionPwd).toString(CryptoJS.enc.Utf8);
788
+ if (!res)
789
+ throw new Error('Failed to decrypt imported account key');
790
+ return res;
791
+ });
792
+ if (this.isZprv(decrypted)) {
793
+ signerForUse = this.createFreshUTXOSigner(decrypted, accountId);
794
+ }
795
+ else {
796
+ // Treat as WIF single-address signer wrapper exposing sign(psbt)
797
+ const network = vault.activeNetwork;
798
+ const { networks } = getNetworkConfig(network.slip44, network.currency);
799
+ const isTestnet = network.slip44 === 1;
800
+ const bitcoinjsNetwork = isTestnet
801
+ ? networks.testnet
802
+ : networks.mainnet;
803
+ signerForUse = {
804
+ sign: async (psbt) => {
805
+ return await syscoinjs.utils.signWithWIF(psbt, decrypted, bitcoinjsNetwork);
806
+ },
807
+ };
808
+ }
809
+ }
810
+ else {
811
+ throw new Error(`Unsupported account type for UTXO signing: ${accountType}`);
812
+ }
813
+ const scope = {
814
+ account: vault.accounts[accountType][accountId],
815
+ accountId,
816
+ accountType,
817
+ network: vault.activeNetwork,
818
+ };
819
+ if (typeof signerForUse.getRootNode === 'function') {
820
+ const getRootNode = signerForUse.getRootNode.bind(signerForUse);
821
+ const guardPath = createPsbtDerivationGuard(scope);
822
+ signerForUse.getRootNode = () => {
823
+ const root = getRootNode();
824
+ const scopedRoot = Object.create(root);
825
+ scopedRoot.derivePath = (path) => {
826
+ guardPath(path);
827
+ return root.derivePath(path);
828
+ };
829
+ return scopedRoot;
830
+ };
831
+ }
832
+ const sign = signerForUse.sign.bind(signerForUse);
833
+ signerForUse.sign = async (psbt) => {
834
+ assertCurrent();
835
+ assertPsbtAccountScope(psbt, scope);
836
+ const result = await sign(psbt);
837
+ assertCurrent();
838
+ return result;
839
+ };
840
+ // Create syscoinjs instance with current network (no need to attach signer for signing flow)
841
+ const network = vault.activeNetwork;
842
+ const networkConfig = getNetworkConfig(network.slip44, network.currency);
843
+ const isTestnet = network.slip44 === 1;
844
+ const bitcoinjsNetwork = isTestnet
845
+ ? networkConfig?.networks?.testnet
846
+ : networkConfig?.networks?.mainnet;
847
+ const syscoinMainSigner = new syscoinjs.SyscoinJSLib(null, network.url, bitcoinjsNetwork);
848
+ return {
849
+ hd: signerForUse,
850
+ main: syscoinMainSigner,
851
+ };
852
+ };
853
+ // Read-only version that works when wallet is locked
854
+ this.getReadOnlySigner = (ownedRead) => {
855
+ ownedRead?.assertCurrent();
856
+ if ((ownedRead ? ownedRead.network.kind : this.getActiveChain()) !==
857
+ INetworkType.Syscoin) {
858
+ throw new Error('Switch to UTXO chain');
859
+ }
860
+ // Create syscoinjs instance without HD signer for read-only operations
861
+ const network = ownedRead?.network ?? this.getVault().activeNetwork;
862
+ const networkConfig = getNetworkConfig(network.slip44, network.currency);
863
+ const isTestnet = network.slip44 === 1;
864
+ const bitcoinjsNetwork = isTestnet
865
+ ? networkConfig?.networks?.testnet
866
+ : networkConfig?.networks?.mainnet;
867
+ if (ownedRead && !bitcoinjsNetwork)
868
+ throw new Error('Missing owned UTXO network configuration');
869
+ const syscoinMainSigner = new syscoinjs.SyscoinJSLib(null, // No HD signer needed for read-only operations
870
+ network.url, bitcoinjsNetwork, ...(ownedRead ? [ownedRead.transport] : []));
871
+ if (!ownedRead)
872
+ return { main: syscoinMainSigner };
873
+ return {
874
+ main: syscoinMainSigner,
875
+ getOwnedAddress: (xpub, isChangeAddress) => {
876
+ if (xpub !== ownedRead.xpub)
877
+ throw new Error('UTXO account is not active');
878
+ return this.discoverUtxoAddress(isChangeAddress, ownedRead);
879
+ },
880
+ };
881
+ };
882
+ this.getAccountsState = () => {
883
+ const vault = this.getVault();
884
+ const { accounts, activeAccount, activeNetwork } = vault;
885
+ return {
886
+ activeAccountId: activeAccount.id,
887
+ accounts,
888
+ activeAccountType: activeAccount.type,
889
+ activeNetwork,
890
+ };
891
+ };
892
+ /**
893
+ *
894
+ * @param password
895
+ * @param salt
896
+ * @returns derived key: hex string
897
+ */
898
+ this.encryptSHA512 = (password, salt) => {
899
+ // Session/encryption key derivation (used to encrypt/decrypt xprv in-memory and persisted state).
900
+ // PBKDF2 with SHA-512, 20,000 iterations (prod default), 256-bit output.
901
+ // NOTE: Tests override iterations to keep CI fast.
902
+ const isTestEnv = typeof process !== 'undefined' && process?.env?.NODE_ENV === 'test';
903
+ const iterations = Number.parseInt(process?.env?.SYSWEB3_PBKDF2_ENC_ITERS || '', 10) ||
904
+ (isTestEnv ? 1000 : 20000);
905
+ const key = CryptoJS.PBKDF2(password, CryptoJS.enc.Hex.parse(salt), {
906
+ keySize: 256 / 32, // 256 bits
907
+ iterations,
908
+ hasher: CryptoJS.algo.SHA512,
909
+ });
910
+ return key.toString();
911
+ };
912
+ this.hasWebCrypto = () => {
913
+ // MV3 service worker & extension pages: globalThis.crypto.subtle exists.
914
+ // Jest tests mock global.crypto without subtle -> fallback to CryptoJS.
915
+ return !!globalThis?.crypto?.subtle;
916
+ };
917
+ this.hexToBytes = (hex) => {
918
+ const clean = (hex || '').trim();
919
+ if (!/^[0-9a-fA-F]*$/.test(clean) || clean.length % 2 !== 0) {
920
+ throw new Error('Invalid hex string');
921
+ }
922
+ const out = new Uint8Array(clean.length / 2);
923
+ for (let i = 0; i < out.length; i++) {
924
+ out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
925
+ }
926
+ return out;
927
+ };
928
+ this.bytesToHex = (bytes) => {
929
+ const u8 = new Uint8Array(bytes);
930
+ let hex = '';
931
+ for (let i = 0; i < u8.length; i++) {
932
+ hex += u8[i].toString(16).padStart(2, '0');
933
+ }
934
+ return hex;
935
+ };
936
+ this.getEncIterations = () => {
937
+ const isTestEnv = typeof process !== 'undefined' && process?.env?.NODE_ENV === 'test';
938
+ return (Number.parseInt(process?.env?.SYSWEB3_PBKDF2_ENC_ITERS || '', 10) ||
939
+ (isTestEnv ? 1000 : 900000));
940
+ };
941
+ this.getSysActivePrivateKey = (hd) => {
942
+ if (hd === null)
943
+ throw new Error('No HD Signer');
944
+ const accountIndex = hd.Signer.accountIndex;
945
+ // Verify the account exists now
946
+ if (!hd.Signer.accounts.has(accountIndex)) {
947
+ throw new Error(`Account at index ${accountIndex} could not be created`);
948
+ }
949
+ return hd.Signer.accounts.get(accountIndex).getAccountPrivateKey();
950
+ };
951
+ this.getInitialAccountData = ({ label, signer, sysAccount, xprv, }) => {
952
+ const { address, xpub } = sysAccount;
953
+ return {
954
+ id: signer.Signer.accountIndex,
955
+ label: label || `Account ${signer.Signer.accountIndex + 1}`,
956
+ xpub,
957
+ xprv,
958
+ address,
959
+ isTrezorWallet: false,
960
+ isLedgerWallet: false,
961
+ isImported: false,
962
+ };
963
+ };
964
+ this.getFormattedBackendAccount = async ({ signer, }) => {
965
+ // MUCH SIMPLER: Just use the signer directly - no BIP84 needed!
966
+ // Get address directly from the signer (always correct for current network)
967
+ const address = signer.createAddress(0, false, 84);
968
+ const xpub = signer.getAccountXpub();
969
+ return {
970
+ address,
971
+ xpub,
972
+ };
973
+ };
974
+ this.setLatestIndexesFromXPubTokens = function (tokens) {
975
+ let changeIndexInternal = -1, receivingIndexInternal = -1;
976
+ if (tokens) {
977
+ tokens.forEach((token) => {
978
+ if (!token.transfers || !token.path) {
979
+ return {
980
+ changeIndex: changeIndexInternal + 1,
981
+ receivingIndex: receivingIndexInternal + 1,
982
+ };
983
+ }
984
+ const transfers = parseInt(token.transfers, 10);
985
+ if (token.path && transfers > 0) {
986
+ const splitPath = token.path.split('/');
987
+ if (splitPath.length >= 6) {
988
+ const change = parseInt(splitPath[4], 10);
989
+ const index = parseInt(splitPath[5], 10);
990
+ if (change === 1) {
991
+ if (index > changeIndexInternal) {
992
+ changeIndexInternal = index;
993
+ }
994
+ }
995
+ else if (index > receivingIndexInternal) {
996
+ receivingIndexInternal = index;
997
+ }
998
+ }
999
+ }
1000
+ });
1001
+ }
1002
+ return {
1003
+ changeIndex: changeIndexInternal + 1,
1004
+ receivingIndex: receivingIndexInternal + 1,
1005
+ };
1006
+ };
1007
+ this.getBasicWeb3AccountInfo = (id, label) => {
1008
+ return {
1009
+ id,
1010
+ isTrezorWallet: false,
1011
+ isLedgerWallet: false,
1012
+ label: label ? label : `Account ${id + 1}`,
1013
+ };
1014
+ };
1015
+ this.setDerivedWeb3Accounts = async (id, label) => {
1016
+ try {
1017
+ // For account creation, derive from mnemonic (since account doesn't exist yet)
1018
+ const mnemonic = this.getDecryptedMnemonic();
1019
+ const derivationPath = getAddressDerivationPath('eth', 60, 0, false, id);
1020
+ const derivedAccount = deriveEvmAccountFromMnemonic(mnemonic, derivationPath);
1021
+ const basicAccountInfo = this.getBasicWeb3AccountInfo(id, label);
1022
+ const createdAccount = {
1023
+ address: derivedAccount.address,
1024
+ xpub: derivedAccount.publicKey,
1025
+ xprv: this.withSecureData((sessionPwd) => {
1026
+ return CryptoJS.AES.encrypt(derivedAccount.privateKey, sessionPwd).toString();
1027
+ }),
1028
+ isImported: false,
1029
+ ...basicAccountInfo,
1030
+ balances: { syscoin: 0, ethereum: 0 },
1031
+ };
1032
+ // NOTE: Account creation should be dispatched to Redux store, not stored here
1033
+ // Return the account data for Pali to add to store
1034
+ return createdAccount;
1035
+ }
1036
+ catch (error) {
1037
+ console.log('ERROR setDerivedWeb3Accounts', {
1038
+ error,
1039
+ });
1040
+ this.validateAndHandleErrorByMessage(error.message);
1041
+ throw error;
1042
+ }
1043
+ };
1044
+ this.setSignerEVM = async (network) => {
1045
+ const abortController = new AbortController();
1046
+ try {
1047
+ // With multi-keyring architecture, this is only called on EVM keyrings
1048
+ this.ethereumTransaction.setWeb3Provider(network);
1049
+ abortController.abort();
1050
+ }
1051
+ catch (error) {
1052
+ abortController.abort();
1053
+ throw new Error(`SetSignerEVM: Failed with ${error}`);
1054
+ }
1055
+ };
1056
+ this.clearTemporaryLocalKeys = async () => {
1057
+ // Clear the vault completely.
1058
+ // For v4, the vault is encrypted with a derived key, so the safest wipe is to delete the stored blob.
1059
+ await this.storage.deleteItem('vault');
1060
+ // Remove vault-keys from storage so no vault exists at all
1061
+ await this.storage.deleteItem('vault-keys');
1062
+ console.log('[KeyringManager] Temporary local keys cleared');
1063
+ this.logout();
1064
+ };
1065
+ // NEW: Separate session initialization from account creation
1066
+ this.initializeSession = async (seedPhrase, password) => {
1067
+ // Validate inputs first
1068
+ if (!BIP84.validateMnemonic(seedPhrase)) {
1069
+ throw new Error('Invalid Seed');
1070
+ }
1071
+ const assertCurrent = __classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_captureSessionGuard).call(this);
1072
+ assertCurrent();
1073
+ const [vault, vaultKeys] = await Promise.all([
1074
+ this.storage.get('vault'),
1075
+ this.storage.get('vault-keys'),
1076
+ ]);
1077
+ assertCurrent();
1078
+ const hasVault = vault !== undefined && vault !== null;
1079
+ const hasVaultKeys = vaultKeys !== undefined && vaultKeys !== null;
1080
+ const livePassword = this.sessionPassword;
1081
+ const liveMnemonic = this.sessionMnemonic;
1082
+ // Only an already-unlocked, matching session may repeat initialization.
1083
+ // Restoring any existing wallet must use unlock(), never rewrite its vault.
1084
+ if (livePassword &&
1085
+ liveMnemonic &&
1086
+ !livePassword.isCleared() &&
1087
+ !liveMnemonic.isCleared()) {
1088
+ if (!hasVault || !hasVaultKeys || !vaultKeys.salt) {
1089
+ throw new Error('Cannot initialize a new vault over existing wallet storage');
1090
+ }
1091
+ const sessionPasswordSaltedHash = await this.deriveStoredSessionKey(password, vaultKeys);
1092
+ assertCurrent();
1093
+ if (livePassword === this.sessionPassword &&
1094
+ liveMnemonic === this.sessionMnemonic &&
1095
+ sessionPasswordSaltedHash === this.getSessionPasswordString()) {
1096
+ // Same password - check if it's the same mnemonic to ensure full idempotency
1097
+ let currentMnemonic;
1098
+ try {
1099
+ currentMnemonic = this.withSecureData((sessionPwd, sessionMnemonic) => {
1100
+ return CryptoJS.AES.decrypt(sessionMnemonic, sessionPwd).toString(CryptoJS.enc.Utf8);
1101
+ });
1102
+ }
1103
+ catch (error) {
1104
+ // If we can't decrypt, fall through to error
1105
+ }
1106
+ if (currentMnemonic === seedPhrase) {
1107
+ const storedVault = await this.vaultPersistence.getDecryptedVault(sessionPasswordSaltedHash);
1108
+ assertCurrent();
1109
+ if (livePassword === this.sessionPassword &&
1110
+ liveMnemonic === this.sessionMnemonic &&
1111
+ !livePassword.isCleared() &&
1112
+ !liveMnemonic.isCleared() &&
1113
+ storedVault?.mnemonic === seedPhrase) {
1114
+ // This is a read-only identity check, not a second vault write.
1115
+ return;
1116
+ }
1117
+ }
1118
+ }
1119
+ // Different password or mnemonic - this is not a simple re-initialization
1120
+ throw new Error('Wallet already initialized with different parameters. Unlock the existing wallet or explicitly remove it before starting setup.');
1121
+ }
1122
+ if (hasVault || hasVaultKeys) {
1123
+ throw new Error('Cannot initialize a new vault over existing wallet storage');
1124
+ }
1125
+ const salt = crypto.randomBytes(16).toString('hex');
1126
+ // v4 stores only the salt on disk. A genuinely fresh initialization always
1127
+ // commits its own salt and ciphertext together under the vault mutex.
1128
+ const sessionPasswordSaltedHash = await this.deriveStoredSessionKey(password, { salt });
1129
+ // Encrypt and store vault (mnemonic storage) - now uses single vault for all networks
1130
+ await this.vaultPersistence.setEncryptedVault({
1131
+ mnemonic: seedPhrase, // Store plain mnemonic - setEncryptedVault will encrypt the entire vault
1132
+ },
1133
+ // v4 vault is encrypted with the derived session password key (PBKDF2 output)
1134
+ sessionPasswordSaltedHash, { salt }, assertCurrent);
1135
+ assertCurrent();
1136
+ const assertInstalled = await __classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_recreateSessionFromVault).call(this, sessionPasswordSaltedHash, assertCurrent);
1137
+ assertInstalled();
1138
+ };
1139
+ // NEW: Create first account without signer setup
1140
+ this.createFirstAccount = async (label) => {
1141
+ if (!this.sessionPassword || !this.sessionMnemonic) {
1142
+ throw new Error('Session must be initialized first. Call initializeSession.');
1143
+ }
1144
+ const vault = this.getVault();
1145
+ const network = vault.activeNetwork;
1146
+ if (network.kind === INetworkType.Syscoin) {
1147
+ // UTXO accounts get network-aware labels since each network has separate keyrings
1148
+ const defaultLabel = label || this.generateNetworkAwareLabel(0, network);
1149
+ // Create UTXO account using on-demand signer
1150
+ const freshHDSigner = this.createOnDemandUTXOSigner(0);
1151
+ const sysAccount = await this.getFormattedBackendAccount({
1152
+ signer: freshHDSigner,
1153
+ });
1154
+ const encryptedXprv = this.getEncryptedXprv(freshHDSigner);
1155
+ return {
1156
+ ...this.getInitialAccountData({
1157
+ label: defaultLabel,
1158
+ signer: freshHDSigner,
1159
+ sysAccount,
1160
+ xprv: encryptedXprv,
1161
+ }),
1162
+ balances: { syscoin: 0, ethereum: 0 },
1163
+ };
1164
+ }
1165
+ else {
1166
+ // EVM accounts get generic labels since they work across all EVM networks
1167
+ const defaultLabel = label || 'Account 1';
1168
+ return await this.setDerivedWeb3Accounts(0, defaultLabel);
1169
+ }
1170
+ };
1171
+ this.initializeWalletSecurely = async (seedPhrase, password) => {
1172
+ // Use new separated approach
1173
+ await this.initializeSession(seedPhrase, password);
1174
+ return await this.createFirstAccount();
1175
+ };
1176
+ const dependencies = takeKeyringHostBinding(binding);
1177
+ this.authority = dependencies.authority;
1178
+ __classPrivateFieldSet(this, _KeyringCore_sessionOwnership, {
1179
+ tracker: dependencies.sessionOwner,
1180
+ token: Symbol(),
1181
+ }, "f");
1182
+ this.storage = dependencies.storage;
1183
+ this.vaultPersistence = dependencies.vaultPersistence;
1184
+ this.ledgerSigner = hardware.ledgerSigner;
1185
+ this.trezorSigner = hardware.trezorSigner;
1186
+ this.syscoinHardwareSigners = { ...hardware.syscoinHardwareSigners };
1187
+ this.writeUtf8Diagnostic(false);
1188
+ this.utf8Error = false;
1189
+ this.initialTrezorAccountState = initialActiveTrezorAccountState;
1190
+ this.initialLedgerAccountState = initialActiveLedgerAccountState;
1191
+ this.syscoinTransaction = this.createSyscoinTransactions();
1192
+ this.ethereumTransaction = new EthereumTransactions(this.getNetwork, this.getDecryptedPrivateKey, this.getAccountsState, this.ledgerSigner, this.trezorSigner, this.capturePsbtSigningContext, dependencies.captureEvmSubmission);
1193
+ if (dependencies.deviceUnlock) {
1194
+ const { port, receive } = dependencies.deviceUnlock;
1195
+ const readVaultPair = this.storage.readDeviceUnlockVaultPair;
1196
+ if (typeof readVaultPair !== 'function')
1197
+ throw new TypeError('Device unlock requires an atomic vault pair reader');
1198
+ receive(__classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_createDeviceUnlockControls).call(this, port, readVaultPair.bind(this.storage)));
1199
+ }
1200
+ }
1201
+ // Convenience method for complete setup after construction
1202
+ async initialize(seed, password, network) {
1203
+ // Set the network if provided (this is crucial for proper address derivation)
1204
+ if (network) {
1205
+ await this.setSignerNetwork(network);
1206
+ }
1207
+ // Use the new secure initialization method (eliminates temporary plaintext storage)
1208
+ const account = await this.initializeWalletSecurely(seed, password);
1209
+ // NOTE: Active account management is now handled by vault state/Redux
1210
+ // No need to explicitly set active account - it's managed externally
1211
+ return account;
1212
+ }
1213
+ /**
1214
+ * Returns canLogin:false only when the first vault authentication fails.
1215
+ * Storage, platform and session restoration failures reject so callers do
1216
+ * not charge a failed password attempt for an operational error.
1217
+ */
1218
+ async unlock(password) {
1219
+ let assertCurrent = __classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_captureSessionGuard).call(this);
1220
+ let authenticated = false;
1221
+ try {
1222
+ assertCurrent();
1223
+ const vaultKeys = await this.storage.get('vault-keys');
1224
+ if (!vaultKeys) {
1225
+ assertCurrent();
1226
+ throw new Error('Vault keys not found. Reload the wallet and try again.');
1227
+ }
1228
+ // v4: Validate password by deriving the session key once and decrypting the vault with it.
1229
+ // This avoids an extra KDF pass just for a stored verifier.
1230
+ const { salt } = vaultKeys;
1231
+ const readVaultPair = this.storage.readDeviceUnlockVaultPair;
1232
+ const accountingSalt = readVaultPair
1233
+ ? currentVaultSalt(vaultKeys)
1234
+ : undefined;
1235
+ // Permit only an authenticated negative verdict for the same atomic
1236
+ // pair after cancellation. Legacy migration and positive results stay guarded.
1237
+ if (accountingSalt === undefined)
1238
+ assertCurrent();
1239
+ // Derive session encryption key (in-memory only) used to encrypt/decrypt xprv fields
1240
+ // IMPORTANT: This MUST NOT be the same value as the stored verifier above.
1241
+ let sessionPasswordKey = await this.deriveStoredSessionKey(password, accountingSalt === undefined ? vaultKeys : { salt: accountingSalt });
1242
+ if (accountingSalt === undefined)
1243
+ assertCurrent();
1244
+ // Handle migration from old vault format with currentSessionSalt
1245
+ if (vaultKeys.currentSessionSalt) {
1246
+ console.log('[KeyringManager] Detected old vault format, handling session migration...');
1247
+ // The old format used currentSessionSalt for session data encryption
1248
+ // We need to use it temporarily to decrypt the mnemonic correctly
1249
+ const oldSessionPassword = this.encryptSHA512(password, vaultKeys.currentSessionSalt);
1250
+ // v3 CBC vaults use the raw password; migrated GCM vaults use the
1251
+ // derived key. Selecting by stored format also permits a safe retry
1252
+ // if the vault write succeeded but the metadata write was interrupted.
1253
+ const { mnemonic } = await this.vaultPersistence.getDecryptedVault(sessionPasswordKey, password);
1254
+ assertCurrent();
1255
+ authenticated = true;
1256
+ const isValidSecret = (value) => {
1257
+ if (typeof value !== 'string' || !value)
1258
+ return false;
1259
+ if (this.isSeedValid(value))
1260
+ return true;
1261
+ // Imported extended private keys can also be stored as the secret.
1262
+ if (!/^(xprv|yprv|zprv|tprv|uprv|vprv)/.test(value))
1263
+ return false;
1264
+ try {
1265
+ const decoded = bs58check.decode(value);
1266
+ return (decoded.length === 78 &&
1267
+ decoded[45] === 0 &&
1268
+ ecc.isPrivate(decoded.subarray(46)));
1269
+ }
1270
+ catch {
1271
+ return false;
1272
+ }
1273
+ };
1274
+ let decryptedMnemonic = mnemonic;
1275
+ if (!isValidSecret(decryptedMnemonic) && typeof mnemonic === 'string') {
1276
+ for (const legacyKey of [password, oldSessionPassword]) {
1277
+ try {
1278
+ const candidate = CryptoJS.AES.decrypt(mnemonic, legacyKey).toString(CryptoJS.enc.Utf8);
1279
+ if (isValidSecret(candidate)) {
1280
+ decryptedMnemonic = candidate;
1281
+ break;
1282
+ }
1283
+ }
1284
+ catch (error) {
1285
+ if (error?.message !== 'Malformed UTF-8 data')
1286
+ throw error;
1287
+ }
1288
+ }
1289
+ }
1290
+ if (!isValidSecret(decryptedMnemonic)) {
1291
+ throw new Error('Legacy vault secret could not be recovered. Existing vault preserved.');
1292
+ }
1293
+ // Do not change either stored record until a valid secret is recovered.
1294
+ await this.vaultPersistence.setEncryptedVault({ mnemonic: decryptedMnemonic }, sessionPasswordKey, undefined, assertCurrent);
1295
+ assertCurrent();
1296
+ console.log('[KeyringManager] Vault mnemonic format normalized');
1297
+ // Remove currentSessionSalt from vault-keys (v4 keeps only the salt on disk)
1298
+ const migratedVaultKeys = {
1299
+ salt: vaultKeys.salt,
1300
+ };
1301
+ assertCurrent();
1302
+ await this.storage.set('vault-keys', migratedVaultKeys);
1303
+ assertCurrent();
1304
+ console.log('[KeyringManager] Old vault format migration completed');
1305
+ }
1306
+ // v4 vault is encrypted with the derived session password key (PBKDF2 output).
1307
+ // If the password is wrong, this will throw and we return canLogin:false below.
1308
+ let decryptedVault;
1309
+ let legacyProfile = vaultKeys.keyDerivation === LEGACY_ENCRYPTION_KDF;
1310
+ try {
1311
+ decryptedVault = await this.vaultPersistence.getDecryptedVault(sessionPasswordKey);
1312
+ }
1313
+ catch (error) {
1314
+ const matchesRefusedVault = accountingSalt === undefined
1315
+ ? undefined
1316
+ : currentVaultRefusals.get(error);
1317
+ if (matchesRefusedVault && readVaultPair) {
1318
+ const pair = await readCurrentVaultPair(readVaultPair.bind(this.storage)).catch((readError) => {
1319
+ // A storage rejection cannot attest to password authentication.
1320
+ if (readError instanceof VaultAuthenticationError)
1321
+ throw new Error('Wallet unlock vault read failed');
1322
+ throw readError;
1323
+ });
1324
+ if (!pair ||
1325
+ !matchesRefusedVault(pair.vault) ||
1326
+ pair.salt !== accountingSalt)
1327
+ throw new Error('Wallet unlock vault changed');
1328
+ // This exact negative result has no session authority. Keep it
1329
+ // available for the host counter even if its owner just retired.
1330
+ return { canLogin: false };
1331
+ }
1332
+ assertCurrent();
1333
+ // Only the historical CBC fallback used the 20k key without metadata.
1334
+ // Never try it for GCM, malformed records, or platform/storage failures.
1335
+ const rawVault = error instanceof VaultAuthenticationError &&
1336
+ !vaultKeys.keyDerivation &&
1337
+ !vaultKeys.currentSessionSalt
1338
+ ? await this.storage.get('vault')
1339
+ : undefined;
1340
+ assertCurrent();
1341
+ if (typeof rawVault !== 'string' || rawVault.trim().startsWith('{')) {
1342
+ throw error;
1343
+ }
1344
+ const legacyKey = await this.deriveLegacySessionKey(password, salt);
1345
+ assertCurrent();
1346
+ decryptedVault = await this.vaultPersistence.getDecryptedVault(legacyKey);
1347
+ assertCurrent();
1348
+ authenticated = true;
1349
+ sessionPasswordKey = legacyKey;
1350
+ // Keep the historical account wrapping key. Persist its profile before
1351
+ // GCM rewrapping so either interrupted write remains recoverable.
1352
+ const migratedVaultKeys = {
1353
+ ...vaultKeys,
1354
+ keyDerivation: LEGACY_ENCRYPTION_KDF,
1355
+ };
1356
+ assertCurrent();
1357
+ await this.storage.set('vault-keys', migratedVaultKeys);
1358
+ assertCurrent();
1359
+ legacyProfile = true;
1360
+ }
1361
+ assertCurrent();
1362
+ authenticated = true;
1363
+ if (legacyProfile && this.hasWebCrypto()) {
1364
+ const storedVault = await this.storage.get('vault');
1365
+ assertCurrent();
1366
+ if (typeof storedVault === 'string' &&
1367
+ !storedVault.trim().startsWith('{')) {
1368
+ // A failed earlier rewrap can leave the authenticated profile plus
1369
+ // CBC. Retry that write without changing the account wrapping key.
1370
+ await this.vaultPersistence.setEncryptedVault(decryptedVault, sessionPasswordKey, undefined, assertCurrent);
1371
+ assertCurrent();
1372
+ }
1373
+ }
1374
+ // If session data missing or corrupted, recreate from vault
1375
+ if (!this.sessionMnemonic) {
1376
+ assertCurrent = await __classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_recreateSessionFromVault).call(this, sessionPasswordKey, assertCurrent);
1377
+ assertCurrent();
1378
+ console.log('[KeyringManager] Session data recreated from vault');
1379
+ }
1380
+ assertCurrent();
1381
+ const result = __classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_finishUnlock).call(this);
1382
+ assertCurrent();
1383
+ return result;
1384
+ }
1385
+ catch (error) {
1386
+ // A superseded attempt must not report authentication or clear its winner.
1387
+ assertCurrent();
1388
+ if (!authenticated && error instanceof VaultAuthenticationError) {
1389
+ return { canLogin: false };
1390
+ }
1391
+ if (authenticated) {
1392
+ // lockWallet clears secret buffers synchronously; hardware cleanup
1393
+ // is best effort and must not hide the original operational error.
1394
+ void this.lockWallet().catch(() => undefined);
1395
+ }
1396
+ throw error;
1397
+ }
1398
+ }
1399
+ assertUtxoDerivationAvailable(account, field) {
1400
+ if (account.isImported && account.xpub === account.address)
1401
+ throw new Error(`${field} not available for single-address imported accounts`);
1402
+ if (this.isWatchOnlyAccount(account))
1403
+ throw new Error(`${field} not available for watch-only accounts`);
1404
+ }
1405
+ isWatchOnlyAccount(a) {
1406
+ if (a.isLedgerWallet || a.isTrezorWallet)
1407
+ return false;
1408
+ if (!a.xprv || a.xprv === '') {
1409
+ if (a.xpub === a.address)
1410
+ return true; // single-address imported
1411
+ if (this.isXpubLike(a.xpub) || this.isDescriptor(a.xpub))
1412
+ return true; // xpub/descriptor watch-only
1413
+ }
1414
+ return false;
1415
+ }
1416
+ privateRecordSession() {
1417
+ const password = this.sessionPassword;
1418
+ const mnemonic = this.sessionMnemonic;
1419
+ const revision = this.privateRecordSessionRevision;
1420
+ const assertCurrent = () => {
1421
+ if (!password ||
1422
+ !mnemonic ||
1423
+ password.isCleared() ||
1424
+ mnemonic.isCleared() ||
1425
+ this.sessionPassword !== password ||
1426
+ this.sessionMnemonic !== mnemonic ||
1427
+ this.privateRecordSessionRevision !== revision) {
1428
+ throw new Error('Private record session changed');
1429
+ }
1430
+ };
1431
+ assertCurrent();
1432
+ const root = this.getDecryptedMnemonic().normalize('NFKD');
1433
+ if (!root || root.length > 4096)
1434
+ throw new Error('Invalid private record root');
1435
+ return { secret: new TextEncoder().encode(root), assertCurrent };
1436
+ }
1437
+ /** Stable local profile identifier; never an account address or a key. */
1438
+ async getPrivateRecordProfileId() {
1439
+ const session = this.privateRecordSession();
1440
+ try {
1441
+ const profile = await privateRecordProfileId(session.secret, session.assertCurrent);
1442
+ session.assertCurrent();
1443
+ return profile;
1444
+ }
1445
+ finally {
1446
+ session.secret.fill(0);
1447
+ }
1448
+ }
1449
+ /** Encrypt caller-owned bytes; persistence and encrypted backup belong to the caller. */
1450
+ async encryptPrivateRecord(params) {
1451
+ const session = this.privateRecordSession();
1452
+ try {
1453
+ const envelope = await encryptPrivateRecordBytes(session.secret, params, session.assertCurrent);
1454
+ session.assertCurrent();
1455
+ return envelope;
1456
+ }
1457
+ finally {
1458
+ session.secret.fill(0);
1459
+ }
1460
+ }
1461
+ async decryptPrivateRecord(params) {
1462
+ const session = this.privateRecordSession();
1463
+ let plaintext;
1464
+ try {
1465
+ plaintext = await decryptPrivateRecordBytes(session.secret, params, session.assertCurrent);
1466
+ session.assertCurrent();
1467
+ return plaintext;
1468
+ }
1469
+ catch (error) {
1470
+ plaintext?.fill(0);
1471
+ throw error;
1472
+ }
1473
+ finally {
1474
+ session.secret.fill(0);
1475
+ }
1476
+ }
1477
+ async importTrezorAccount(label) {
1478
+ const vault = this.getVault();
1479
+ const currency = vault.activeNetwork.currency;
1480
+ if (!currency) {
1481
+ throw new Error('Active network currency is not defined');
1482
+ }
1483
+ // Use getNextAccountId to filter out placeholder accounts
1484
+ const nextIndex = this.getNextAccountId(vault.accounts[KeyringAccountType.Trezor]);
1485
+ const importedAccount = await this._createTrezorAccount(currency, vault.activeNetwork.slip44, nextIndex);
1486
+ importedAccount.label = label ? label : `Trezor ${importedAccount.id + 1}`;
1487
+ // NOTE: Account creation should be dispatched to Redux store, not updated here
1488
+ // The calling code should handle the Redux dispatch
1489
+ // Return the created account for Pali to add to store
1490
+ return importedAccount;
1491
+ }
1492
+ async importLedgerAccount(label) {
1493
+ try {
1494
+ const vault = this.getVault();
1495
+ const currency = vault.activeNetwork.currency;
1496
+ if (!currency) {
1497
+ throw new Error('Active network currency is not defined');
1498
+ }
1499
+ // Use getNextAccountId to filter out placeholder accounts
1500
+ const nextIndex = this.getNextAccountId(vault.accounts[KeyringAccountType.Ledger]);
1501
+ const importedAccount = await this._createLedgerAccount(currency, vault.activeNetwork.slip44, nextIndex);
1502
+ importedAccount.label = label
1503
+ ? label
1504
+ : `Ledger ${importedAccount.id + 1}`;
1505
+ // NOTE: Account creation should be dispatched to Redux store, not updated here
1506
+ // The calling code should handle the Redux dispatch
1507
+ // Return the created account for Pali to add to store
1508
+ return importedAccount;
1509
+ }
1510
+ catch (error) {
1511
+ console.log({ error });
1512
+ throw error;
1513
+ }
1514
+ }
1515
+ loadBip84AddressAccount(xpub, network) {
1516
+ const { types, networks } = getNetworkConfig(network.slip44, network.currency);
1517
+ if (!types?.zPubType)
1518
+ throw new Error('Missing zPubType in network configuration');
1519
+ return new BIP84.fromZPub(xpub, types.zPubType, networks);
1520
+ }
1521
+ deriveFirstUtxoAddress(xpub, network) {
1522
+ if (this.isDescriptor(xpub))
1523
+ return deriveDescriptorAddress(xpub, network);
1524
+ return this.loadBip84AddressAccount(xpub, network).getAddress(0, false, 84);
1525
+ }
1526
+ async importAccount(privKey, label, options) {
1527
+ // Check if wallet is unlocked
1528
+ if (!this.isUnlocked()) {
1529
+ throw new Error('Wallet must be unlocked to import accounts');
1530
+ }
1531
+ const importedAccount = await this._getPrivateKeyAccountInfos(privKey, label, options);
1532
+ // NOTE: Account creation should be dispatched to Redux store, not updated here
1533
+ // The calling code should handle the Redux dispatch
1534
+ // Return the created account for Pali to add to store
1535
+ return importedAccount;
1536
+ }
1537
+ // Import a WIF on UTXO networks and force legacy P2PKH address/type.
1538
+ // Keeps it as a single-address Imported account, storing the address in both xpub and address.
1539
+ async importWatchOnly(identifier, label) {
1540
+ // Validate via Blockbook and create a watch-only Imported account
1541
+ const vault = this.getVault();
1542
+ const { accounts, activeNetwork } = vault;
1543
+ if (!identifier || typeof identifier !== 'string') {
1544
+ throw new Error('Identifier is required');
1545
+ }
1546
+ const isXpub = this.isXpubLike(identifier);
1547
+ const isDesc = this.isDescriptor(identifier);
1548
+ const isAddress = !isXpub && !isDesc;
1549
+ // Compute address field
1550
+ let addressToStore = identifier;
1551
+ if (isDesc) {
1552
+ // Keep the descriptor as the history reference, never as a recipient.
1553
+ // Backend-provided address metadata must not select the deposit address.
1554
+ addressToStore = deriveDescriptorAddress(identifier, activeNetwork);
1555
+ }
1556
+ else if (isXpub) {
1557
+ // Minimal approach: treat any extended pubkey as BIP84 by converting to zpub/vpub
1558
+ const { types } = getNetworkConfig(activeNetwork.slip44, activeNetwork.currency || 'Syscoin');
1559
+ const target = activeNetwork.slip44 === 1
1560
+ ? types.zPubType.testnet.vpub
1561
+ : types.zPubType.mainnet.zpub;
1562
+ const bip84Key = convertExtendedKeyVersion(identifier, target);
1563
+ addressToStore = this.deriveFirstUtxoAddress(bip84Key, activeNetwork);
1564
+ }
1565
+ // Validate duplicates across all account buckets, including passkey smart accounts.
1566
+ if (this.accountAddressExists(accounts, addressToStore) ||
1567
+ ((isXpub || isDesc) &&
1568
+ Object.values(accounts || {}).some((bucket) => Object.values(bucket || {}).some((account) => account?.xpub === identifier)))) {
1569
+ throw new Error('Account already exists on your Wallet.');
1570
+ }
1571
+ // Confirm with Blockbook
1572
+ const options = 'details=basic';
1573
+ // Validate against Blockbook directly to capture precise error details (e.g., checksum mismatch)
1574
+ const baseUrl = activeNetwork.url.replace(/\/$/, '');
1575
+ const path = isXpub || isDesc ? '/api/v2/xpub/' : '/api/v2/address/';
1576
+ const url = `${baseUrl}${path}${encodeURIComponent(identifier)}?${options}`;
1577
+ let res = null;
1578
+ try {
1579
+ const response = await fetch(url);
1580
+ if (!response.ok) {
1581
+ let errorText = response.statusText || 'Request failed';
1582
+ try {
1583
+ const bodyText = await response.text();
1584
+ if (bodyText) {
1585
+ try {
1586
+ const json = JSON.parse(bodyText);
1587
+ if (json && json.error) {
1588
+ errorText = json.error;
1589
+ }
1590
+ else {
1591
+ errorText = bodyText;
1592
+ }
1593
+ }
1594
+ catch {
1595
+ errorText = bodyText;
1596
+ }
1597
+ }
1598
+ }
1599
+ catch (parseError) {
1600
+ // ignore body parse error; fall back to statusText
1601
+ }
1602
+ throw new Error(errorText);
1603
+ }
1604
+ res = await response.json();
1605
+ }
1606
+ catch (e) {
1607
+ throw new Error(e?.message || 'Identifier validation failed');
1608
+ }
1609
+ if (!res) {
1610
+ throw new Error('Identifier not found on the active network');
1611
+ }
1612
+ const id = this.getNextAccountId(accounts[KeyringAccountType.Imported]);
1613
+ const defaultLabel = label || `Watch-only ${id + 1}`;
1614
+ const balances = { syscoin: 0, ethereum: 0 };
1615
+ const watchOnlyAccount = {
1616
+ ...initialActiveImportedAccountState,
1617
+ address: addressToStore,
1618
+ label: defaultLabel,
1619
+ id,
1620
+ balances,
1621
+ isImported: true,
1622
+ xprv: '',
1623
+ xpub: isAddress ? addressToStore : identifier,
1624
+ assets: {
1625
+ syscoin: [],
1626
+ ethereum: [],
1627
+ },
1628
+ };
1629
+ return watchOnlyAccount;
1630
+ }
1631
+ validateZprv(zprv, targetNetwork) {
1632
+ // Use the active network if targetNetwork is not provided
1633
+ const networkToValidateAgainst = targetNetwork || this.getVault().activeNetwork;
1634
+ if (!networkToValidateAgainst) {
1635
+ throw new Error('No network available for validation');
1636
+ }
1637
+ try {
1638
+ // Check if it looks like an extended key based on known prefixes
1639
+ const knownExtendedKeyPrefixes = [
1640
+ 'xprv',
1641
+ 'xpub',
1642
+ 'yprv',
1643
+ 'ypub',
1644
+ 'zprv',
1645
+ 'zpub',
1646
+ 'tprv',
1647
+ 'tpub',
1648
+ 'uprv',
1649
+ 'upub',
1650
+ 'vprv',
1651
+ 'vpub',
1652
+ ];
1653
+ const prefix = zprv.substring(0, 4);
1654
+ const looksLikeExtendedKey = knownExtendedKeyPrefixes.includes(prefix);
1655
+ // Only check prefix validity if it looks like an extended key
1656
+ if (looksLikeExtendedKey) {
1657
+ const validBip84Prefixes = ['zprv', 'vprv']; // zprv for mainnet, vprv for testnet
1658
+ if (!validBip84Prefixes.includes(prefix)) {
1659
+ throw new Error(`Invalid key prefix '${prefix}'. Only BIP84 keys (zprv/vprv) are supported for UTXO imports. BIP44 keys (xprv/tprv) are not supported.`);
1660
+ }
1661
+ }
1662
+ else {
1663
+ // Not an extended key format
1664
+ throw new Error('Not an extended private key');
1665
+ }
1666
+ const bip32 = BIP32Factory(ecc);
1667
+ const decoded = bs58check.decode(zprv);
1668
+ if (decoded.length !== 78) {
1669
+ throw new Error('Invalid length for a BIP-32 key');
1670
+ }
1671
+ // Get network configuration for the target network
1672
+ const { networks, types } = getNetworkConfig(networkToValidateAgainst.slip44, networkToValidateAgainst.currency || 'Bitcoin');
1673
+ // For BIP84 (zprv/zpub), we need to use the correct magic bytes from zPubType
1674
+ // Determine key type: zprv = mainnet key, vprv = testnet key
1675
+ const keyIsTestnet = prefix === 'vprv';
1676
+ // Determine target network type: testnet networks typically have slip44 1
1677
+ const targetIsTestnet = networkToValidateAgainst.slip44 === 1;
1678
+ // Cross-network validation: reject if key type doesn't match target network
1679
+ if (keyIsTestnet && !targetIsTestnet) {
1680
+ throw new Error(`Extended private key is not compatible with ${networkToValidateAgainst.label}. ` +
1681
+ `This appears to be a testnet key (${prefix}) but the target network is mainnet.`);
1682
+ }
1683
+ if (!keyIsTestnet && targetIsTestnet) {
1684
+ throw new Error(`Extended private key is not compatible with ${networkToValidateAgainst.label}. ` +
1685
+ `This appears to be a mainnet key (${prefix}) but the target network is testnet.`);
1686
+ }
1687
+ const pubTypes = keyIsTestnet
1688
+ ? types.zPubType.testnet
1689
+ : types.zPubType.mainnet;
1690
+ const baseNetwork = keyIsTestnet ? networks.testnet : networks.mainnet;
1691
+ const network = {
1692
+ ...baseNetwork,
1693
+ bip32: {
1694
+ public: parseInt(pubTypes.vpub || pubTypes.zpub, 16),
1695
+ private: parseInt(pubTypes.vprv || pubTypes.zprv, 16),
1696
+ },
1697
+ };
1698
+ // Validate that the key prefix matches the expected network format
1699
+ // This ensures the key was generated for a compatible network
1700
+ const expectedPrefixes = ['zprv', 'vprv', 'xprv', 'yprv']; // Accept various BIP32/84 formats
1701
+ if (!expectedPrefixes.includes(prefix)) {
1702
+ throw new Error(`Invalid extended private key prefix: ${prefix}. Expected one of: ${expectedPrefixes.join(', ')}`);
1703
+ }
1704
+ // Strict network matching - only allow keys that match the target network
1705
+ let node;
1706
+ try {
1707
+ node = bip32.fromBase58(zprv, network);
1708
+ }
1709
+ catch (e) {
1710
+ throw new Error(`Extended private key is not compatible with ${networkToValidateAgainst.label}. Please use a key generated for this specific network.`);
1711
+ }
1712
+ if (!node.privateKey) {
1713
+ throw new Error('Private key not found in extended private key');
1714
+ }
1715
+ if (!ecc.isPrivate(node.privateKey)) {
1716
+ throw new Error('Invalid private key for secp256k1 curve');
1717
+ }
1718
+ return {
1719
+ isValid: true,
1720
+ node,
1721
+ network,
1722
+ message: 'The extended private key is valid for this network.',
1723
+ };
1724
+ }
1725
+ catch (error) {
1726
+ return { isValid: false, message: error.message };
1727
+ }
1728
+ }
1729
+ validateWif(wif, targetNetwork) {
1730
+ // Use the active network if targetNetwork is not provided
1731
+ const networkToValidateAgainst = targetNetwork || this.getVault().activeNetwork;
1732
+ if (!networkToValidateAgainst) {
1733
+ throw new Error('No network available for validation');
1734
+ }
1735
+ try {
1736
+ // Get bitcoinjs network for the target network (Syscoin/BTC etc.)
1737
+ const { networks } = getNetworkConfig(networkToValidateAgainst.slip44, networkToValidateAgainst.currency || 'Bitcoin');
1738
+ const isTestnet = networkToValidateAgainst.slip44 === 1;
1739
+ const bitcoinNetwork = isTestnet ? networks.testnet : networks.mainnet;
1740
+ // Try to parse the WIF for the given network
1741
+ const keyPair = syscoinjs.utils.bitcoinjs.ECPair.fromWIF(wif, bitcoinNetwork);
1742
+ if (!keyPair || !keyPair.privateKey)
1743
+ throw new Error('Invalid WIF private key');
1744
+ // Derive an address based on network capability
1745
+ // Try different address types to ensure the key can derive addresses
1746
+ let address;
1747
+ try {
1748
+ // Try P2TR (Taproot) first if bech32 is supported
1749
+ if (bitcoinNetwork.bech32) {
1750
+ try {
1751
+ address = bjs.payments.p2wpkh({
1752
+ pubkey: keyPair.publicKey,
1753
+ network: bitcoinNetwork,
1754
+ }).address;
1755
+ }
1756
+ catch (e) {
1757
+ // P2TR might not be supported, try P2WPKH
1758
+ }
1759
+ }
1760
+ if (!address) {
1761
+ address = bjs.payments.p2pkh({
1762
+ pubkey: keyPair.publicKey,
1763
+ network: bitcoinNetwork,
1764
+ }).address;
1765
+ }
1766
+ }
1767
+ catch (e) {
1768
+ // ignore and handle below
1769
+ }
1770
+ if (!address)
1771
+ throw new Error('Failed to derive address from WIF');
1772
+ return { isValid: true };
1773
+ }
1774
+ catch (error) {
1775
+ return { isValid: false, message: error.message };
1776
+ }
1777
+ }
1778
+ /** Fixed-path derivation for an authenticated software host; no persistence. */
1779
+ async deriveSoftwareEvmHdRuntimeAccount(input, options) {
1780
+ // Capture the session and selection before inspecting caller-owned objects.
1781
+ // Hydration may have no account records yet; account existence is not a guard.
1782
+ const session = this.sessionPassword;
1783
+ const mnemonic = this.sessionMnemonic;
1784
+ const revision = this.privateRecordSessionRevision;
1785
+ const vault = this.getVault();
1786
+ const selected = { ...vault.activeAccount };
1787
+ const network = { ...vault.activeNetwork };
1788
+ const assertCurrent = () => {
1789
+ const current = this.getVault();
1790
+ if (!session ||
1791
+ session.isCleared() ||
1792
+ this.sessionPassword !== session ||
1793
+ !mnemonic ||
1794
+ mnemonic.isCleared() ||
1795
+ this.sessionMnemonic !== mnemonic ||
1796
+ this.privateRecordSessionRevision !== revision ||
1797
+ network.kind !== INetworkType.Ethereum ||
1798
+ network.slip44 !== 60 ||
1799
+ !Number.isSafeInteger(network.chainId) ||
1800
+ network.chainId <= 0 ||
1801
+ typeof network.url !== 'string' ||
1802
+ !network.url ||
1803
+ current.activeAccount.id !== selected.id ||
1804
+ current.activeAccount.type !== selected.type ||
1805
+ current.activeNetwork.kind !== network.kind ||
1806
+ current.activeNetwork.chainId !== network.chainId ||
1807
+ current.activeNetwork.slip44 !== network.slip44 ||
1808
+ current.activeNetwork.url !== network.url)
1809
+ throw new Error('Ethereum HD derivation context changed or is unsupported');
1810
+ };
1811
+ assertCurrent();
1812
+ const index = snapshotEvmHdIndex(input);
1813
+ const hostGuard = preparedEvmGuard(options);
1814
+ hostGuard();
1815
+ assertCurrent();
1816
+ const account = await this.setDerivedWeb3Accounts(index, `Account ${index + 1}`);
1817
+ hostGuard();
1818
+ assertCurrent();
1819
+ return account;
1820
+ }
1821
+ /** The software facade owns this public capability; no device fallback. */
1822
+ signPreparedSoftwareEvmTransaction(input, options) {
1823
+ // Capture authority before parsing caller data or invoking host callbacks.
1824
+ const assertSession = this.capturePsbtSigningContext();
1825
+ const vault = this.getVault();
1826
+ const selected = { ...vault.activeAccount };
1827
+ const selectedAccount = vault.accounts[selected.type]?.[selected.id];
1828
+ const encryptedKey = selectedAccount?.xprv;
1829
+ const request = snapshotPreparedEvmRequest(input);
1830
+ const hostGuard = preparedEvmGuard(options);
1831
+ const assertCurrent = () => {
1832
+ assertSession();
1833
+ const current = this.getVault();
1834
+ const account = current.accounts[selected.type]?.[selected.id];
1835
+ if ((selected.type !== KeyringAccountType.HDAccount &&
1836
+ selected.type !== KeyringAccountType.Imported) ||
1837
+ !account ||
1838
+ account.id !== selected.id ||
1839
+ account.isLedgerWallet ||
1840
+ account.isTrezorWallet ||
1841
+ account.isSmartAccount ||
1842
+ typeof encryptedKey !== 'string' ||
1843
+ encryptedKey.length === 0 ||
1844
+ account.xprv !== encryptedKey ||
1845
+ request.account.type !== selected.type ||
1846
+ request.account.id !== selected.id ||
1847
+ request.account.expectedAddress !== getAddress(account.address) ||
1848
+ current.activeNetwork.kind !== INetworkType.Ethereum ||
1849
+ current.activeNetwork.slip44 !== 60 ||
1850
+ !Number.isSafeInteger(current.activeNetwork.chainId) ||
1851
+ current.activeNetwork.chainId <= 0 ||
1852
+ BigInt(current.activeNetwork.chainId).toString() !==
1853
+ request.network.chainId ||
1854
+ current.activeNetwork.url !== request.network.url)
1855
+ throw new Error('Prepared EVM signing context changed or is unsupported');
1856
+ };
1857
+ assertCurrent();
1858
+ hostGuard();
1859
+ assertCurrent();
1860
+ const key = this.getDecryptedPrivateKey();
1861
+ if (getAddress(key.address) !== request.account.expectedAddress)
1862
+ throw new Error('Prepared EVM signer address mismatch');
1863
+ assertCurrent();
1864
+ const { signed } = signPreparedEvmEnvelope(request.transaction, key.decryptedPrivateKey);
1865
+ hostGuard();
1866
+ assertCurrent();
1867
+ return signed;
1868
+ }
1869
+ writeUtf8Diagnostic(hasUtf8Error) {
1870
+ try {
1871
+ void Promise.resolve(this.storage.set('utf8Error', { hasUtf8Error })).catch(() => undefined);
1872
+ }
1873
+ catch {
1874
+ // This diagnostic must not interrupt authentication or become an
1875
+ // unhandled rejection when an asynchronous storage client is offline.
1876
+ }
1877
+ }
1878
+ validateAndHandleErrorByMessage(message) {
1879
+ const utf8ErrorMessage = 'Malformed UTF-8 data';
1880
+ if (message.includes(utf8ErrorMessage) ||
1881
+ message.toLowerCase().includes(utf8ErrorMessage.toLowerCase())) {
1882
+ this.writeUtf8Diagnostic(true);
1883
+ }
1884
+ }
1885
+ async pbkdf2WebCryptoHex(params) {
1886
+ const subtle = globalThis.crypto.subtle;
1887
+ const enc = new TextEncoder();
1888
+ const passBytes = enc.encode(params.password);
1889
+ const saltBytes = this.hexToBytes(params.saltHex);
1890
+ const keyMaterial = await subtle.importKey('raw', passBytes, { name: 'PBKDF2' }, false, ['deriveBits']);
1891
+ const bits = await subtle.deriveBits({
1892
+ name: 'PBKDF2',
1893
+ salt: saltBytes,
1894
+ iterations: params.iterations,
1895
+ hash: params.hash,
1896
+ }, keyMaterial, params.lengthBytes * 8);
1897
+ return this.bytesToHex(bits);
1898
+ }
1899
+ async encryptSHA512Async(password, salt) {
1900
+ if (this.hasWebCrypto()) {
1901
+ // Match CryptoJS PBKDF2 output format: hex string
1902
+ return await this.pbkdf2WebCryptoHex({
1903
+ password,
1904
+ saltHex: salt,
1905
+ iterations: this.getEncIterations(),
1906
+ lengthBytes: 32, // 256-bit
1907
+ hash: 'SHA-512',
1908
+ });
1909
+ }
1910
+ throw new Error('WebCrypto is required for vault key derivation');
1911
+ }
1912
+ async deriveLegacySessionKey(password, salt) {
1913
+ if (this.hasWebCrypto()) {
1914
+ return this.pbkdf2WebCryptoHex({
1915
+ password,
1916
+ saltHex: salt,
1917
+ iterations: 20000,
1918
+ lengthBytes: 32,
1919
+ hash: 'SHA-512',
1920
+ });
1921
+ }
1922
+ // Read compatibility only; new vault writes still require WebCrypto.
1923
+ return CryptoJS.PBKDF2(password, CryptoJS.enc.Hex.parse(salt), {
1924
+ keySize: 256 / 32,
1925
+ iterations: 20000,
1926
+ hasher: CryptoJS.algo.SHA512,
1927
+ }).toString();
1928
+ }
1929
+ async deriveStoredSessionKey(password, vaultKeys) {
1930
+ if (vaultKeys.keyDerivation === LEGACY_ENCRYPTION_KDF) {
1931
+ return this.deriveLegacySessionKey(password, vaultKeys.salt);
1932
+ }
1933
+ if (vaultKeys.keyDerivation !== undefined) {
1934
+ throw new Error('Unsupported vault key derivation profile');
1935
+ }
1936
+ return this.encryptSHA512Async(password, vaultKeys.salt);
1937
+ }
1938
+ async _createTrezorAccount(coin, slip44, index, label) {
1939
+ const vault = this.getVault();
1940
+ const { accounts } = vault;
1941
+ // For EVM networks, Trezor expects 'eth' regardless of the network's currency
1942
+ const trezorCoin = slip44 === 60 ? 'eth' : coin;
1943
+ const { descriptor, balance: _balance } = await this.requireTrezorSigner().getAccountInfo({
1944
+ coin: trezorCoin,
1945
+ slip44,
1946
+ index,
1947
+ });
1948
+ const xpub = descriptor;
1949
+ const balance = _balance;
1950
+ let ethPubKey = '';
1951
+ const isEVM = isEvmCoin(coin, slip44);
1952
+ // For EVM networks, we need to get the actual address from Trezor
1953
+ let address;
1954
+ if (isEVM) {
1955
+ // For EVM, the descriptor from getAccountInfo is the address
1956
+ address = xpub;
1957
+ // Get the public key for EVM
1958
+ const response = await this.requireTrezorSigner().getPublicKey({
1959
+ coin: trezorCoin,
1960
+ slip44,
1961
+ index: +index,
1962
+ });
1963
+ ethPubKey = response.publicKey;
1964
+ }
1965
+ else {
1966
+ // For UTXO, use the xpub to derive the address
1967
+ // Always use first receive address (index 0) for imported account display
1968
+ address = this.deriveFirstUtxoAddress(xpub, vault.activeNetwork);
1969
+ }
1970
+ if (this.accountAddressExists(accounts, address, isEVM))
1971
+ throw new Error('Account already exists on your Wallet.');
1972
+ if (!xpub || !address)
1973
+ throw new Error('Something wrong happened. Please, try again or report it');
1974
+ // Use getNextAccountId to properly handle placeholder accounts
1975
+ const id = this.getNextAccountId(accounts[KeyringAccountType.Trezor]);
1976
+ // Convert balance from satoshis to SYS safely
1977
+ // Using string manipulation to avoid precision loss
1978
+ let syscoinBalance = 0;
1979
+ if (!isEVM && balance) {
1980
+ const balanceStr = balance.toString();
1981
+ // Handle conversion without division to preserve precision
1982
+ if (balanceStr.length > 8) {
1983
+ // Has whole SYS part
1984
+ const wholePart = balanceStr.slice(0, -8);
1985
+ const decimalPart = balanceStr.slice(-8);
1986
+ syscoinBalance = parseFloat(`${wholePart}.${decimalPart}`);
1987
+ }
1988
+ else {
1989
+ // Less than 1 SYS
1990
+ const paddedBalance = balanceStr.padStart(8, '0');
1991
+ syscoinBalance = parseFloat(`0.${paddedBalance}`);
1992
+ }
1993
+ }
1994
+ const trezorAccount = {
1995
+ ...this.initialTrezorAccountState,
1996
+ balances: {
1997
+ syscoin: isEVM ? 0 : syscoinBalance,
1998
+ ethereum: 0,
1999
+ },
2000
+ address,
2001
+ label: label ? label : `Trezor ${id + 1}`,
2002
+ id,
2003
+ xprv: '',
2004
+ xpub: isEVM ? ethPubKey : xpub,
2005
+ assets: {
2006
+ syscoin: [],
2007
+ ethereum: [],
2008
+ },
2009
+ };
2010
+ return trezorAccount;
2011
+ }
2012
+ async _createLedgerAccount(coin, slip44, index, label) {
2013
+ const vault = this.getVault();
2014
+ const { accounts } = vault;
2015
+ let xpub;
2016
+ let address = '';
2017
+ if (isEvmCoin(coin, slip44)) {
2018
+ const { address: ethAddress, publicKey } = await this.requireLedgerSigner().evm.getEvmAddressAndPubKey({
2019
+ accountIndex: index,
2020
+ });
2021
+ address = ethAddress;
2022
+ xpub = publicKey;
2023
+ }
2024
+ else {
2025
+ const ledgerXpub = await this.requireLedgerSigner().utxo.getXpub({
2026
+ index: index,
2027
+ coin,
2028
+ slip44,
2029
+ });
2030
+ // Convert device-returned extended key to BIP84 zpub/vpub for storage/backend usage
2031
+ const { types } = getNetworkConfig(slip44, coin);
2032
+ const bip84Target = slip44 === 1
2033
+ ? types.zPubType.testnet.vpub
2034
+ : types.zPubType.mainnet.zpub;
2035
+ xpub = convertExtendedKeyVersion(ledgerXpub, bip84Target);
2036
+ // Always use first receive address (index 0) for imported account display
2037
+ address = this.deriveFirstUtxoAddress(xpub, vault.activeNetwork);
2038
+ }
2039
+ if (this.accountAddressExists(accounts, address, isEvmCoin(coin, slip44)))
2040
+ throw new Error('Account already exists on your Wallet.');
2041
+ if (!xpub || !address)
2042
+ throw new Error('Something wrong happened. Please, try again or report it');
2043
+ // Use getNextAccountId to properly handle placeholder accounts
2044
+ const id = this.getNextAccountId(accounts[KeyringAccountType.Ledger]);
2045
+ const currentBalances = { syscoin: 0, ethereum: 0 };
2046
+ const ledgerAccount = {
2047
+ ...this.initialLedgerAccountState,
2048
+ balances: currentBalances,
2049
+ address,
2050
+ label: label ? label : `Ledger ${id + 1}`,
2051
+ id,
2052
+ xprv: '',
2053
+ xpub,
2054
+ assets: {
2055
+ syscoin: [],
2056
+ ethereum: [],
2057
+ },
2058
+ };
2059
+ return ledgerAccount;
2060
+ }
2061
+ // Common helper method for UTXO account creation
2062
+ async createUTXOAccountAtIndex(accountId, label) {
2063
+ try {
2064
+ // Create fresh signer just for this account creation operation
2065
+ const freshHDSigner = this.createOnDemandUTXOSigner(accountId);
2066
+ const sysAccount = await this.getFormattedBackendAccount({
2067
+ signer: freshHDSigner,
2068
+ });
2069
+ const encryptedXprv = this.getEncryptedXprv(freshHDSigner);
2070
+ // Generate network-aware label if none provided
2071
+ const vault = this.getVault();
2072
+ const network = vault.activeNetwork;
2073
+ const defaultLabel = this.generateNetworkAwareLabel(accountId, network);
2074
+ return {
2075
+ ...this.getInitialAccountData({
2076
+ label: label || defaultLabel,
2077
+ signer: freshHDSigner,
2078
+ sysAccount,
2079
+ xprv: encryptedXprv,
2080
+ }),
2081
+ balances: { syscoin: 0, ethereum: 0 },
2082
+ };
2083
+ }
2084
+ catch (error) {
2085
+ console.log('ERROR createUTXOAccountAtIndex', {
2086
+ error,
2087
+ });
2088
+ this.validateAndHandleErrorByMessage(error.message);
2089
+ throw error;
2090
+ }
2091
+ }
2092
+ async addNewAccountToSyscoinChain(label) {
2093
+ try {
2094
+ // Get next available account ID
2095
+ const vault = this.getVault();
2096
+ const accounts = vault.accounts[KeyringAccountType.HDAccount];
2097
+ const nextId = this.getNextAccountId(accounts);
2098
+ return await this.createUTXOAccountAtIndex(nextId, label);
2099
+ }
2100
+ catch (error) {
2101
+ console.log('ERROR addNewAccountToSyscoinChain', {
2102
+ error,
2103
+ });
2104
+ this.validateAndHandleErrorByMessage(error.message);
2105
+ throw error;
2106
+ }
2107
+ }
2108
+ async addNewAccountToEth(label) {
2109
+ try {
2110
+ // Get next available account ID
2111
+ const vault = this.getVault();
2112
+ const accounts = vault.accounts[KeyringAccountType.HDAccount];
2113
+ const nextId = this.getNextAccountId(accounts);
2114
+ // EVM accounts should use generic labels since they work across all EVM networks
2115
+ const defaultLabel = `Account ${nextId + 1}`;
2116
+ const newAccount = await this.setDerivedWeb3Accounts(nextId, label || defaultLabel);
2117
+ return newAccount;
2118
+ }
2119
+ catch (error) {
2120
+ console.log('ERROR addNewAccountToEth', {
2121
+ error,
2122
+ });
2123
+ this.validateAndHandleErrorByMessage(error.message);
2124
+ throw error;
2125
+ }
2126
+ }
2127
+ // Helper method to get next available account ID - fills gaps when accounts are deleted
2128
+ getNextAccountId(accounts) {
2129
+ const existingIds = Object.values(accounts)
2130
+ .filter((account) => {
2131
+ // Only count accounts that have been properly initialized
2132
+ // Placeholder accounts have empty addresses/xprv/xpub
2133
+ return account && account.address && account.xpub;
2134
+ })
2135
+ .map((account) => account.id)
2136
+ .filter((id) => !isNaN(id))
2137
+ .sort((a, b) => a - b); // Sort to find gaps efficiently
2138
+ if (existingIds.length === 0) {
2139
+ return 0;
2140
+ }
2141
+ // Find the first gap in the sequence
2142
+ for (let i = 0; i < existingIds.length; i++) {
2143
+ if (existingIds[i] !== i) {
2144
+ // Found a gap at position i
2145
+ return i;
2146
+ }
2147
+ }
2148
+ // No gaps found, return next sequential ID
2149
+ return existingIds.length;
2150
+ }
2151
+ async _getPrivateKeyAccountInfos(privKey, label, options) {
2152
+ const vault = this.getVault();
2153
+ const { accounts } = vault;
2154
+ let importedAccountValue = null;
2155
+ const balances = {
2156
+ syscoin: 0,
2157
+ ethereum: 0,
2158
+ };
2159
+ // Try to validate as extended private key first
2160
+ const networkToUse = vault.activeNetwork;
2161
+ const zprvValidation = this.validateZprv(privKey, networkToUse);
2162
+ // Check if we're on an EVM network (slip44 = 60) or UTXO network
2163
+ const isEvmNetwork = networkToUse.slip44 === 60;
2164
+ if (zprvValidation.isValid) {
2165
+ // This is a valid UTXO extended private key
2166
+ if (isEvmNetwork) {
2167
+ throw new Error('Cannot import UTXO private key on EVM network. Please switch to a UTXO network (Bitcoin/Syscoin) first.');
2168
+ }
2169
+ const { node, network } = zprvValidation;
2170
+ if (!node || !network) {
2171
+ throw new Error('Failed to validate extended private key');
2172
+ }
2173
+ // Always use index 0 for consistency
2174
+ const nodeChild = node.derivePath(`0/0`);
2175
+ if (!nodeChild) {
2176
+ throw new Error('Failed to derive child node');
2177
+ }
2178
+ // Choose address type based on options or default to p2wpkh
2179
+ let addrObj;
2180
+ if (options?.utxoAddressType === 'p2pkh') {
2181
+ addrObj = bjs.payments.p2pkh({
2182
+ pubkey: nodeChild.publicKey,
2183
+ network,
2184
+ });
2185
+ }
2186
+ else if (options?.utxoAddressType === 'p2tr') {
2187
+ // For taproot, use x-only public key (32 bytes instead of 33)
2188
+ const xOnly = nodeChild.publicKey.length === 33
2189
+ ? nodeChild.publicKey.slice(1, 33)
2190
+ : nodeChild.publicKey;
2191
+ addrObj = bjs.payments.p2tr({
2192
+ internalPubkey: xOnly,
2193
+ network,
2194
+ });
2195
+ }
2196
+ else {
2197
+ // Default to SegWit (p2wpkh)
2198
+ addrObj = bjs.payments.p2wpkh({
2199
+ pubkey: nodeChild.publicKey,
2200
+ network,
2201
+ });
2202
+ }
2203
+ const { address } = addrObj;
2204
+ if (!address) {
2205
+ throw new Error('Failed to generate address');
2206
+ }
2207
+ importedAccountValue = {
2208
+ address,
2209
+ // IMPORTANT: Store BIP84 extended *public* key (zpub/vpub), not device-style xpub/tpub.
2210
+ // Blockbook derives different address branches depending on version bytes.
2211
+ // Using zpub/vpub ensures UTXO discovery and `input.path` match BIP84 (m/84'/slip44'/...),
2212
+ // which is required for HD signing.
2213
+ publicKey: (() => {
2214
+ try {
2215
+ const { types } = getNetworkConfig(networkToUse.slip44, networkToUse.currency || 'Bitcoin');
2216
+ const target = networkToUse.slip44 === 1
2217
+ ? types.zPubType.testnet.vpub
2218
+ : types.zPubType.mainnet.zpub;
2219
+ return convertExtendedKeyVersion(node.neutered().toBase58(), target);
2220
+ }
2221
+ catch (_e) {
2222
+ // Fall back to the default neutered key if conversion fails.
2223
+ return node.neutered().toBase58();
2224
+ }
2225
+ })(),
2226
+ privateKey: privKey,
2227
+ };
2228
+ balances.syscoin = 0;
2229
+ }
2230
+ else {
2231
+ // If not a valid zprv/vprv, on UTXO networks try WIF
2232
+ const isEvmNetwork = networkToUse.slip44 === 60;
2233
+ let handledAsUtxo = false;
2234
+ if (!isEvmNetwork) {
2235
+ const wifValidation = this.validateWif(privKey, networkToUse);
2236
+ if (wifValidation.isValid) {
2237
+ // Create address from WIF and treat as single-address imported account
2238
+ const { networks } = getNetworkConfig(networkToUse.slip44, networkToUse.currency || 'Bitcoin');
2239
+ const isTestnet = networkToUse.slip44 === 1;
2240
+ const bitcoinNetwork = isTestnet
2241
+ ? networks.testnet
2242
+ : networks.mainnet;
2243
+ const keyPair = syscoinjs.utils.bitcoinjs.ECPair.fromWIF(privKey, bitcoinNetwork);
2244
+ // Choose address type based on options or default behavior
2245
+ let addrObj;
2246
+ if (options?.utxoAddressType === 'p2pkh') {
2247
+ addrObj = bjs.payments.p2pkh({
2248
+ pubkey: keyPair.publicKey,
2249
+ network: bitcoinNetwork,
2250
+ });
2251
+ }
2252
+ else if (options?.utxoAddressType === 'p2tr') {
2253
+ // For taproot, use x-only public key (32 bytes instead of 33)
2254
+ const xOnly = keyPair.publicKey.length === 33
2255
+ ? keyPair.publicKey.slice(1, 33)
2256
+ : keyPair.publicKey;
2257
+ addrObj = bjs.payments.p2tr({
2258
+ internalPubkey: xOnly,
2259
+ network: bitcoinNetwork,
2260
+ });
2261
+ }
2262
+ else {
2263
+ // Default to SegWit (p2wpkh)
2264
+ addrObj = bjs.payments.p2wpkh({
2265
+ pubkey: keyPair.publicKey,
2266
+ network: bitcoinNetwork,
2267
+ });
2268
+ }
2269
+ const address = addrObj.address;
2270
+ if (!address) {
2271
+ throw new Error('Failed to generate address from WIF');
2272
+ }
2273
+ importedAccountValue = {
2274
+ address,
2275
+ // For single-address WIF accounts, store xpub as the address marker
2276
+ publicKey: address,
2277
+ privateKey: privKey,
2278
+ };
2279
+ // Set UTXO balance bucket
2280
+ balances.syscoin = 0;
2281
+ handledAsUtxo = true;
2282
+ // Proceed to account creation below
2283
+ }
2284
+ else if (!isEvmNetwork && wifValidation.message) {
2285
+ // Provide useful feedback on UTXO network if WIF was attempted and failed
2286
+ // Continue to EVM handling only if actually EVM network
2287
+ }
2288
+ }
2289
+ // Check if the validation failed due to network mismatch
2290
+ if (zprvValidation.message &&
2291
+ zprvValidation.message.includes('Network mismatch')) {
2292
+ throw new Error(zprvValidation.message);
2293
+ }
2294
+ // Check if the validation failed due to invalid key prefix (only for known extended key formats)
2295
+ if (zprvValidation.message &&
2296
+ zprvValidation.message.includes('Invalid key prefix')) {
2297
+ throw new Error(zprvValidation.message);
2298
+ }
2299
+ // Check if it failed parsing as an extended key
2300
+ if (zprvValidation.message &&
2301
+ zprvValidation.message.includes('Failed to parse extended private key')) {
2302
+ throw new Error(zprvValidation.message);
2303
+ }
2304
+ // Check if it looks like an extended key that failed validation
2305
+ const knownExtendedKeyPrefixes = [
2306
+ 'xprv',
2307
+ 'xpub',
2308
+ 'yprv',
2309
+ 'ypub',
2310
+ 'zprv',
2311
+ 'zpub',
2312
+ 'tprv',
2313
+ 'tpub',
2314
+ 'uprv',
2315
+ 'upub',
2316
+ 'vprv',
2317
+ 'vpub',
2318
+ ];
2319
+ const prefix = privKey.substring(0, 4);
2320
+ const looksLikeExtendedKey = knownExtendedKeyPrefixes.includes(prefix);
2321
+ if (looksLikeExtendedKey) {
2322
+ // This looks like a UTXO extended key, but it failed validation
2323
+ // Don't try to import it as an EVM key
2324
+ if (isEvmNetwork) {
2325
+ throw new Error('Cannot import UTXO private key on EVM network. Please switch to a UTXO network (Bitcoin/Syscoin) first.');
2326
+ }
2327
+ // For UTXO networks, throw the original validation error
2328
+ throw new Error(zprvValidation.message || 'Invalid extended private key');
2329
+ }
2330
+ // If it's not an extended key and not a valid WIF, treat it as an Ethereum private key
2331
+ // But first check if we're on an EVM network
2332
+ if (!isEvmNetwork && !handledAsUtxo) {
2333
+ throw new Error('Cannot import EVM private key on UTXO network. Please switch to an EVM network first.');
2334
+ }
2335
+ if (!handledAsUtxo) {
2336
+ const hexPrivateKey = privKey.slice(0, 2) === '0x' ? privKey : `0x${privKey}`;
2337
+ // Validate it's a valid hex string (32 bytes = 64 hex chars)
2338
+ if (!/^0x[0-9a-fA-F]{64}$/.test(hexPrivateKey) &&
2339
+ !/^[0-9a-fA-F]{64}$/.test(privKey)) {
2340
+ throw new Error('Invalid private key format. Expected 32-byte hex string or extended private key.');
2341
+ }
2342
+ importedAccountValue =
2343
+ this.ethereumTransaction.importAccount(hexPrivateKey);
2344
+ balances.ethereum = 0;
2345
+ }
2346
+ }
2347
+ if (!importedAccountValue) {
2348
+ throw new Error('Invalid private key format. Expected WIF, extended private key, or 32-byte hex.');
2349
+ }
2350
+ const { address, publicKey, privateKey } = importedAccountValue;
2351
+ // Validate duplicates across all account buckets, including passkey smart accounts.
2352
+ if (this.accountAddressExists(accounts, address, isEvmNetwork))
2353
+ throw new Error('Account already exists, try again with another Private Key.');
2354
+ const id = this.getNextAccountId(accounts[KeyringAccountType.Imported]);
2355
+ const defaultLabel = label || `Imported ${id + 1}`;
2356
+ return {
2357
+ ...initialActiveImportedAccountState,
2358
+ address,
2359
+ label: defaultLabel,
2360
+ id,
2361
+ balances,
2362
+ isImported: true,
2363
+ xprv: this.withSecureData((sessionPwd) => {
2364
+ return CryptoJS.AES.encrypt(privateKey, sessionPwd).toString();
2365
+ }),
2366
+ xpub: publicKey,
2367
+ assets: {
2368
+ syscoin: [],
2369
+ ethereum: [],
2370
+ },
2371
+ };
2372
+ }
2373
+ // NEW: On-demand signer creation methods
2374
+ /**
2375
+ * Common method to decrypt mnemonic from session
2376
+ * Eliminates code duplication across multiple methods
2377
+ */
2378
+ getDecryptedMnemonic() {
2379
+ if (!this.sessionMnemonic || !this.sessionPassword) {
2380
+ throw new Error('Session information not available');
2381
+ }
2382
+ const mnemonic = this.withSecureData((sessionPwd, sessionMnemonic) => {
2383
+ const decrypted = CryptoJS.AES.decrypt(sessionMnemonic, sessionPwd).toString(CryptoJS.enc.Utf8);
2384
+ if (!decrypted) {
2385
+ throw new Error('Failed to decrypt mnemonic');
2386
+ }
2387
+ return decrypted;
2388
+ });
2389
+ return mnemonic;
2390
+ }
2391
+ /**
2392
+ * Creates network RPC config from current active network without making RPC calls
2393
+ * Common utility for all on-demand signer creation
2394
+ */
2395
+ createNetworkRpcConfig() {
2396
+ const network = this.getVault().activeNetwork;
2397
+ return {
2398
+ formattedNetwork: network,
2399
+ networkConfig: getNetworkConfig(network.slip44, network.currency),
2400
+ };
2401
+ }
2402
+ /**
2403
+ * Common signer creation logic - takes decrypted mnemonic/zprv and creates fresh signer
2404
+ * OPTIMIZED: No RPC call needed - uses network config directly
2405
+ */
2406
+ createFreshUTXOSigner(mnemonicOrZprv, accountId) {
2407
+ // Create signer using network config directly (no RPC call)
2408
+ const rpcConfig = this.createNetworkRpcConfig();
2409
+ // Type assertion to match getSyscoinSigners expected interface
2410
+ const { hd } = getSyscoinSigners({
2411
+ mnemonic: mnemonicOrZprv,
2412
+ rpc: rpcConfig,
2413
+ });
2414
+ // Create account at the specified index and set it as active
2415
+ // Note: createAccountAtIndex is synchronous despite TypeScript types
2416
+ hd.createAccountAtIndex(accountId, 84);
2417
+ // Verify the account was created correctly
2418
+ if (!hd.Signer.accounts.has(accountId)) {
2419
+ throw new Error(`Failed to create account at index ${accountId}`);
2420
+ }
2421
+ // Verify the correct account is active
2422
+ if (hd.Signer.accountIndex !== accountId) {
2423
+ throw new Error(`Account index mismatch: expected ${accountId}, got ${hd.Signer.accountIndex}`);
2424
+ }
2425
+ return hd;
2426
+ }
2427
+ /**
2428
+ * Creates a fresh UTXO signer for HD accounts derived from the main seed
2429
+ * OPTIMIZED: No RPC call needed - uses network config directly
2430
+ */
2431
+ createOnDemandUTXOSigner(accountId) {
2432
+ // Use common method to avoid code duplication
2433
+ const mnemonic = this.getDecryptedMnemonic();
2434
+ return this.createFreshUTXOSigner(mnemonic, accountId);
2435
+ }
2436
+ // NEW: Helper methods for HD signer management
2437
+ isZprv(key) {
2438
+ const zprvPrefixes = ['zprv', 'tprv', 'vprv', 'xprv'];
2439
+ return zprvPrefixes.some((prefix) => key.startsWith(prefix));
2440
+ }
2441
+ // Helper methods for secure buffer operations
2442
+ getSessionPasswordString() {
2443
+ if (!this.sessionPassword || this.sessionPassword.isCleared()) {
2444
+ throw new Error('Session password not available');
2445
+ }
2446
+ // WARNING: This exposes sensitive data as a string
2447
+ // Use only for CryptoJS operations that require string input
2448
+ return this.sessionPassword.toString();
2449
+ }
2450
+ getSessionMnemonicString() {
2451
+ if (!this.sessionMnemonic || this.sessionMnemonic.isCleared()) {
2452
+ throw new Error('Session mnemonic not available');
2453
+ }
2454
+ // WARNING: This exposes sensitive data as a string
2455
+ // Use only for CryptoJS operations that require string input
2456
+ return this.sessionMnemonic.toString();
2457
+ }
2458
+ // Secure method to perform cryptographic operations without exposing strings
2459
+ withSecureData(operation) {
2460
+ if (!this.sessionPassword || !this.sessionMnemonic) {
2461
+ throw new Error('Session data not available');
2462
+ }
2463
+ // Perform operation with minimal exposure
2464
+ const result = operation(this.getSessionPasswordString(), this.getSessionMnemonicString());
2465
+ // Clear any temporary variables if needed
2466
+ return result;
2467
+ }
2468
+ generateNetworkAwareLabel(accountId, network) {
2469
+ // Generate concise network-specific labels using actual network config
2470
+ const { label, chainId, kind, currency } = network;
2471
+ // Create a shortened network identifier based on actual network configurations
2472
+ let networkPrefix = '';
2473
+ if (kind === INetworkType.Syscoin) {
2474
+ // UTXO networks (slip44 = 57 for mainnet, 1 for testnet)
2475
+ if (chainId === 57) {
2476
+ networkPrefix = 'SYS'; // Syscoin UTXO Mainnet
2477
+ }
2478
+ else if (chainId === 5700) {
2479
+ networkPrefix = 'SYS-T'; // Syscoin UTXO Testnet (slip44=1)
2480
+ }
2481
+ else {
2482
+ // Other UTXO networks - use currency shortcut (e.g., "btc" -> "BTC")
2483
+ if (currency) {
2484
+ networkPrefix = currency.toUpperCase();
2485
+ }
2486
+ else {
2487
+ // Fallback to first word from label if no currency
2488
+ const firstWord = label.split(' ')[0];
2489
+ networkPrefix =
2490
+ firstWord.length > 6 ? firstWord.substring(0, 6) : firstWord;
2491
+ }
2492
+ }
2493
+ }
2494
+ else {
2495
+ // EVM networks (all use slip44=60)
2496
+ if (chainId === 1) {
2497
+ networkPrefix = 'ETH'; // Ethereum Mainnet
2498
+ }
2499
+ else if (chainId === 11155111) {
2500
+ networkPrefix = 'ETH-T'; // Ethereum Sepolia Testnet
2501
+ }
2502
+ else if (chainId === 137) {
2503
+ networkPrefix = 'POLY'; // Polygon Mainnet
2504
+ }
2505
+ else if (chainId === 80001) {
2506
+ networkPrefix = 'POLY-T'; // Polygon Mumbai Testnet
2507
+ }
2508
+ else if (chainId === 57) {
2509
+ networkPrefix = 'NEVM'; // Syscoin NEVM Mainnet
2510
+ }
2511
+ else if (chainId === 5700) {
2512
+ networkPrefix = 'NEVM-T'; // Syscoin NEVM Testnet
2513
+ }
2514
+ else if (chainId === 570) {
2515
+ networkPrefix = 'ROLLUX'; // Rollux Mainnet
2516
+ }
2517
+ else if (chainId === 57000) {
2518
+ networkPrefix = 'ROLLUX-T'; // Rollux Testnet
2519
+ }
2520
+ else {
2521
+ // Other EVM networks - use currency shortcut if available
2522
+ if (currency) {
2523
+ // Check if it's a testnet network
2524
+ if (label.toLowerCase().includes('testnet') ||
2525
+ label.toLowerCase().includes('test')) {
2526
+ networkPrefix = `${currency.toUpperCase()}-T`;
2527
+ }
2528
+ else {
2529
+ networkPrefix = currency.toUpperCase();
2530
+ }
2531
+ }
2532
+ else {
2533
+ // Fallback to extracting meaningful prefix from label
2534
+ const firstWord = label.split(' ')[0];
2535
+ if (firstWord.toLowerCase().includes('testnet') ||
2536
+ firstWord.toLowerCase().includes('test')) {
2537
+ const baseWord = label.split(' ')[0];
2538
+ networkPrefix = `${baseWord.substring(0, 4).toUpperCase()}-T`;
2539
+ }
2540
+ else {
2541
+ networkPrefix =
2542
+ firstWord.length > 6
2543
+ ? firstWord.substring(0, 6).toUpperCase()
2544
+ : firstWord.toUpperCase();
2545
+ }
2546
+ }
2547
+ }
2548
+ }
2549
+ return `${networkPrefix} ${accountId + 1}`;
2550
+ }
2551
+ /**
2552
+ * Clean up all resources
2553
+ */
2554
+ async destroy() {
2555
+ __classPrivateFieldSet(this, _KeyringCore_deviceUnlockRetired, true, "f");
2556
+ __classPrivateFieldGet(this, _KeyringCore_sessionOwnership, "f").destroyed = true;
2557
+ await this.lockWallet();
2558
+ // Clear any remaining references
2559
+ this.ethereumTransaction = {};
2560
+ this.syscoinTransaction = {};
2561
+ }
2562
+ }
2563
+ _KeyringCore_deviceUnlockRetired = new WeakMap(), _KeyringCore_sessionOwnership = new WeakMap(), _KeyringCore_instances = new WeakSet(), _KeyringCore_createDeviceUnlockControls = function _KeyringCore_createDeviceUnlockControls(port, readVaultPair) {
2564
+ const readPair = () => readCurrentVaultPair(readVaultPair);
2565
+ let pending = false;
2566
+ const run = async (request, assertCurrent) => {
2567
+ if (pending)
2568
+ throw new Error('Device unlock operation is busy');
2569
+ pending = true;
2570
+ const assertOwnership = __classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_captureSessionGuard).call(this, 'Device unlock session changed');
2571
+ const issuing = request !== null;
2572
+ const check = () => {
2573
+ const result = assertCurrent();
2574
+ if (result !== undefined) {
2575
+ // Reject async guards and consume a possible rejected promise.
2576
+ if (result &&
2577
+ (typeof result === 'object' || typeof result === 'function'))
2578
+ void Promise.resolve(result).catch(() => undefined);
2579
+ throw new TypeError('Device unlock guard must return undefined synchronously');
2580
+ }
2581
+ if (__classPrivateFieldGet(this, _KeyringCore_deviceUnlockRetired, "f") ||
2582
+ (!issuing && (this.sessionPassword || this.sessionMnemonic))) {
2583
+ throw new Error('Device unlock session changed');
2584
+ }
2585
+ assertOwnership();
2586
+ };
2587
+ let key;
2588
+ try {
2589
+ check();
2590
+ const pair = await readPair();
2591
+ check();
2592
+ if (!pair)
2593
+ return false;
2594
+ const checkPair = async (requireLiveOwner = true) => {
2595
+ const current = await readPair();
2596
+ if (requireLiveOwner)
2597
+ check();
2598
+ if (current?.pairFingerprint !== pair.pairFingerprint ||
2599
+ current.vault !== pair.vault ||
2600
+ current.salt !== pair.salt) {
2601
+ throw new Error('Device unlock vault changed');
2602
+ }
2603
+ };
2604
+ if (request) {
2605
+ key = this.hexToBytes(await this.deriveStoredSessionKey(request.password, {
2606
+ salt: pair.salt,
2607
+ }));
2608
+ }
2609
+ else {
2610
+ const credential = await port.readCredential(pair.pairFingerprint);
2611
+ key = credential?.key;
2612
+ check();
2613
+ if (credential?.pairFingerprint !== pair.pairFingerprint)
2614
+ return false;
2615
+ }
2616
+ check();
2617
+ if (!(key instanceof Uint8Array) || key.length !== 32)
2618
+ return false;
2619
+ const keyHex = Buffer.from(key.buffer, key.byteOffset, key.byteLength).toString('hex');
2620
+ let payload;
2621
+ try {
2622
+ payload = await decryptCurrentVault(pair.vault, keyHex);
2623
+ }
2624
+ catch (error) {
2625
+ if (!(error instanceof VaultAuthenticationError))
2626
+ throw error;
2627
+ // Preserve a definitive wrong-password result for host accounting
2628
+ // even if enrollment was cancelled. The pair must still match and
2629
+ // this branch never installs a session or writes a credential.
2630
+ await checkPair(!issuing);
2631
+ if (!issuing)
2632
+ check();
2633
+ return false;
2634
+ }
2635
+ check();
2636
+ const mnemonic = payload && typeof payload === 'object'
2637
+ ? Object.getOwnPropertyDescriptor(payload, 'mnemonic')?.value
2638
+ : undefined;
2639
+ if (typeof mnemonic !== 'string' || !this.isSeedValid(mnemonic)) {
2640
+ throw new Error('Invalid device unlock vault payload');
2641
+ }
2642
+ await checkPair();
2643
+ check();
2644
+ if (request) {
2645
+ const result = await port.writeCredential(Object.freeze({ pairFingerprint: pair.pairFingerprint, key }), check);
2646
+ if (result !== undefined)
2647
+ throw new Error('Device unlock credential write was not acknowledged');
2648
+ await checkPair();
2649
+ check();
2650
+ }
2651
+ else {
2652
+ if (__classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_finishUnlock).call(this).needsAccountCreation)
2653
+ return false;
2654
+ __classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_installVaultSession).call(this, mnemonic, keyHex, check);
2655
+ }
2656
+ return true;
2657
+ }
2658
+ finally {
2659
+ if (key instanceof Uint8Array)
2660
+ key.fill(0);
2661
+ pending = false;
2662
+ }
2663
+ };
2664
+ return Object.freeze({
2665
+ issue: (password, assertCurrent) => {
2666
+ if (typeof password !== 'string' || !password)
2667
+ return Promise.reject(new TypeError('Wallet password is required'));
2668
+ return run({ password }, assertCurrent);
2669
+ },
2670
+ resume: (assertCurrent) => run(null, assertCurrent),
2671
+ });
2672
+ }, _KeyringCore_acceptSessionFrom = function _KeyringCore_acceptSessionFrom(source) {
2673
+ if (source.authority !== this.authority ||
2674
+ source.storage !== this.storage ||
2675
+ source.vaultPersistence !== this.vaultPersistence ||
2676
+ __classPrivateFieldGet(source, _KeyringCore_sessionOwnership, "f").tracker !== __classPrivateFieldGet(this, _KeyringCore_sessionOwnership, "f").tracker) {
2677
+ throw new Error('Cannot transfer session between different wallet authorities');
2678
+ }
2679
+ if (!source.sessionPassword || !source.sessionMnemonic) {
2680
+ throw new Error('Session data is missing during transfer');
2681
+ }
2682
+ __classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_replaceSessionBuffers).call(this, source.sessionPassword, source.sessionMnemonic, __classPrivateFieldGet(source, _KeyringCore_sessionOwnership, "f").token);
2683
+ }, _KeyringCore_assertSessionOwner = function _KeyringCore_assertSessionOwner(sourceToken) {
2684
+ const ownership = __classPrivateFieldGet(this, _KeyringCore_sessionOwnership, "f");
2685
+ if (ownership.destroyed)
2686
+ throw new Error('Wallet keyring was destroyed');
2687
+ const owner = ownership.tracker.current;
2688
+ if (sourceToken === undefined
2689
+ ? owner !== undefined && owner !== ownership.token
2690
+ : owner !== sourceToken) {
2691
+ throw new Error('Wallet session is owned by another keyring');
2692
+ }
2693
+ }, _KeyringCore_captureSessionGuard = function _KeyringCore_captureSessionGuard(message = 'Wallet unlock session changed') {
2694
+ const ownership = __classPrivateFieldGet(this, _KeyringCore_sessionOwnership, "f");
2695
+ const revision = this.privateRecordSessionRevision;
2696
+ const epoch = ownership.tracker.epoch;
2697
+ return () => {
2698
+ if (this.privateRecordSessionRevision !== revision)
2699
+ throw new Error(message);
2700
+ __classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_assertSessionOwner).call(this);
2701
+ if (ownership.tracker.epoch !== epoch)
2702
+ throw new Error(message);
2703
+ };
2704
+ }, _KeyringCore_replaceSessionBuffers = function _KeyringCore_replaceSessionBuffers(sessionPassword, sessionMnemonic, sourceToken) {
2705
+ __classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_assertSessionOwner).call(this, sourceToken);
2706
+ this.privateRecordSessionRevision++;
2707
+ // Clear any existing data first
2708
+ if (this.sessionPassword) {
2709
+ this.sessionPassword.clear();
2710
+ }
2711
+ if (this.sessionMnemonic) {
2712
+ this.sessionMnemonic.clear();
2713
+ }
2714
+ // Take ownership of the actual SecureBuffer objects
2715
+ // No copying - these are the original objects
2716
+ this.sessionPassword = sessionPassword;
2717
+ this.sessionMnemonic = sessionMnemonic;
2718
+ const ownership = __classPrivateFieldGet(this, _KeyringCore_sessionOwnership, "f");
2719
+ ownership.tracker.current = ownership.token;
2720
+ ownership.tracker.epoch++;
2721
+ }, _KeyringCore_finishUnlock = function _KeyringCore_finishUnlock() {
2722
+ // Re-initialize syscoin transaction handler if it was cleared on lock.
2723
+ // `lockWallet()` intentionally replaces it with a plain object to drop signer references,
2724
+ // so we must restore the real class instance on unlock.
2725
+ if (!this.syscoinTransaction ||
2726
+ typeof this.syscoinTransaction.getRecommendedFee !== 'function') {
2727
+ this.syscoinTransaction = this.createSyscoinTransactions();
2728
+ }
2729
+ // NOTE: Active account management is now handled by vault state/Redux
2730
+ // No need to explicitly set active account after unlock - it's managed externally
2731
+ const vault = this.getVault();
2732
+ if (vault.activeAccount?.id !== undefined && vault.activeAccount?.type) {
2733
+ // Check if the active account actually exists in the accounts map
2734
+ const accountType = vault.activeAccount.type;
2735
+ const accountId = vault.activeAccount.id;
2736
+ const accountExists = vault.accounts?.[accountType]?.[accountId];
2737
+ if (!accountExists) {
2738
+ console.log(`[KeyringManager] Active account ${accountType}:${accountId} not found in accounts map. This may indicate a migration from old vault format.`);
2739
+ // Signal that accounts need to be created after migration
2740
+ return {
2741
+ canLogin: true,
2742
+ needsAccountCreation: true,
2743
+ };
2744
+ }
2745
+ console.log(`[KeyringManager] Active account ${vault.activeAccount.id} available after unlock`);
2746
+ }
2747
+ return {
2748
+ canLogin: true,
2749
+ };
2750
+ }, _KeyringCore_installVaultSession = function _KeyringCore_installVaultSession(mnemonic, key, assertCurrent) {
2751
+ const ciphertext = CryptoJS.AES.encrypt(mnemonic, key).toString();
2752
+ const password = new SecureBuffer(key);
2753
+ const encryptedMnemonic = new SecureBuffer(ciphertext);
2754
+ try {
2755
+ assertCurrent?.();
2756
+ __classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_replaceSessionBuffers).call(this, password, encryptedMnemonic);
2757
+ }
2758
+ catch (error) {
2759
+ password.clear();
2760
+ encryptedMnemonic.clear();
2761
+ throw error;
2762
+ }
2763
+ }, _KeyringCore_recreateSessionFromVault = async function _KeyringCore_recreateSessionFromVault(saltedHashPassword, assertCurrent) {
2764
+ try {
2765
+ // v4 vault is encrypted with the derived session password key (PBKDF2 output)
2766
+ // `saltedHashPassword` is the derived session password key passed by caller.
2767
+ const { mnemonic } = await this.vaultPersistence.getDecryptedVault(saltedHashPassword);
2768
+ assertCurrent();
2769
+ if (!mnemonic) {
2770
+ throw new Error('Mnemonic not found in vault');
2771
+ }
2772
+ __classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_installVaultSession).call(this, mnemonic, saltedHashPassword, assertCurrent);
2773
+ // Capture the installed revision before returning to the async caller.
2774
+ return __classPrivateFieldGet(this, _KeyringCore_instances, "m", _KeyringCore_captureSessionGuard).call(this);
2775
+ }
2776
+ catch (error) {
2777
+ console.error('ERROR recreateSessionFromVault', { error });
2778
+ throw error;
2779
+ }
2780
+ };
2781
+ //# sourceMappingURL=keyring-core.js.map