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 +976 -0
- package/lib/consolidate-utxos.js +17 -6
- package/package.json +3 -2
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
|
package/lib/consolidate-utxos.js
CHANGED
|
@@ -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
|
|
95
|
-
.sort((a, b) => a
|
|
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
|
|
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
|
-
|
|
330
|
+
const value = this._getUtxoValue(utxo)
|
|
331
|
+
if (value < 1000) {
|
|
321
332
|
distribution.dust++
|
|
322
|
-
} else if (
|
|
333
|
+
} else if (value < 10000) {
|
|
323
334
|
distribution.small++
|
|
324
|
-
} else if (
|
|
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
|
+
"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": {
|