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,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
|