@sidhujag/sysweb3-keyring 1.0.547 → 1.0.548

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