minimal-xec-wallet 1.0.1

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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -0
  3. package/dist/minimal-xec-wallet.js +66268 -0
  4. package/dist/minimal-xec-wallet.min.js +55 -0
  5. package/examples/README.md +380 -0
  6. package/examples/advanced/browser-compatibility-test.js +263 -0
  7. package/examples/advanced/get-xec-price.js +149 -0
  8. package/examples/advanced/optimize-utxos.js +255 -0
  9. package/examples/advanced/send-op-return.js +216 -0
  10. package/examples/browser-test.html +350 -0
  11. package/examples/key-management/derive-addresses.js +191 -0
  12. package/examples/key-management/export-to-wif.js +114 -0
  13. package/examples/key-management/validate-address.js +214 -0
  14. package/examples/optimization/simple-consolidation-test.js +79 -0
  15. package/examples/optimization/test-utxo-consolidation.js +179 -0
  16. package/examples/test-examples.js +1204 -0
  17. package/examples/tokens/burn-tokens.js +293 -0
  18. package/examples/tokens/get-token-balance.js +169 -0
  19. package/examples/tokens/get-token-info.js +269 -0
  20. package/examples/tokens/list-all-tokens.js +162 -0
  21. package/examples/tokens/send-any-token.js +260 -0
  22. package/examples/tokens/test-main-wallet-integration.js +193 -0
  23. package/examples/transactions/send-all-xec.js +205 -0
  24. package/examples/transactions/send-to-multiple.js +217 -0
  25. package/examples/transactions/send-xec.js +191 -0
  26. package/examples/utils/show-qr.js +119 -0
  27. package/examples/utils/wallet-helper.js +176 -0
  28. package/examples/validation/comprehensive-infrastructure-test.js +210 -0
  29. package/examples/wallet-creation/create-new-wallet.js +67 -0
  30. package/examples/wallet-creation/import-from-wif.js +135 -0
  31. package/examples/wallet-creation/restore-from-mnemonic.js +100 -0
  32. package/examples/wallet-info/get-balance.js +99 -0
  33. package/examples/wallet-info/get-transactions.js +157 -0
  34. package/examples/wallet-info/get-utxos.js +145 -0
  35. package/examples/wallet.json +11 -0
  36. package/lib/adapters/robust-chronik-router.js +507 -0
  37. package/lib/adapters/router.js +651 -0
  38. package/lib/alp-token-handler.js +581 -0
  39. package/lib/browser-wasm-loader.js +271 -0
  40. package/lib/consolidate-utxos.js +338 -0
  41. package/lib/hybrid-token-manager.js +322 -0
  42. package/lib/key-derivation.js +466 -0
  43. package/lib/op-return.js +314 -0
  44. package/lib/security.js +270 -0
  45. package/lib/send-xec.js +396 -0
  46. package/lib/slp-token-handler.js +572 -0
  47. package/lib/token-protocol-detector.js +307 -0
  48. package/lib/utxos.js +303 -0
  49. package/package.json +125 -0
@@ -0,0 +1,581 @@
1
+ /*
2
+ ALP Token Handler - Uses native ecash-lib ALP functions
3
+ Handles A Ledger Protocol token operations
4
+ */
5
+
6
+ const {
7
+ TxBuilder,
8
+ P2PKHSignatory,
9
+ Script,
10
+ fromHex,
11
+ toHex,
12
+ Ecc,
13
+ alpSend,
14
+ alpBurn,
15
+ emppScript,
16
+ ALL_BIP143
17
+ } = require('ecash-lib')
18
+ const { decodeCashAddress } = require('ecashaddrjs')
19
+ const KeyDerivation = require('./key-derivation')
20
+ const SecurityValidator = require('./security')
21
+
22
+ class ALPTokenHandler {
23
+ constructor (localConfig = {}) {
24
+ this.chronik = localConfig.chronik
25
+ this.ar = localConfig.ar
26
+
27
+ if (!this.chronik) {
28
+ throw new Error('Chronik client required for ALP token operations')
29
+ }
30
+
31
+ if (!this.ar) {
32
+ throw new Error('AdapterRouter required for ALP token operations')
33
+ }
34
+
35
+ // Initialize components
36
+ this.keyDerivation = new KeyDerivation()
37
+ this.security = new SecurityValidator(localConfig.security)
38
+
39
+ // Initialize ECC for ecash-lib
40
+ try {
41
+ this.ecc = new Ecc()
42
+ } catch (err) {
43
+ throw new Error(`Ecc initialization failed: ${err.message}`)
44
+ }
45
+
46
+ // Configuration
47
+ this.dustLimit = localConfig.dustLimit || 546
48
+ this.defaultSatsPerByte = localConfig.defaultSatsPerByte || 1.2
49
+ this.ALP_STANDARD = 0 // Standard ALP token type
50
+ }
51
+
52
+ async sendTokens (tokenId, outputs, walletInfo, utxos, satsPerByte = this.defaultSatsPerByte) {
53
+ try {
54
+ const txHex = await this.createSendTransaction(tokenId, outputs, walletInfo, utxos, satsPerByte)
55
+ const txid = await this.ar.sendTx(txHex)
56
+ return txid
57
+ } catch (err) {
58
+ throw new Error(`ALP token send failed: ${err.message}`)
59
+ }
60
+ }
61
+
62
+ async burnTokens (tokenId, amount, walletInfo, utxos, satsPerByte = this.defaultSatsPerByte) {
63
+ try {
64
+ const txHex = await this.createBurnTransaction(tokenId, amount, walletInfo, utxos, satsPerByte)
65
+ const txid = await this.ar.sendTx(txHex)
66
+ return txid
67
+ } catch (err) {
68
+ throw new Error(`ALP token burn failed: ${err.message}`)
69
+ }
70
+ }
71
+
72
+ async createSendTransaction (tokenId, outputs, walletInfo, utxos, satsPerByte = this.defaultSatsPerByte) {
73
+ try {
74
+ // Validate inputs
75
+ if (!walletInfo || !walletInfo.xecAddress) {
76
+ throw new Error('Valid wallet info required')
77
+ }
78
+
79
+ if (!tokenId || typeof tokenId !== 'string') {
80
+ throw new Error('Valid token ID required')
81
+ }
82
+
83
+ if (!Array.isArray(outputs) || outputs.length === 0) {
84
+ throw new Error('Valid outputs array required')
85
+ }
86
+
87
+ if (outputs.length > 19) {
88
+ throw new Error('Too many outputs - ALP limit is 19 recipients per transaction')
89
+ }
90
+
91
+ // Get token metadata for validation
92
+ const tokenInfo = await this.chronik.token(tokenId)
93
+ if (tokenInfo.tokenType.protocol !== 'ALP') {
94
+ throw new Error('Token is not an ALP token')
95
+ }
96
+
97
+ // Filter UTXOs by type
98
+ const { alpUtxos, xecUtxos } = this._categorizeUtxos(utxos, tokenId)
99
+
100
+ if (alpUtxos.length === 0) {
101
+ throw new Error(`No ${tokenInfo.genesisInfo.tokenTicker} tokens found in wallet`)
102
+ }
103
+
104
+ // Calculate required token amounts in atoms
105
+ const atomOutputs = outputs.map(output => ({
106
+ ...output,
107
+ atoms: this._displayToAtoms(output.value || output.amount, tokenInfo.genesisInfo.decimals)
108
+ }))
109
+
110
+ const totalRequiredAtoms = atomOutputs.reduce((sum, output) => sum + output.atoms, 0n)
111
+
112
+ // Select token UTXOs
113
+ const tokenSelection = this._selectTokenUtxos(alpUtxos, totalRequiredAtoms, tokenInfo)
114
+
115
+ // Calculate total XEC requirement: dust outputs + fees
116
+ const tokenChangeAmount = tokenSelection.totalSelected - totalRequiredAtoms
117
+ const dustOutputsNeeded = outputs.length + (tokenChangeAmount > 0n ? 1 : 0) // recipient + change (if needed)
118
+ const dustRequirement = dustOutputsNeeded * this.dustLimit
119
+
120
+ // Select XEC UTXOs for total requirement (dust + fees) - iterative approach
121
+ const baseInputs = tokenSelection.selectedUtxos.length
122
+ const baseOutputs = outputs.length + 1 + (tokenChangeAmount > 0n ? 1 : 0) // outputs + OP_RETURN + change (if needed)
123
+
124
+ // Start with base fee estimate
125
+ let estimatedFee = this._estimateTransactionFee(baseInputs, baseOutputs, satsPerByte)
126
+ const totalXecRequired = dustRequirement + estimatedFee
127
+
128
+ // Try XEC selection with initial estimate
129
+ let feeSelection = this._selectXecUtxos(xecUtxos, totalXecRequired, tokenSelection.selectedUtxos)
130
+
131
+ // If we need additional XEC inputs, recalculate fee and check if we need even more UTXOs
132
+ if (feeSelection.selectedUtxos.length > 0) {
133
+ const newInputs = baseInputs + feeSelection.selectedUtxos.length
134
+ estimatedFee = this._estimateTransactionFee(newInputs, baseOutputs, satsPerByte)
135
+ const newTotalRequired = dustRequirement + estimatedFee
136
+
137
+ // If the new fee requirement exceeds what we selected, try again
138
+ if (newTotalRequired > totalXecRequired) {
139
+ feeSelection = this._selectXecUtxos(xecUtxos, newTotalRequired, tokenSelection.selectedUtxos)
140
+
141
+ // Final fee calculation with actual selected inputs
142
+ const finalInputs = baseInputs + feeSelection.selectedUtxos.length
143
+ estimatedFee = this._estimateTransactionFee(finalInputs, baseOutputs, satsPerByte)
144
+ }
145
+ }
146
+
147
+ // Get private key
148
+ const privateKeyHex = this._getPrivateKey(walletInfo)
149
+ const sk = fromHex(privateKeyHex)
150
+ const pk = this.ecc.derivePubkey(sk)
151
+
152
+ // Build ALP script with eMPP
153
+ const sendAmounts = atomOutputs.map(output => output.atoms)
154
+
155
+ // Add change amount if needed
156
+ const changeAtoms = tokenSelection.totalSelected - totalRequiredAtoms
157
+ if (changeAtoms > 0n) {
158
+ sendAmounts.push(changeAtoms)
159
+ }
160
+
161
+ const alpScript = emppScript([
162
+ alpSend(tokenId, this.ALP_STANDARD, sendAmounts)
163
+ ])
164
+
165
+ // Build transaction inputs
166
+ const inputs = [
167
+ // Token inputs
168
+ ...tokenSelection.selectedUtxos.map(utxo => ({
169
+ input: {
170
+ prevOut: utxo.outpoint,
171
+ signData: {
172
+ sats: BigInt(this._getUtxoValue(utxo)), // Use actual UTXO value
173
+ outputScript: this._getOutputScript(walletInfo.xecAddress)
174
+ }
175
+ },
176
+ signatory: P2PKHSignatory(sk, pk, ALL_BIP143)
177
+ }))
178
+ ]
179
+
180
+ // Add additional XEC inputs if needed
181
+ for (const utxo of feeSelection.selectedUtxos) {
182
+ inputs.push({
183
+ input: {
184
+ prevOut: utxo.outpoint,
185
+ signData: {
186
+ sats: BigInt(this._getUtxoValue(utxo)),
187
+ outputScript: this._getOutputScript(walletInfo.xecAddress)
188
+ }
189
+ },
190
+ signatory: P2PKHSignatory(sk, pk, ALL_BIP143)
191
+ })
192
+ }
193
+
194
+ // Build transaction outputs with EXPLICIT amounts
195
+ const txOutputs = [
196
+ // 1. ALP OP_RETURN output (always first)
197
+ {
198
+ sats: 0n,
199
+ script: new Script(alpScript.bytecode)
200
+ },
201
+ // 2. Token outputs to recipients (DUST ONLY - 546 sats each)
202
+ ...outputs.map(output => ({
203
+ sats: BigInt(this.dustLimit), // EXACTLY 546 sats for token
204
+ script: this._getOutputScript(output.address)
205
+ }))
206
+ ]
207
+
208
+ // 3. Token change output if needed (DUST ONLY - 546 sats)
209
+ if (changeAtoms > 0n) {
210
+ txOutputs.push({
211
+ sats: BigInt(this.dustLimit), // EXACTLY 546 sats for token change
212
+ script: this._getOutputScript(walletInfo.xecAddress)
213
+ })
214
+ }
215
+
216
+ // 4. XEC change output - calculate from all XEC inputs
217
+ // Calculate total XEC input from both token UTXOs and additional XEC UTXOs
218
+ const xecFromTokens = tokenSelection.selectedUtxos.reduce((total, utxo) => {
219
+ return total + this._getUtxoValue(utxo)
220
+ }, 0)
221
+
222
+ const xecFromAdditionalInputs = feeSelection.selectedUtxos.reduce((total, utxo) => {
223
+ return total + this._getUtxoValue(utxo)
224
+ }, 0)
225
+
226
+ const totalInputXec = BigInt(xecFromTokens + xecFromAdditionalInputs)
227
+ const totalTokenOutputs = BigInt(outputs.length * this.dustLimit) +
228
+ (changeAtoms > 0n ? BigInt(this.dustLimit) : 0n)
229
+ const estimatedFeeInSats = BigInt(estimatedFee)
230
+ const xecChange = totalInputXec - totalTokenOutputs - estimatedFeeInSats
231
+
232
+ if (xecChange >= BigInt(this.dustLimit)) {
233
+ txOutputs.push({
234
+ sats: xecChange,
235
+ script: this._getOutputScript(walletInfo.xecAddress)
236
+ })
237
+ }
238
+
239
+ // Build and sign transaction
240
+ const txBuilder = new TxBuilder({ inputs, outputs: txOutputs })
241
+ const tx = txBuilder.sign({
242
+ feePerKb: BigInt(Math.round(satsPerByte * 1000)),
243
+ dustSats: BigInt(this.dustLimit)
244
+ })
245
+
246
+ return toHex(tx.ser())
247
+ } catch (err) {
248
+ // Provide better error messages for common issues
249
+ if (err.message.includes('Cannot be converted to a BigInt') || err.message.includes('NaN')) {
250
+ throw new Error('Insufficient XEC for transaction fees')
251
+ }
252
+ throw new Error(`ALP send transaction creation failed: ${err.message}`)
253
+ }
254
+ }
255
+
256
+ async createBurnTransaction (tokenId, amount, walletInfo, utxos, satsPerByte) {
257
+ try {
258
+ // Get token metadata
259
+ const tokenInfo = await this.chronik.token(tokenId)
260
+ if (tokenInfo.tokenType.protocol !== 'ALP') {
261
+ throw new Error('Token is not an ALP token')
262
+ }
263
+
264
+ // Filter UTXOs
265
+ const { alpUtxos, xecUtxos } = this._categorizeUtxos(utxos, tokenId)
266
+
267
+ if (alpUtxos.length === 0) {
268
+ throw new Error(`No ${tokenInfo.genesisInfo.tokenTicker} tokens found to burn`)
269
+ }
270
+
271
+ // Calculate burn amount in atoms
272
+ const burnAtoms = this._displayToAtoms(amount, tokenInfo.genesisInfo.decimals)
273
+
274
+ // Select token UTXOs for burning
275
+ const tokenSelection = this._selectTokenUtxos(alpUtxos, burnAtoms, tokenInfo)
276
+
277
+ // Calculate total XEC requirement: dust outputs + fees
278
+ const tokenChangeAmount = tokenSelection.totalSelected - burnAtoms
279
+ const dustOutputsNeeded = tokenChangeAmount > 0n ? 1 : 0 // only change output (if needed)
280
+ const dustRequirement = dustOutputsNeeded * this.dustLimit
281
+
282
+ // Select XEC UTXOs for total requirement (dust + fees) - iterative approach
283
+ const baseInputs = tokenSelection.selectedUtxos.length
284
+ const baseOutputs = 1 + (tokenChangeAmount > 0n ? 1 : 0) // OP_RETURN + change (if needed)
285
+
286
+ // Start with base fee estimate
287
+ let estimatedFee = this._estimateTransactionFee(baseInputs, baseOutputs, satsPerByte)
288
+ const totalXecRequired = dustRequirement + estimatedFee
289
+
290
+ // Try XEC selection with initial estimate
291
+ let feeSelection = this._selectXecUtxos(xecUtxos, totalXecRequired, tokenSelection.selectedUtxos)
292
+
293
+ // If we need additional XEC inputs, recalculate fee and check if we need even more UTXOs
294
+ if (feeSelection.selectedUtxos.length > 0) {
295
+ const newInputs = baseInputs + feeSelection.selectedUtxos.length
296
+ estimatedFee = this._estimateTransactionFee(newInputs, baseOutputs, satsPerByte)
297
+ const newTotalRequired = dustRequirement + estimatedFee
298
+
299
+ // If the new fee requirement exceeds what we selected, try again
300
+ if (newTotalRequired > totalXecRequired) {
301
+ feeSelection = this._selectXecUtxos(xecUtxos, newTotalRequired, tokenSelection.selectedUtxos)
302
+
303
+ // Final fee calculation with actual selected inputs
304
+ const finalInputs = baseInputs + feeSelection.selectedUtxos.length
305
+ estimatedFee = this._estimateTransactionFee(finalInputs, baseOutputs, satsPerByte)
306
+ }
307
+ }
308
+
309
+ // Get private key
310
+ const privateKeyHex = this._getPrivateKey(walletInfo)
311
+ const sk = fromHex(privateKeyHex)
312
+ const pk = this.ecc.derivePubkey(sk)
313
+
314
+ // Build ALP script for burn operation
315
+ let alpScript
316
+ if (tokenChangeAmount > 0n) {
317
+ // Partial burn: use SEND transaction with only change amount (burns by omission)
318
+ alpScript = emppScript([
319
+ alpSend(tokenId, this.ALP_STANDARD, [tokenChangeAmount])
320
+ ])
321
+ } else {
322
+ // Complete burn: use BURN transaction (burns all input tokens)
323
+ alpScript = emppScript([
324
+ alpBurn(tokenId, this.ALP_STANDARD, burnAtoms)
325
+ ])
326
+ }
327
+
328
+ // Build inputs
329
+ const inputs = [
330
+ // Token inputs
331
+ ...tokenSelection.selectedUtxos.map(utxo => ({
332
+ input: {
333
+ prevOut: utxo.outpoint,
334
+ signData: {
335
+ sats: BigInt(this._getUtxoValue(utxo)), // Use actual UTXO value
336
+ outputScript: this._getOutputScript(walletInfo.xecAddress)
337
+ }
338
+ },
339
+ signatory: P2PKHSignatory(sk, pk, ALL_BIP143)
340
+ }))
341
+ ]
342
+
343
+ // Add additional XEC inputs if needed
344
+ for (const utxo of feeSelection.selectedUtxos) {
345
+ inputs.push({
346
+ input: {
347
+ prevOut: utxo.outpoint,
348
+ signData: {
349
+ sats: BigInt(this._getUtxoValue(utxo)),
350
+ outputScript: this._getOutputScript(walletInfo.xecAddress)
351
+ }
352
+ },
353
+ signatory: P2PKHSignatory(sk, pk, ALL_BIP143)
354
+ })
355
+ }
356
+
357
+ // Build outputs
358
+ const txOutputs = [
359
+ // ALP burn OP_RETURN
360
+ {
361
+ sats: 0n,
362
+ script: new Script(alpScript.bytecode)
363
+ }
364
+ ]
365
+
366
+ // Add token change if not burning all (DUST ONLY - 546 sats)
367
+ const changeAtoms = tokenSelection.totalSelected - burnAtoms
368
+ if (changeAtoms > 0n) {
369
+ txOutputs.push({
370
+ sats: BigInt(this.dustLimit), // EXACTLY 546 sats for token change
371
+ script: this._getOutputScript(walletInfo.xecAddress)
372
+ })
373
+ }
374
+
375
+ // XEC change output - calculate from all XEC inputs
376
+ // Calculate total XEC input from both token UTXOs and additional XEC UTXOs
377
+ const xecFromTokens = tokenSelection.selectedUtxos.reduce((total, utxo) => {
378
+ return total + this._getUtxoValue(utxo)
379
+ }, 0)
380
+
381
+ const xecFromAdditionalInputs = feeSelection.selectedUtxos.reduce((total, utxo) => {
382
+ return total + this._getUtxoValue(utxo)
383
+ }, 0)
384
+
385
+ const totalInputXec = BigInt(xecFromTokens + xecFromAdditionalInputs)
386
+ const totalTokenOutputs = changeAtoms > 0n ? BigInt(this.dustLimit) : 0n
387
+ const estimatedFeeInSats = BigInt(estimatedFee)
388
+ const xecChange = totalInputXec - totalTokenOutputs - estimatedFeeInSats
389
+
390
+ if (xecChange >= BigInt(this.dustLimit)) {
391
+ txOutputs.push({
392
+ sats: xecChange,
393
+ script: this._getOutputScript(walletInfo.xecAddress)
394
+ })
395
+ }
396
+
397
+ // Build and sign transaction
398
+ const txBuilder = new TxBuilder({ inputs, outputs: txOutputs })
399
+ const tx = txBuilder.sign({
400
+ feePerKb: BigInt(Math.round(satsPerByte * 1000)),
401
+ dustSats: BigInt(this.dustLimit)
402
+ })
403
+
404
+ return toHex(tx.ser())
405
+ } catch (err) {
406
+ throw new Error(`ALP burn transaction creation failed: ${err.message}`)
407
+ }
408
+ }
409
+
410
+ // Helper methods
411
+
412
+ _categorizeUtxos (utxos, tokenId) {
413
+ const alpUtxos = utxos.filter(utxo =>
414
+ utxo && utxo.token &&
415
+ utxo.token.tokenId === tokenId &&
416
+ utxo.token.tokenType?.protocol === 'ALP'
417
+ )
418
+
419
+ // Pure XEC UTXOs (no token data)
420
+ const pureXecUtxos = utxos.filter(utxo => utxo && !utxo.token)
421
+
422
+ // Other token UTXOs (different tokens) - their XEC can be used for fees
423
+ const otherTokenUtxos = utxos.filter(utxo =>
424
+ utxo && utxo.token &&
425
+ utxo.token.tokenId !== tokenId
426
+ )
427
+
428
+ // Only use pure XEC UTXOs for fees to avoid token burns
429
+ const xecUtxos = pureXecUtxos
430
+
431
+ return { alpUtxos, xecUtxos, pureXecUtxos, otherTokenUtxos }
432
+ }
433
+
434
+ _selectTokenUtxos (alpUtxos, requiredAtoms, tokenInfo) {
435
+ // Sort by atoms amount (largest first)
436
+ const sortedUtxos = alpUtxos
437
+ .slice()
438
+ .sort((a, b) => {
439
+ const aAtoms = BigInt(a.token.atoms)
440
+ const bAtoms = BigInt(b.token.atoms)
441
+ return aAtoms > bAtoms ? -1 : aAtoms < bAtoms ? 1 : 0
442
+ })
443
+
444
+ const selectedUtxos = []
445
+ let totalSelected = 0n
446
+
447
+ for (const utxo of sortedUtxos) {
448
+ selectedUtxos.push(utxo)
449
+ totalSelected += BigInt(utxo.token.atoms)
450
+
451
+ if (totalSelected >= requiredAtoms) {
452
+ return { selectedUtxos, totalSelected }
453
+ }
454
+ }
455
+
456
+ throw new Error(
457
+ `Insufficient ${tokenInfo.genesisInfo.tokenTicker} tokens. ` +
458
+ `Need: ${this._atomsToDisplay(requiredAtoms, tokenInfo.genesisInfo.decimals)}, ` +
459
+ `Available: ${this._atomsToDisplay(totalSelected, tokenInfo.genesisInfo.decimals)}`
460
+ )
461
+ }
462
+
463
+ _selectXecUtxos (xecUtxos, requiredSats, tokenUtxosBeingSpent = []) {
464
+ // Calculate total XEC available from token UTXOs being spent
465
+ const xecFromTokenUtxos = tokenUtxosBeingSpent.reduce((total, utxo) => {
466
+ return total + this._getUtxoValue(utxo)
467
+ }, 0)
468
+
469
+ // If token UTXOs provide enough XEC for fees, no additional XEC input needed
470
+ if (xecFromTokenUtxos >= requiredSats) {
471
+ return { selectedUtxos: [], xecFromTokens: xecFromTokenUtxos }
472
+ }
473
+
474
+ // Otherwise, need additional XEC UTXOs
475
+ const additionalXecNeeded = requiredSats - xecFromTokenUtxos
476
+
477
+ // Sort available XEC UTXOs by value (largest first)
478
+ const sortedUtxos = xecUtxos
479
+ .slice()
480
+ .sort((a, b) => this._getUtxoValue(b) - this._getUtxoValue(a))
481
+
482
+ if (sortedUtxos.length === 0) {
483
+ throw new Error(`Insufficient XEC for transaction fees. Need ${requiredSats} sats, have ${xecFromTokenUtxos} from tokens`)
484
+ }
485
+
486
+ // Select multiple UTXOs if needed to meet the requirement
487
+ const selectedUtxos = []
488
+ let selectedXec = 0
489
+
490
+ for (const utxo of sortedUtxos) {
491
+ selectedUtxos.push(utxo)
492
+ selectedXec += this._getUtxoValue(utxo)
493
+
494
+ if (selectedXec >= additionalXecNeeded) {
495
+ break
496
+ }
497
+ }
498
+
499
+ if (selectedXec < additionalXecNeeded) {
500
+ throw new Error(`Insufficient XEC for transaction fees. Need ${requiredSats} sats, have ${xecFromTokenUtxos} from tokens + ${selectedXec} from UTXOs`)
501
+ }
502
+
503
+ return { selectedUtxos, xecFromTokens: xecFromTokenUtxos }
504
+ }
505
+
506
+ _displayToAtoms (displayAmount, decimals) {
507
+ if (decimals === 0) {
508
+ return BigInt(Math.floor(displayAmount))
509
+ }
510
+
511
+ const atoms = Math.floor(displayAmount * Math.pow(10, decimals))
512
+ return BigInt(atoms)
513
+ }
514
+
515
+ _atomsToDisplay (atoms, decimals) {
516
+ if (decimals === 0) {
517
+ return Number(atoms)
518
+ }
519
+
520
+ return Number(atoms) / Math.pow(10, decimals)
521
+ }
522
+
523
+ _estimateTransactionFee (numInputs, numOutputs, satsPerByte) {
524
+ const estimatedSize = (numInputs * 148) + (numOutputs * 34) + 50 // +50 for eMPP script
525
+ return Math.ceil(estimatedSize * satsPerByte)
526
+ }
527
+
528
+ _getPrivateKey (walletInfo) {
529
+ if (walletInfo.mnemonic) {
530
+ const keyData = this.keyDerivation.deriveFromMnemonic(walletInfo.mnemonic, walletInfo.hdPath)
531
+ return keyData.privateKey
532
+ } else {
533
+ return walletInfo.privateKey
534
+ }
535
+ }
536
+
537
+ _getOutputScript (address) {
538
+ const decoded = decodeCashAddress(address)
539
+ return Script.p2pkh(fromHex(decoded.hash))
540
+ }
541
+
542
+ _getUtxoValue (utxo) {
543
+ if (!utxo) return 0
544
+
545
+ // Try sats property first (this is the correct property name)
546
+ if (utxo.sats !== undefined) {
547
+ if (typeof utxo.sats === 'bigint') {
548
+ return Number(utxo.sats)
549
+ }
550
+ if (typeof utxo.sats === 'string') {
551
+ const parsed = parseInt(utxo.sats)
552
+ if (isNaN(parsed)) {
553
+ console.warn(`Invalid UTXO sats value: ${utxo.sats}`)
554
+ return 0
555
+ }
556
+ return parsed
557
+ }
558
+ if (typeof utxo.sats === 'number') {
559
+ return utxo.sats
560
+ }
561
+ }
562
+
563
+ // Fallback to value property if available (though this seems to be undefined)
564
+ if (utxo.value !== undefined) {
565
+ if (typeof utxo.value === 'bigint') {
566
+ return Number(utxo.value)
567
+ }
568
+ if (typeof utxo.value === 'string') {
569
+ const parsed = parseInt(utxo.value)
570
+ return isNaN(parsed) ? 0 : parsed
571
+ }
572
+ if (typeof utxo.value === 'number') {
573
+ return utxo.value
574
+ }
575
+ }
576
+
577
+ return 0
578
+ }
579
+ }
580
+
581
+ module.exports = ALPTokenHandler