minimal-xec-wallet 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,976 @@
1
+ /*
2
+ An npm JavaScript library for front end web apps. Implements a minimal
3
+ eCash (XEC) wallet with eToken support.
4
+ */
5
+
6
+ /* eslint-disable no-async-promise-executor */
7
+
8
+ 'use strict'
9
+
10
+ const { ChronikClient } = require('chronik-client')
11
+ const crypto = require('crypto-js')
12
+
13
+ // Local libraries
14
+ const SendXEC = require('./lib/send-xec')
15
+ const Utxos = require('./lib/utxos')
16
+ const AdapterRouter = require('./lib/adapters/router')
17
+ const OpReturn = require('./lib/op-return')
18
+ const ConsolidateUtxos = require('./lib/consolidate-utxos.js')
19
+ const KeyDerivation = require('./lib/key-derivation')
20
+ const HybridTokenManager = require('./lib/hybrid-token-manager')
21
+
22
+ // let this
23
+
24
+ class MinimalXECWallet {
25
+ constructor (hdPrivateKeyOrMnemonic, advancedOptions = {}) {
26
+ this.advancedOptions = advancedOptions
27
+
28
+ // BEGIN Handle advanced options.
29
+ // HD Derivation path for XEC (coin type 899)
30
+ this.hdPath = this.advancedOptions.hdPath || "m/44'/899'/0'/0/0"
31
+
32
+ // Default Chronik endpoints (working as of 2025)
33
+ const chronikOptions = {
34
+ chronikUrls: advancedOptions.chronikUrls || [
35
+ 'https://chronik.e.cash',
36
+ 'https://chronik.be.cash',
37
+ 'https://xec.paybutton.org',
38
+ 'https://chronik.pay2stay.com/xec',
39
+ 'https://chronik.pay2stay.com/xec2',
40
+ 'https://chronik1.alitayin.com',
41
+ 'https://chronik2.alitayin.com'
42
+ ]
43
+ }
44
+
45
+ // Set the fee rate (XEC uses same structure as BCH, but lower amounts)
46
+ this.fee = 1.2
47
+ if (this.advancedOptions.fee) {
48
+ this.fee = this.advancedOptions.fee
49
+ }
50
+
51
+ // Donation setting (defaults to false for security and user consent)
52
+ this.enableDonations = this.advancedOptions.enableDonations || false
53
+ // END Handle advanced options.
54
+
55
+ // Encapsulate the external libraries.
56
+ this.crypto = crypto
57
+ this.ChronikClient = ChronikClient
58
+
59
+ // Initialize key derivation
60
+ this.keyDerivation = new KeyDerivation()
61
+
62
+ // Initialize chronik client with fallback strategy - use first endpoint immediately
63
+ // The adapter router will handle connection strategy internally
64
+ this.chronik = new ChronikClient(chronikOptions.chronikUrls[0])
65
+ chronikOptions.chronik = this.chronik
66
+
67
+ // Instantiate the adapter router (it handles connection strategy internally)
68
+ this.ar = new AdapterRouter(chronikOptions)
69
+ chronikOptions.ar = this.ar
70
+
71
+ // Instantiate local libraries
72
+ this.sendXecLib = new SendXEC(chronikOptions)
73
+ this.utxos = new Utxos(chronikOptions)
74
+ this.opReturn = new OpReturn(chronikOptions)
75
+ this.consolidateUtxos = new ConsolidateUtxos(this)
76
+ this.hybridTokens = new HybridTokenManager(chronikOptions)
77
+
78
+ this.temp = []
79
+ this.isInitialized = false
80
+
81
+ // The create() function returns a promise. When it resolves, the
82
+ // walletInfoCreated flag will be set to true. The instance will also
83
+ // have a new `walletInfo` property that will contain the wallet information.
84
+ this.walletInfoCreated = false
85
+ this.walletInfoPromise = this.create(hdPrivateKeyOrMnemonic)
86
+
87
+ // Initialize WebAssembly early for better browser compatibility
88
+ this.wasmInitPromise = this._initializeWASM()
89
+
90
+ // Bind the 'this' object to all functions
91
+ this.create = this.create.bind(this)
92
+ this.initialize = this.initialize.bind(this)
93
+ this.getUtxos = this.getUtxos.bind(this)
94
+ this.getXecBalance = this.getXecBalance.bind(this)
95
+ this.getDetailedBalance = this.getDetailedBalance.bind(this)
96
+ this.getTransactions = this.getTransactions.bind(this)
97
+ this.getTxData = this.getTxData.bind(this)
98
+ this.sendXec = this.sendXec.bind(this)
99
+ this.sendETokens = this.sendETokens.bind(this) // Phase 2
100
+ this.burnETokens = this.burnETokens.bind(this) // Phase 2
101
+ this.listETokens = this.listETokens.bind(this) // Phase 2
102
+ this.sendAllXec = this.sendAllXec.bind(this)
103
+ this.burnAllETokens = this.burnAllETokens.bind(this) // Phase 2
104
+ this.getXecUsd = this.getXecUsd.bind(this)
105
+ this.sendOpReturn = this.sendOpReturn.bind(this)
106
+ this.utxoIsValid = this.utxoIsValid.bind(this)
107
+ this.getETokenData = this.getETokenData.bind(this) // Phase 2
108
+ this.getKeyPair = this.getKeyPair.bind(this)
109
+ this.optimize = this.optimize.bind(this)
110
+ this.getETokenBalance = this.getETokenBalance.bind(this) // Phase 2
111
+ this.getPubKey = this.getPubKey.bind(this)
112
+ this.broadcast = this.broadcast.bind(this)
113
+ this.cid2json = this.cid2json.bind(this)
114
+ this._validateAddress = this._validateAddress.bind(this)
115
+ this._sanitizeError = this._sanitizeError.bind(this)
116
+ this._secureWalletInfo = this._secureWalletInfo.bind(this)
117
+ this.exportPrivateKeyAsWIF = this.exportPrivateKeyAsWIF.bind(this)
118
+ this.validateWIF = this.validateWIF.bind(this)
119
+ }
120
+
121
+ // Private method to validate XEC addresses
122
+ _validateAddress (address) {
123
+ try {
124
+ if (!address || typeof address !== 'string') {
125
+ throw new Error('Address must be a non-empty string')
126
+ }
127
+
128
+ // Allow test addresses in test environment
129
+ if ((process.env.NODE_ENV === 'test' || process.env.TEST === 'unit') && address.startsWith('test-')) {
130
+ return true
131
+ }
132
+
133
+ // Only allow eCash addresses (ecash: prefix)
134
+ if (!address.startsWith('ecash:')) {
135
+ throw new Error('Invalid address format - must be eCash address (ecash: prefix)')
136
+ }
137
+
138
+ // Use ecashaddrjs to validate the eCash address
139
+ const { decodeCashAddress } = require('ecashaddrjs')
140
+ decodeCashAddress(address)
141
+ return true
142
+ } catch (err) {
143
+ throw new Error(`Address validation failed: ${err.message}`)
144
+ }
145
+ }
146
+
147
+ // Private method to sanitize error messages
148
+ _sanitizeError (error, context = '') {
149
+ const safeMessage = error.message || 'An error occurred'
150
+ // Remove potentially sensitive information from error messages
151
+ const sanitized = safeMessage
152
+ .replace(/[A-Za-z0-9+/=]{64,}/g, '[SENSITIVE_DATA_REMOVED]')
153
+ .replace(/[LK][1-9A-HJ-NP-Za-km-z]{51}/g, '[PRIVATE_KEY_REMOVED]')
154
+ .replace(/ecash:[a-z0-9]{42}/g, '[ADDRESS_REMOVED]')
155
+
156
+ return new Error(`${context ? context + ': ' : ''}${sanitized}`)
157
+ }
158
+
159
+ // Private method to create secure wallet info object
160
+ _secureWalletInfo (walletInfo) {
161
+ // Create a copy without exposing sensitive data directly
162
+ return {
163
+ mnemonic: walletInfo.mnemonic,
164
+ xecAddress: walletInfo.xecAddress,
165
+ hdPath: walletInfo.hdPath,
166
+ fee: this.fee,
167
+ // Store private key securely - consider implementing memory protection
168
+ privateKey: walletInfo.privateKey,
169
+ // Include donation setting (defaults to false for security)
170
+ enableDonations: this.advancedOptions.enableDonations || false
171
+ }
172
+ }
173
+
174
+ // Create a new wallet. Returns a promise that resolves into a wallet object.
175
+ async create (mnemonicOrWif) {
176
+ try {
177
+ // Attempt to decrypt mnemonic if password is provided.
178
+ if (mnemonicOrWif && this.advancedOptions.password) {
179
+ mnemonicOrWif = this.decrypt(
180
+ mnemonicOrWif,
181
+ this.advancedOptions.password
182
+ )
183
+ }
184
+
185
+ const walletInfo = {}
186
+
187
+ // No input. Generate a new mnemonic.
188
+ if (!mnemonicOrWif) {
189
+ // Generate new mnemonic using key derivation library
190
+ const mnemonic = this._generateMnemonic()
191
+ const { privateKey, publicKey, address } = this._deriveFromMnemonic(mnemonic)
192
+
193
+ walletInfo.privateKey = privateKey
194
+ walletInfo.publicKey = publicKey
195
+ walletInfo.mnemonic = mnemonic
196
+ walletInfo.xecAddress = address
197
+ walletInfo.hdPath = this.hdPath
198
+ } else {
199
+ // A WIF will start with L, K, 5 (mainnet) or c, 9 (testnet), will have no spaces,
200
+ // and will be 51-52 characters long.
201
+ const startsWithWIFChar =
202
+ mnemonicOrWif &&
203
+ (['k', 'l', 'c', '5', '9'].includes(mnemonicOrWif[0].toString().toLowerCase()))
204
+ const isWIFLength = mnemonicOrWif && (mnemonicOrWif.length === 51 || mnemonicOrWif.length === 52)
205
+
206
+ if (startsWithWIFChar && isWIFLength) {
207
+ // Enhanced WIF Private Key handling
208
+ if (!this.keyDerivation._isValidWIF(mnemonicOrWif)) {
209
+ throw new Error('Invalid WIF format or checksum')
210
+ }
211
+
212
+ const { privateKey, publicKey, address, isCompressed, wif } = this._deriveFromWif(mnemonicOrWif)
213
+ walletInfo.privateKey = privateKey
214
+ walletInfo.publicKey = publicKey
215
+ walletInfo.mnemonic = null
216
+ walletInfo.xecAddress = address
217
+ walletInfo.hdPath = null
218
+ walletInfo.isCompressed = isCompressed
219
+ walletInfo.wif = wif
220
+ } else if (mnemonicOrWif.length === 64 && /^[a-fA-F0-9]+$/.test(mnemonicOrWif)) {
221
+ // Hex Private Key (64 characters, all hex)
222
+ const { publicKey, address } = this._deriveFromWif(mnemonicOrWif)
223
+ walletInfo.privateKey = mnemonicOrWif
224
+ walletInfo.publicKey = publicKey
225
+ walletInfo.mnemonic = null
226
+ walletInfo.xecAddress = address
227
+ walletInfo.hdPath = null
228
+ } else {
229
+ // 12-word Mnemonic
230
+ const mnemonic = mnemonicOrWif
231
+ const { privateKey, publicKey, address } = this._deriveFromMnemonic(mnemonic)
232
+
233
+ walletInfo.privateKey = privateKey
234
+ walletInfo.publicKey = publicKey
235
+ walletInfo.mnemonic = mnemonic
236
+ walletInfo.xecAddress = address
237
+ walletInfo.hdPath = this.hdPath
238
+ }
239
+ }
240
+
241
+ // Encrypt the mnemonic if a password is provided.
242
+ if (this.advancedOptions.password && walletInfo.mnemonic) {
243
+ walletInfo.mnemonicEncrypted = this.encrypt(
244
+ walletInfo.mnemonic,
245
+ this.advancedOptions.password
246
+ )
247
+ }
248
+
249
+ this.walletInfoCreated = true
250
+ this.walletInfo = walletInfo
251
+
252
+ return walletInfo
253
+ } catch (err) {
254
+ throw this._sanitizeError(err, 'Wallet creation failed')
255
+ }
256
+ }
257
+
258
+ // Helper method to generate mnemonic
259
+ _generateMnemonic (strength = 128) {
260
+ try {
261
+ return this.keyDerivation.generateMnemonic(strength)
262
+ } catch (err) {
263
+ throw this._sanitizeError(err, 'Mnemonic generation failed')
264
+ }
265
+ }
266
+
267
+ // Helper method to derive keys from mnemonic
268
+ _deriveFromMnemonic (mnemonic) {
269
+ try {
270
+ return this.keyDerivation.deriveFromMnemonic(mnemonic, this.hdPath)
271
+ } catch (err) {
272
+ throw this._sanitizeError(err, 'HD derivation failed')
273
+ }
274
+ }
275
+
276
+ // Helper method to derive keys from WIF
277
+ _deriveFromWif (wif) {
278
+ try {
279
+ return this.keyDerivation.deriveFromWif(wif)
280
+ } catch (err) {
281
+ throw this._sanitizeError(err, 'WIF derivation failed')
282
+ }
283
+ }
284
+
285
+ // Initialize WebAssembly for browser compatibility
286
+ async _initializeWASM () {
287
+ try {
288
+ // Only initialize WASM in browser environments
289
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
290
+ // Dynamic import to avoid issues in Node.js environments
291
+ const wasmShim = require('./browser-shims/ecash_lib_wasm_browser')
292
+
293
+ if (wasmShim && wasmShim.init) {
294
+ console.log('Initializing WebAssembly for browser compatibility...')
295
+ await wasmShim.init()
296
+ console.log('WebAssembly initialization completed')
297
+ }
298
+ }
299
+
300
+ return true
301
+ } catch (err) {
302
+ // WASM initialization failure should not prevent wallet from working
303
+ console.warn('WebAssembly initialization failed (using fallbacks):', err.message)
304
+ return false
305
+ }
306
+ }
307
+
308
+ // Initialize is called to initialize the UTXO store, download token data, and
309
+ // get a balance of the wallet.
310
+ async initialize () {
311
+ await this.walletInfoPromise
312
+
313
+ // Ensure WASM is initialized (but don't block on it)
314
+ try {
315
+ await this.wasmInitPromise
316
+ } catch (err) {
317
+ console.warn('WASM initialization incomplete, continuing with fallbacks')
318
+ }
319
+
320
+ await this.utxos.initUtxoStore(this.walletInfo.xecAddress)
321
+
322
+ this.isInitialized = true
323
+
324
+ return true
325
+ }
326
+
327
+ // Encrypt the mnemonic of the wallet using secure key derivation.
328
+ encrypt (mnemonic, password) {
329
+ try {
330
+ // Validate inputs
331
+ if (!mnemonic || typeof mnemonic !== 'string') {
332
+ throw new Error('Invalid mnemonic provided for encryption')
333
+ }
334
+ if (!password || typeof password !== 'string' || password.length < 8) {
335
+ throw new Error('Password must be at least 8 characters long')
336
+ }
337
+
338
+ // Generate a random salt
339
+ const salt = this.crypto.lib.WordArray.random(256 / 8)
340
+
341
+ // Use PBKDF2 for key derivation with 10000 iterations
342
+ const key = this.crypto.PBKDF2(password, salt, {
343
+ keySize: 256 / 32,
344
+ iterations: 10000
345
+ })
346
+
347
+ // Generate random IV
348
+ const iv = this.crypto.lib.WordArray.random(128 / 8)
349
+
350
+ // Encrypt with AES-256-CBC
351
+ const encrypted = this.crypto.AES.encrypt(mnemonic, key, {
352
+ iv: iv,
353
+ mode: this.crypto.mode.CBC,
354
+ padding: this.crypto.pad.Pkcs7
355
+ })
356
+
357
+ // Combine salt, IV, and encrypted data
358
+ const combined = {
359
+ salt: salt.toString(),
360
+ iv: iv.toString(),
361
+ encrypted: encrypted.toString()
362
+ }
363
+
364
+ return JSON.stringify(combined)
365
+ } catch (err) {
366
+ throw new Error(`Encryption failed: ${err.message}`)
367
+ }
368
+ }
369
+
370
+ // Decrypt the mnemonic of the wallet using secure key derivation.
371
+ decrypt (mnemonicEncrypted, password) {
372
+ try {
373
+ // Validate inputs
374
+ if (!mnemonicEncrypted || typeof mnemonicEncrypted !== 'string') {
375
+ throw new Error('Invalid encrypted data provided')
376
+ }
377
+ if (!password || typeof password !== 'string') {
378
+ throw new Error('Password is required for decryption')
379
+ }
380
+
381
+ // Check if it's the old CryptoJS format (starts with base64 "U2FsdGVkX1")
382
+ if (mnemonicEncrypted.startsWith('U2FsdGVkX1')) {
383
+ return this._decryptLegacyFormat(mnemonicEncrypted, password)
384
+ }
385
+
386
+ // Parse the new encrypted data format
387
+ let combined
388
+ try {
389
+ combined = JSON.parse(mnemonicEncrypted)
390
+ } catch (parseErr) {
391
+ throw new Error('Invalid encrypted data format')
392
+ }
393
+
394
+ if (!combined.salt || !combined.iv || !combined.encrypted) {
395
+ throw new Error('Encrypted data is missing required components')
396
+ }
397
+
398
+ // Recreate the key using the stored salt
399
+ const salt = this.crypto.enc.Hex.parse(combined.salt)
400
+ const key = this.crypto.PBKDF2(password, salt, {
401
+ keySize: 256 / 32,
402
+ iterations: 10000
403
+ })
404
+
405
+ // Parse IV
406
+ const iv = this.crypto.enc.Hex.parse(combined.iv)
407
+
408
+ // Decrypt
409
+ const decrypted = this.crypto.AES.decrypt(combined.encrypted, key, {
410
+ iv: iv,
411
+ mode: this.crypto.mode.CBC,
412
+ padding: this.crypto.pad.Pkcs7
413
+ })
414
+
415
+ const mnemonic = decrypted.toString(this.crypto.enc.Utf8)
416
+
417
+ if (!mnemonic) {
418
+ throw new Error('Decryption failed - wrong password or corrupted data')
419
+ }
420
+
421
+ return mnemonic
422
+ } catch (err) {
423
+ throw new Error(`Decryption failed: ${err.message}`)
424
+ }
425
+ }
426
+
427
+ // Decrypt legacy CryptoJS format for backward compatibility
428
+ _decryptLegacyFormat (mnemonicEncrypted, password) {
429
+ try {
430
+ // Use the old CryptoJS format decryption
431
+ const decrypted = this.crypto.AES.decrypt(mnemonicEncrypted, password)
432
+ const mnemonic = decrypted.toString(this.crypto.enc.Utf8)
433
+
434
+ if (!mnemonic) {
435
+ throw new Error('Wrong password')
436
+ }
437
+
438
+ return mnemonic
439
+ } catch (err) {
440
+ // Return specific error message for wrong password
441
+ if (err.message === 'Wrong password') {
442
+ throw err
443
+ }
444
+ throw new Error('Wrong password')
445
+ }
446
+ }
447
+
448
+ // Get the UTXO information for this wallet.
449
+ async getUtxos (xecAddress) {
450
+ try {
451
+ let addr = xecAddress
452
+
453
+ // Validate address if provided
454
+ if (xecAddress) {
455
+ this._validateAddress(xecAddress)
456
+ }
457
+
458
+ // If no address is passed in, but the wallet has been initialized, use the
459
+ // wallet's address.
460
+ if (!xecAddress && this.walletInfo && this.walletInfo.xecAddress) {
461
+ addr = this.walletInfo.xecAddress
462
+ await this.utxos.initUtxoStore(addr)
463
+ return this.ar.getUtxos(addr)
464
+ }
465
+
466
+ if (!addr) {
467
+ throw new Error('No address provided and wallet not initialized')
468
+ }
469
+
470
+ const utxos = await this.ar.getUtxos(addr)
471
+ return utxos
472
+ } catch (err) {
473
+ throw this._sanitizeError(err, 'Failed to get UTXOs')
474
+ }
475
+ }
476
+
477
+ // Get the balance of the wallet in XEC.
478
+ async getXecBalance (inObj = {}) {
479
+ try {
480
+ // Handle backward compatibility: if inObj is a string, treat it as xecAddress
481
+ let xecAddress
482
+ if (typeof inObj === 'string') {
483
+ xecAddress = inObj
484
+ } else {
485
+ xecAddress = inObj.xecAddress
486
+ }
487
+ let addr = xecAddress
488
+
489
+ // Validate address if provided
490
+ if (xecAddress) {
491
+ this._validateAddress(xecAddress)
492
+ }
493
+
494
+ // If no address is passed in, but the wallet has been initialized, use the
495
+ // wallet's address.
496
+ if (!xecAddress && this.walletInfo && this.walletInfo.xecAddress) {
497
+ addr = this.walletInfo.xecAddress
498
+ }
499
+
500
+ if (!addr) {
501
+ throw new Error('No address provided and wallet not initialized')
502
+ }
503
+
504
+ const balances = await this.ar.getBalance(addr)
505
+ // Convert from satoshis to XEC (divide by 100, not 100,000,000 like BCH)
506
+ return (balances.balance.confirmed + balances.balance.unconfirmed) / 100
507
+ } catch (err) {
508
+ throw this._sanitizeError(err, 'Failed to get XEC balance')
509
+ }
510
+ }
511
+
512
+ // Get detailed balance information including confirmed and unconfirmed amounts
513
+ async getDetailedBalance (inObj = {}) {
514
+ try {
515
+ // Handle backward compatibility: if inObj is a string, treat it as xecAddress
516
+ let xecAddress
517
+ if (typeof inObj === 'string') {
518
+ xecAddress = inObj
519
+ } else {
520
+ xecAddress = inObj.xecAddress
521
+ }
522
+ let addr = xecAddress
523
+
524
+ // Validate address if provided
525
+ if (xecAddress) {
526
+ this._validateAddress(xecAddress)
527
+ }
528
+
529
+ // If no address is passed in, but the wallet has been initialized, use the
530
+ // wallet's address.
531
+ if (!xecAddress && this.walletInfo && this.walletInfo.xecAddress) {
532
+ addr = this.walletInfo.xecAddress
533
+ }
534
+
535
+ if (!addr) {
536
+ throw new Error('No address provided and wallet not initialized')
537
+ }
538
+
539
+ const balances = await this.ar.getBalance(addr)
540
+
541
+ // Convert from satoshis to XEC (divide by 100, not 100,000,000 like BCH)
542
+ const confirmed = balances.balance.confirmed / 100
543
+ const unconfirmed = balances.balance.unconfirmed / 100
544
+ const total = confirmed + unconfirmed
545
+
546
+ return {
547
+ confirmed,
548
+ unconfirmed,
549
+ total,
550
+ satoshis: {
551
+ confirmed: balances.balance.confirmed,
552
+ unconfirmed: balances.balance.unconfirmed,
553
+ total: balances.balance.confirmed + balances.balance.unconfirmed
554
+ }
555
+ }
556
+ } catch (err) {
557
+ throw this._sanitizeError(err, 'Failed to get detailed balance')
558
+ }
559
+ }
560
+
561
+ // Get transactions associated with the wallet.
562
+ async getTransactions (xecAddress, sortingOrder = 'DESCENDING') {
563
+ let addr = xecAddress
564
+
565
+ // If no address is passed in, but the wallet has been initialized, use the
566
+ // wallet's address.
567
+ if (!xecAddress && this.walletInfo && this.walletInfo.xecAddress) {
568
+ addr = this.walletInfo.xecAddress
569
+ }
570
+
571
+ const data = await this.ar.getTransactions(addr, sortingOrder)
572
+ return data.transactions
573
+ }
574
+
575
+ // Get transaction data for up to 20 TXIDs.
576
+ async getTxData (txids = []) {
577
+ const data = await this.ar.getTxData(txids)
578
+ return data
579
+ }
580
+
581
+ // Send XEC. Returns a promise that resolves into a TXID.
582
+ async sendXec (outputs) {
583
+ try {
584
+ // Wait for wallet to be initialized
585
+ await this.walletInfoPromise
586
+
587
+ if (!this.isInitialized) {
588
+ await this.initialize()
589
+ }
590
+
591
+ // Get XEC UTXOs - prefer non-token UTXOs to prevent accidental token burning
592
+ const xecOnlyUtxos = this.utxos.utxoStore.xecUtxos.filter(utxo => !utxo.token)
593
+
594
+ // If no pure XEC UTXOs available, provide helpful error
595
+ if (xecOnlyUtxos.length === 0) {
596
+ const tokenUtxoCount = this.utxos.utxoStore.xecUtxos.filter(utxo => utxo.token).length
597
+ throw new Error(`No pure XEC UTXOs available for transaction. All ${tokenUtxoCount} UTXOs contain tokens. To send XEC, first run wallet.optimize() to consolidate UTXOs and create pure XEC UTXOs, or use sendETokens() if you want to send tokens instead.`)
598
+ }
599
+
600
+ return await this.sendXecLib.sendXec(
601
+ outputs,
602
+ {
603
+ mnemonic: this.walletInfo.mnemonic,
604
+ xecAddress: this.walletInfo.xecAddress,
605
+ hdPath: this.walletInfo.hdPath,
606
+ fee: this.fee,
607
+ privateKey: this.walletInfo.privateKey,
608
+ publicKey: this.walletInfo.publicKey
609
+ },
610
+ xecOnlyUtxos
611
+ )
612
+ } catch (err) {
613
+ throw this._sanitizeError(err, 'XEC send failed')
614
+ }
615
+ }
616
+
617
+ // Send eTokens. Returns a promise that resolves into a TXID.
618
+ async sendETokens (tokenId, outputs, satsPerByte = this.fee) {
619
+ try {
620
+ // Wait for wallet to be initialized
621
+ await this.walletInfoPromise
622
+
623
+ if (!this.isInitialized) {
624
+ await this.initialize()
625
+ }
626
+
627
+ // Validate inputs
628
+ if (!tokenId || typeof tokenId !== 'string') {
629
+ throw new Error('Token ID is required and must be a string')
630
+ }
631
+
632
+ if (!Array.isArray(outputs) || outputs.length === 0) {
633
+ throw new Error('Outputs array is required and cannot be empty')
634
+ }
635
+
636
+ // Ensure UTXOs are loaded before token operations
637
+ if (!this.utxos || !this.utxos.utxoStore || !Array.isArray(this.utxos.utxoStore.xecUtxos)) {
638
+ throw new Error('Wallet UTXOs not loaded. Try calling initialize() first.')
639
+ }
640
+
641
+ // Use hybrid token manager for protocol detection and routing
642
+ return await this.hybridTokens.sendTokens(
643
+ tokenId,
644
+ outputs,
645
+ {
646
+ mnemonic: this.walletInfo.mnemonic,
647
+ xecAddress: this.walletInfo.xecAddress,
648
+ hdPath: this.walletInfo.hdPath,
649
+ fee: this.fee,
650
+ privateKey: this.walletInfo.privateKey,
651
+ publicKey: this.walletInfo.publicKey
652
+ },
653
+ this.utxos.utxoStore.xecUtxos,
654
+ satsPerByte
655
+ )
656
+ } catch (err) {
657
+ throw this._sanitizeError(err, 'eToken send failed')
658
+ }
659
+ }
660
+
661
+ // Send all XEC to an address
662
+ async sendAllXec (toAddress) {
663
+ try {
664
+ await this.walletInfoPromise
665
+
666
+ if (!this.isInitialized) {
667
+ await this.initialize()
668
+ }
669
+
670
+ return await this.sendXecLib.sendAllXec(
671
+ toAddress,
672
+ {
673
+ mnemonic: this.walletInfo.mnemonic,
674
+ xecAddress: this.walletInfo.xecAddress,
675
+ hdPath: this.walletInfo.hdPath,
676
+ fee: this.fee,
677
+ privateKey: this.walletInfo.privateKey,
678
+ publicKey: this.walletInfo.publicKey
679
+ },
680
+ this.utxos.utxoStore.xecUtxos
681
+ )
682
+ } catch (err) {
683
+ console.error('Error in sendAllXec():', err.message)
684
+ throw this._sanitizeError(err, 'Send all XEC failed')
685
+ }
686
+ }
687
+
688
+ // Send OP_RETURN transaction
689
+ async sendOpReturn (msg = '', prefix = '6d02', xecOutput = [], satsPerByte = 1.0) {
690
+ try {
691
+ await this.walletInfoPromise
692
+
693
+ if (!this.isInitialized) {
694
+ await this.initialize()
695
+ }
696
+
697
+ // Get XEC UTXOs for OP_RETURN - prefer non-token UTXOs to prevent accidental token burning
698
+ const xecOnlyUtxos = this.utxos.utxoStore.xecUtxos.filter(utxo => !utxo.token)
699
+
700
+ // If no pure XEC UTXOs available, provide helpful error
701
+ if (xecOnlyUtxos.length === 0) {
702
+ const tokenUtxoCount = this.utxos.utxoStore.xecUtxos.filter(utxo => utxo.token).length
703
+ throw new Error(`No pure XEC UTXOs available for OP_RETURN transaction. All ${tokenUtxoCount} UTXOs contain tokens. To send OP_RETURN, first run wallet.optimize() to consolidate UTXOs and create pure XEC UTXOs.`)
704
+ }
705
+
706
+ return await this.opReturn.sendOpReturn(
707
+ this.walletInfo,
708
+ xecOnlyUtxos,
709
+ msg,
710
+ prefix,
711
+ xecOutput,
712
+ satsPerByte
713
+ )
714
+ } catch (err) {
715
+ console.error('Error in sendOpReturn():', err.message)
716
+ throw this._sanitizeError(err, 'OP_RETURN send failed')
717
+ }
718
+ }
719
+
720
+ // Validate if a UTXO is still spendable
721
+ async utxoIsValid (utxo) {
722
+ try {
723
+ return await this.ar.utxoIsValid(utxo)
724
+ } catch (err) {
725
+ throw this._sanitizeError(err, 'UTXO validation failed')
726
+ }
727
+ }
728
+
729
+ // Get key pair for HD index
730
+ async getKeyPair (hdIndex = 0) {
731
+ try {
732
+ await this.walletInfoPromise
733
+
734
+ if (!this.walletInfo.mnemonic) {
735
+ throw new Error('Wallet does not have a mnemonic. Cannot generate key pair.')
736
+ }
737
+
738
+ const customPath = `m/44'/899'/0'/0/${hdIndex}`
739
+ const keyData = this.keyDerivation.deriveFromMnemonic(this.walletInfo.mnemonic, customPath)
740
+
741
+ return {
742
+ hdIndex,
743
+ wif: keyData.privateKey, // In real implementation, convert to WIF format
744
+ publicKey: keyData.publicKey,
745
+ xecAddress: keyData.address
746
+ }
747
+ } catch (err) {
748
+ throw this._sanitizeError(err, 'Key pair generation failed')
749
+ }
750
+ }
751
+
752
+ // Optimize wallet by consolidating UTXOs
753
+ async optimize (dryRun = false) {
754
+ try {
755
+ return await this.consolidateUtxos.start({ dryRun })
756
+ } catch (err) {
757
+ throw this._sanitizeError(err, 'UTXO optimization failed')
758
+ }
759
+ }
760
+
761
+ // Get public key for address
762
+ async getPubKey (addr) {
763
+ try {
764
+ return await this.ar.getPubKey(addr)
765
+ } catch (err) {
766
+ throw this._sanitizeError(err, 'Public key query failed')
767
+ }
768
+ }
769
+
770
+ // Broadcast transaction hex
771
+ async broadcast (inObj = {}) {
772
+ try {
773
+ const { hex } = inObj
774
+ if (!hex) {
775
+ throw new Error('Transaction hex is required')
776
+ }
777
+
778
+ return await this.ar.sendTx(hex)
779
+ } catch (err) {
780
+ throw this._sanitizeError(err, 'Transaction broadcast failed')
781
+ }
782
+ }
783
+
784
+ // Convert CID to JSON
785
+ async cid2json (inObj = {}) {
786
+ try {
787
+ return await this.ar.cid2json(inObj)
788
+ } catch (err) {
789
+ throw this._sanitizeError(err, 'CID to JSON conversion failed')
790
+ }
791
+ }
792
+
793
+ // Get the spot price of XEC in USD.
794
+ async getXecUsd () {
795
+ try {
796
+ return await this.ar.getXecUsd()
797
+ } catch (err) {
798
+ throw this._sanitizeError(err, 'XEC price query failed')
799
+ }
800
+ }
801
+
802
+ // eToken operations - Hybrid SLP/ALP token support
803
+ async listETokens (xecAddress) {
804
+ try {
805
+ // Wait for wallet to be initialized
806
+ await this.walletInfoPromise
807
+
808
+ // Determine address to use
809
+ let addr = xecAddress
810
+ if (!xecAddress && this.walletInfo && this.walletInfo.xecAddress) {
811
+ addr = this.walletInfo.xecAddress
812
+ }
813
+
814
+ if (!addr) {
815
+ throw new Error('No address provided and wallet not initialized')
816
+ }
817
+
818
+ // Validate address if provided
819
+ if (xecAddress) {
820
+ this._validateAddress(xecAddress)
821
+ }
822
+
823
+ // Use hybrid token manager to list tokens from address
824
+ return await this.hybridTokens.listTokensFromAddress(addr)
825
+ } catch (err) {
826
+ throw this._sanitizeError(err, 'eToken listing failed')
827
+ }
828
+ }
829
+
830
+ async getETokenBalance (inObj = {}) {
831
+ try {
832
+ // Wait for wallet to be initialized
833
+ await this.walletInfoPromise
834
+
835
+ // Extract tokenId from input object
836
+ const { tokenId, xecAddress } = inObj
837
+
838
+ if (!tokenId || typeof tokenId !== 'string') {
839
+ throw new Error('Token ID is required and must be a string')
840
+ }
841
+
842
+ // Determine address to use
843
+ let addr = xecAddress
844
+ if (!xecAddress && this.walletInfo && this.walletInfo.xecAddress) {
845
+ addr = this.walletInfo.xecAddress
846
+ }
847
+
848
+ if (!addr) {
849
+ throw new Error('No address provided and wallet not initialized')
850
+ }
851
+
852
+ // Validate address if provided
853
+ if (xecAddress) {
854
+ this._validateAddress(xecAddress)
855
+ }
856
+
857
+ // Get UTXOs for the address and calculate balance
858
+ const utxos = await this.getUtxos(addr)
859
+ return await this.hybridTokens.getTokenBalance(tokenId, utxos.utxos)
860
+ } catch (err) {
861
+ throw this._sanitizeError(err, 'eToken balance query failed')
862
+ }
863
+ }
864
+
865
+ async burnETokens (tokenId, amount, satsPerByte = this.fee) {
866
+ try {
867
+ // Wait for wallet to be initialized
868
+ await this.walletInfoPromise
869
+
870
+ if (!this.isInitialized) {
871
+ await this.initialize()
872
+ }
873
+
874
+ // Validate inputs
875
+ if (!tokenId || typeof tokenId !== 'string') {
876
+ throw new Error('Token ID is required and must be a string')
877
+ }
878
+
879
+ if (!amount || typeof amount !== 'number' || amount <= 0) {
880
+ throw new Error('Amount is required and must be a positive number')
881
+ }
882
+
883
+ // Use hybrid token manager for protocol detection and routing
884
+ return await this.hybridTokens.burnTokens(
885
+ tokenId,
886
+ amount,
887
+ {
888
+ mnemonic: this.walletInfo.mnemonic,
889
+ xecAddress: this.walletInfo.xecAddress,
890
+ hdPath: this.walletInfo.hdPath,
891
+ fee: this.fee,
892
+ privateKey: this.walletInfo.privateKey,
893
+ publicKey: this.walletInfo.publicKey
894
+ },
895
+ this.utxos.utxoStore.utxos,
896
+ satsPerByte
897
+ )
898
+ } catch (err) {
899
+ throw this._sanitizeError(err, 'eToken burn failed')
900
+ }
901
+ }
902
+
903
+ async burnAllETokens (tokenId, satsPerByte = this.fee) {
904
+ try {
905
+ // Wait for wallet to be initialized
906
+ await this.walletInfoPromise
907
+
908
+ if (!this.isInitialized) {
909
+ await this.initialize()
910
+ }
911
+
912
+ // Validate inputs
913
+ if (!tokenId || typeof tokenId !== 'string') {
914
+ throw new Error('Token ID is required and must be a string')
915
+ }
916
+
917
+ // Use hybrid token manager for protocol detection and routing
918
+ return await this.hybridTokens.burnAllTokens(
919
+ tokenId,
920
+ {
921
+ mnemonic: this.walletInfo.mnemonic,
922
+ xecAddress: this.walletInfo.xecAddress,
923
+ hdPath: this.walletInfo.hdPath,
924
+ fee: this.fee,
925
+ privateKey: this.walletInfo.privateKey,
926
+ publicKey: this.walletInfo.publicKey
927
+ },
928
+ this.utxos.utxoStore.utxos
929
+ )
930
+ } catch (err) {
931
+ throw this._sanitizeError(err, 'eToken burn all failed')
932
+ }
933
+ }
934
+
935
+ async getETokenData (tokenId, withTxHistory = false, sortOrder = 'DESCENDING') {
936
+ try {
937
+ // Validate inputs
938
+ if (!tokenId || typeof tokenId !== 'string') {
939
+ throw new Error('Token ID is required and must be a string')
940
+ }
941
+
942
+ // Use hybrid token manager to get comprehensive token data
943
+ return await this.hybridTokens.getTokenData(tokenId, withTxHistory, sortOrder)
944
+ } catch (err) {
945
+ throw this._sanitizeError(err, 'eToken data query failed')
946
+ }
947
+ }
948
+
949
+ // Export private key as WIF format
950
+ exportPrivateKeyAsWIF (compressed = true, testnet = false) {
951
+ try {
952
+ if (!this.walletInfo || !this.walletInfo.privateKey) {
953
+ throw new Error('Wallet not initialized or no private key available')
954
+ }
955
+
956
+ return this.keyDerivation.exportToWif(
957
+ this.walletInfo.privateKey,
958
+ compressed,
959
+ testnet
960
+ )
961
+ } catch (err) {
962
+ throw this._sanitizeError(err, 'WIF export failed')
963
+ }
964
+ }
965
+
966
+ // Validate WIF format (public utility method)
967
+ validateWIF (wif) {
968
+ try {
969
+ return this.keyDerivation._isValidWIF(wif)
970
+ } catch (err) {
971
+ return false
972
+ }
973
+ }
974
+ }
975
+
976
+ module.exports = MinimalXECWallet
@@ -91,8 +91,8 @@ class ConsolidateUtxos {
91
91
 
92
92
  // Filter UTXOs that should be consolidated (smaller ones first)
93
93
  const utxosToConsolidate = pureXecUtxos
94
- .filter(utxo => utxo.value <= options.consolidationThreshold)
95
- .sort((a, b) => a.value - b.value) // Sort by value ascending
94
+ .filter(utxo => this._getUtxoValue(utxo) <= options.consolidationThreshold)
95
+ .sort((a, b) => this._getUtxoValue(a) - this._getUtxoValue(b)) // Sort by value ascending
96
96
 
97
97
  if (utxosToConsolidate.length < this.minUtxosForConsolidation) {
98
98
  return {
@@ -267,7 +267,17 @@ class ConsolidateUtxos {
267
267
  // Helper methods
268
268
 
269
269
  _calculateTotalValue (utxos) {
270
- return utxos.reduce((total, utxo) => total + utxo.value, 0)
270
+ return utxos.reduce((total, utxo) => total + this._getUtxoValue(utxo), 0)
271
+ }
272
+
273
+ _getUtxoValue (utxo) {
274
+ if (utxo.sats !== undefined) {
275
+ return typeof utxo.sats === 'bigint' ? Number(utxo.sats) : parseInt(utxo.sats)
276
+ }
277
+ if (utxo.value !== undefined) {
278
+ return typeof utxo.value === 'bigint' ? Number(utxo.value) : parseInt(utxo.value)
279
+ }
280
+ return 0
271
281
  }
272
282
 
273
283
  _calculateConsolidationFee (numInputs, numOutputs, satsPerByte) {
@@ -317,11 +327,12 @@ class ConsolidateUtxos {
317
327
 
318
328
  // Only analyze pure XEC UTXOs for consolidation
319
329
  for (const utxo of pureXecUtxos) {
320
- if (utxo.value < 1000) {
330
+ const value = this._getUtxoValue(utxo)
331
+ if (value < 1000) {
321
332
  distribution.dust++
322
- } else if (utxo.value < 10000) {
333
+ } else if (value < 10000) {
323
334
  distribution.small++
324
- } else if (utxo.value < 100000) {
335
+ } else if (value < 100000) {
325
336
  distribution.medium++
326
337
  } else {
327
338
  distribution.large++
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimal-xec-wallet",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "A minimalist eCash (XEC) wallet npm library, for use in web apps. Supports eTokens.",
5
5
  "main": "./index.js",
6
6
  "module": "./dist/minimal-xec-wallet.min.js",
@@ -11,7 +11,8 @@
11
11
  "files": [
12
12
  "dist/",
13
13
  "examples/",
14
- "lib/"
14
+ "lib/",
15
+ "index.js"
15
16
  ],
16
17
  "unpkg": "dist/minimal-xec-wallet.min.js",
17
18
  "scripts": {