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.
- package/LICENSE +21 -0
- package/README.md +241 -0
- package/dist/minimal-xec-wallet.js +66268 -0
- package/dist/minimal-xec-wallet.min.js +55 -0
- package/examples/README.md +380 -0
- package/examples/advanced/browser-compatibility-test.js +263 -0
- package/examples/advanced/get-xec-price.js +149 -0
- package/examples/advanced/optimize-utxos.js +255 -0
- package/examples/advanced/send-op-return.js +216 -0
- package/examples/browser-test.html +350 -0
- package/examples/key-management/derive-addresses.js +191 -0
- package/examples/key-management/export-to-wif.js +114 -0
- package/examples/key-management/validate-address.js +214 -0
- package/examples/optimization/simple-consolidation-test.js +79 -0
- package/examples/optimization/test-utxo-consolidation.js +179 -0
- package/examples/test-examples.js +1204 -0
- package/examples/tokens/burn-tokens.js +293 -0
- package/examples/tokens/get-token-balance.js +169 -0
- package/examples/tokens/get-token-info.js +269 -0
- package/examples/tokens/list-all-tokens.js +162 -0
- package/examples/tokens/send-any-token.js +260 -0
- package/examples/tokens/test-main-wallet-integration.js +193 -0
- package/examples/transactions/send-all-xec.js +205 -0
- package/examples/transactions/send-to-multiple.js +217 -0
- package/examples/transactions/send-xec.js +191 -0
- package/examples/utils/show-qr.js +119 -0
- package/examples/utils/wallet-helper.js +176 -0
- package/examples/validation/comprehensive-infrastructure-test.js +210 -0
- package/examples/wallet-creation/create-new-wallet.js +67 -0
- package/examples/wallet-creation/import-from-wif.js +135 -0
- package/examples/wallet-creation/restore-from-mnemonic.js +100 -0
- package/examples/wallet-info/get-balance.js +99 -0
- package/examples/wallet-info/get-transactions.js +157 -0
- package/examples/wallet-info/get-utxos.js +145 -0
- package/examples/wallet.json +11 -0
- package/lib/adapters/robust-chronik-router.js +507 -0
- package/lib/adapters/router.js +651 -0
- package/lib/alp-token-handler.js +581 -0
- package/lib/browser-wasm-loader.js +271 -0
- package/lib/consolidate-utxos.js +338 -0
- package/lib/hybrid-token-manager.js +322 -0
- package/lib/key-derivation.js +466 -0
- package/lib/op-return.js +314 -0
- package/lib/security.js +270 -0
- package/lib/send-xec.js +396 -0
- package/lib/slp-token-handler.js +572 -0
- package/lib/token-protocol-detector.js +307 -0
- package/lib/utxos.js +303 -0
- 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
|