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