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
package/lib/send-xec.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Simplified XEC transaction creation for minimal wallet
|
|
3
|
+
|
|
4
|
+
Core functionality only:
|
|
5
|
+
- Create transactions with single/multiple outputs
|
|
6
|
+
- Simple fee calculation
|
|
7
|
+
- Basic UTXO selection (largest first)
|
|
8
|
+
- Transaction signing and broadcasting
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { TxBuilder, P2PKHSignatory, fromHex, toHex, Ecc, Script, ALL_BIP143 } = require('ecash-lib')
|
|
12
|
+
const { decodeCashAddress } = require('ecashaddrjs')
|
|
13
|
+
const KeyDerivation = require('./key-derivation')
|
|
14
|
+
const SecurityValidator = require('./security')
|
|
15
|
+
|
|
16
|
+
class SendXEC {
|
|
17
|
+
constructor (localConfig = {}) {
|
|
18
|
+
this.chronik = localConfig.chronik
|
|
19
|
+
this.ar = localConfig.ar
|
|
20
|
+
|
|
21
|
+
if (!this.chronik) {
|
|
22
|
+
throw new Error('Chronik client required for XEC transactions')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!this.ar) {
|
|
26
|
+
throw new Error('AdapterRouter required for XEC transactions')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Initialize components
|
|
30
|
+
this.keyDerivation = new KeyDerivation()
|
|
31
|
+
this.security = new SecurityValidator(localConfig.security)
|
|
32
|
+
|
|
33
|
+
// Initialize ECC for ecash-lib
|
|
34
|
+
try {
|
|
35
|
+
this.ecc = new Ecc()
|
|
36
|
+
} catch (err) {
|
|
37
|
+
throw new Error(`Ecc initialization failed: ${err.message}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Simple configuration - XEC dust limit is 546 satoshis (5.46 XEC)
|
|
41
|
+
this.dustLimit = localConfig.dustLimit || 546
|
|
42
|
+
this.maxRetries = localConfig.maxRetries || 3
|
|
43
|
+
this.defaultSatsPerByte = localConfig.defaultSatsPerByte || 1.2
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create transaction with single or multiple outputs
|
|
48
|
+
* @param {Array|Object} outputs - Output(s) to send to
|
|
49
|
+
* @param {Object} walletInfo - Wallet information
|
|
50
|
+
* @param {Array} utxos - Available UTXOs
|
|
51
|
+
* @returns {string} - Transaction hex
|
|
52
|
+
*/
|
|
53
|
+
async createTransaction (outputs, walletInfo, utxos) {
|
|
54
|
+
try {
|
|
55
|
+
// Normalize outputs to array
|
|
56
|
+
const normalizedOutputs = Array.isArray(outputs) ? outputs : [outputs]
|
|
57
|
+
|
|
58
|
+
// Validate outputs
|
|
59
|
+
const outputValidation = this.security.validateOutputs(normalizedOutputs)
|
|
60
|
+
if (!outputValidation.isValid) {
|
|
61
|
+
throw new Error(`Invalid outputs: ${outputValidation.errors.join(', ')}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Select UTXOs
|
|
65
|
+
const selection = this._selectUtxos(normalizedOutputs, utxos)
|
|
66
|
+
|
|
67
|
+
// Build transaction
|
|
68
|
+
const txHex = await this._buildTransaction(
|
|
69
|
+
selection.selectedUtxos,
|
|
70
|
+
normalizedOutputs,
|
|
71
|
+
selection.change,
|
|
72
|
+
walletInfo
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return txHex
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw new Error(`Transaction creation failed: ${err.message}`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create send-all transaction
|
|
83
|
+
* @param {string} address - Destination address
|
|
84
|
+
* @param {Object} walletInfo - Wallet information
|
|
85
|
+
* @param {Array} utxos - Available UTXOs
|
|
86
|
+
* @returns {string} - Transaction hex
|
|
87
|
+
*/
|
|
88
|
+
async createSendAllTx (address, walletInfo, utxos) {
|
|
89
|
+
try {
|
|
90
|
+
// Validate address
|
|
91
|
+
if (!this.security.isValidAddress(address)) {
|
|
92
|
+
throw new Error(`Invalid destination address: ${address}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Filter secure UTXOs (allow unconfirmed if needed)
|
|
96
|
+
let secureUtxos = this.security.filterSecureUtxos(utxos, { excludeDustAttack: false })
|
|
97
|
+
|
|
98
|
+
// If no confirmed UTXOs available, allow unconfirmed ones
|
|
99
|
+
if (secureUtxos.length === 0) {
|
|
100
|
+
console.warn('No confirmed UTXOs available, including unconfirmed UTXOs')
|
|
101
|
+
secureUtxos = this.security.filterSecureUtxos(utxos, { includeUnconfirmed: true, excludeDustAttack: false })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (secureUtxos.length === 0) {
|
|
105
|
+
throw new Error('No spendable UTXOs available')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Calculate total available
|
|
109
|
+
const totalSats = secureUtxos.reduce((sum, utxo) => sum + this._getUtxoValue(utxo), 0)
|
|
110
|
+
|
|
111
|
+
// Estimate fee
|
|
112
|
+
const estimatedFee = this._calculateFee(secureUtxos.length, 1) // All inputs, 1 output
|
|
113
|
+
|
|
114
|
+
const sendAmount = totalSats - estimatedFee
|
|
115
|
+
if (sendAmount <= this.dustLimit) {
|
|
116
|
+
throw new Error('Amount too small after fees')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Create output
|
|
120
|
+
const outputs = [{
|
|
121
|
+
address,
|
|
122
|
+
amountSat: sendAmount
|
|
123
|
+
}]
|
|
124
|
+
|
|
125
|
+
// Build transaction (no change output needed)
|
|
126
|
+
const txHex = await this._buildTransaction(secureUtxos, outputs, 0, walletInfo)
|
|
127
|
+
|
|
128
|
+
return txHex
|
|
129
|
+
} catch (err) {
|
|
130
|
+
throw new Error(`Send-all transaction failed: ${err.message}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Legacy method for backward compatibility
|
|
136
|
+
* @param {Array} outputs - Transaction outputs
|
|
137
|
+
* @param {Array} utxos - Available UTXOs
|
|
138
|
+
* @returns {Object} - UTXO selection result
|
|
139
|
+
*/
|
|
140
|
+
getNecessaryUtxosAndChange (outputs, utxos) {
|
|
141
|
+
try {
|
|
142
|
+
const selection = this._selectUtxos(outputs, utxos)
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
necessaryUtxos: selection.selectedUtxos,
|
|
146
|
+
totalAmount: selection.totalAmount,
|
|
147
|
+
change: selection.change,
|
|
148
|
+
estimatedFee: selection.estimatedFee
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
throw new Error(`UTXO selection failed: ${err.message}`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Public fee calculation method
|
|
157
|
+
* @param {number} inputCount - Number of inputs
|
|
158
|
+
* @param {number} outputCount - Number of outputs
|
|
159
|
+
* @param {number} feeRate - Fee rate in sats/byte
|
|
160
|
+
* @returns {number} - Fee in satoshis
|
|
161
|
+
*/
|
|
162
|
+
calculateFee (inputCount, outputCount, feeRate = null) {
|
|
163
|
+
const rate = feeRate !== null ? feeRate : this.defaultSatsPerByte
|
|
164
|
+
return this._calculateFee(inputCount, outputCount, rate)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Sort UTXOs by size
|
|
169
|
+
* @param {Array} utxos - UTXOs to sort
|
|
170
|
+
* @param {string} order - 'asc' or 'desc'
|
|
171
|
+
* @returns {Array} - Sorted UTXOs
|
|
172
|
+
*/
|
|
173
|
+
sortUtxosBySize (utxos, order = 'desc') {
|
|
174
|
+
return [...utxos].sort((a, b) => {
|
|
175
|
+
const aValue = this._getUtxoValue(a)
|
|
176
|
+
const bValue = this._getUtxoValue(b)
|
|
177
|
+
return order === 'desc' ? bValue - aValue : aValue - bValue
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Legacy sendAllXec method
|
|
183
|
+
* @param {string} address - Destination address
|
|
184
|
+
* @param {Object} walletInfo - Wallet information
|
|
185
|
+
* @param {Array} utxos - Available UTXOs
|
|
186
|
+
* @returns {string} - Transaction ID
|
|
187
|
+
*/
|
|
188
|
+
async sendAllXec (address, walletInfo, utxos) {
|
|
189
|
+
try {
|
|
190
|
+
const txHex = await this.createSendAllTx(address, walletInfo, utxos)
|
|
191
|
+
|
|
192
|
+
// Broadcast transaction
|
|
193
|
+
const txid = await this.ar.sendTx(txHex)
|
|
194
|
+
return txid
|
|
195
|
+
} catch (err) {
|
|
196
|
+
throw new Error(`Send all XEC failed: ${err.message}`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get key pair from wallet info using ecash-lib
|
|
202
|
+
* @param {Object} walletInfo - Wallet information
|
|
203
|
+
* @returns {Object} - Key pair object with privateKey, publicKey, and address
|
|
204
|
+
*/
|
|
205
|
+
async getKeyPairFromMnemonic (walletInfo) {
|
|
206
|
+
try {
|
|
207
|
+
// Use ecash-lib based key derivation
|
|
208
|
+
if (walletInfo.mnemonic) {
|
|
209
|
+
return this.keyDerivation.deriveFromMnemonic(walletInfo.mnemonic, walletInfo.hdPath)
|
|
210
|
+
} else if (walletInfo.privateKey) {
|
|
211
|
+
return this.keyDerivation.deriveFromWif(walletInfo.privateKey)
|
|
212
|
+
} else {
|
|
213
|
+
throw new Error('No private key or mnemonic provided')
|
|
214
|
+
}
|
|
215
|
+
} catch (err) {
|
|
216
|
+
throw new Error(`Key pair creation failed: ${err.message}`)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* High-level sendXec method
|
|
222
|
+
* @param {Array|Object} outputs - Outputs to send
|
|
223
|
+
* @param {Object} walletInfo - Wallet information
|
|
224
|
+
* @param {Array} utxos - Available UTXOs
|
|
225
|
+
* @returns {string} - Transaction ID
|
|
226
|
+
*/
|
|
227
|
+
async sendXec (outputs, walletInfo, utxos) {
|
|
228
|
+
try {
|
|
229
|
+
const txHex = await this.createTransaction(outputs, walletInfo, utxos)
|
|
230
|
+
|
|
231
|
+
// Broadcast transaction
|
|
232
|
+
const txid = await this.ar.sendTx(txHex)
|
|
233
|
+
return txid
|
|
234
|
+
} catch (err) {
|
|
235
|
+
throw new Error(`Send XEC failed: ${err.message}`)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Private methods
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Simple UTXO selection (largest first)
|
|
243
|
+
* @param {Array} outputs - Transaction outputs
|
|
244
|
+
* @param {Array} utxos - Available UTXOs
|
|
245
|
+
* @returns {Object} - Selection result
|
|
246
|
+
*/
|
|
247
|
+
_selectUtxos (outputs, utxos) {
|
|
248
|
+
// Calculate target amount
|
|
249
|
+
const targetAmount = outputs.reduce((sum, output) => sum + (output.amountSat || output.amount), 0)
|
|
250
|
+
|
|
251
|
+
// Filter secure UTXOs (allow unconfirmed if needed)
|
|
252
|
+
let secureUtxos = this.security.filterSecureUtxos(utxos, { excludeDustAttack: false })
|
|
253
|
+
|
|
254
|
+
// If no confirmed UTXOs available, allow unconfirmed ones
|
|
255
|
+
if (secureUtxos.length === 0) {
|
|
256
|
+
console.warn('No confirmed UTXOs available, including unconfirmed UTXOs')
|
|
257
|
+
secureUtxos = this.security.filterSecureUtxos(utxos, { includeUnconfirmed: true, excludeDustAttack: false })
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (secureUtxos.length === 0) {
|
|
261
|
+
throw new Error('No spendable UTXOs available')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Sort by value descending (largest first)
|
|
265
|
+
const sortedUtxos = secureUtxos.sort((a, b) => {
|
|
266
|
+
return this._getUtxoValue(b) - this._getUtxoValue(a)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// Greedy selection
|
|
270
|
+
const selectedUtxos = []
|
|
271
|
+
let totalAmount = 0
|
|
272
|
+
|
|
273
|
+
for (const utxo of sortedUtxos) {
|
|
274
|
+
const utxoValue = this._getUtxoValue(utxo)
|
|
275
|
+
selectedUtxos.push(utxo)
|
|
276
|
+
totalAmount += utxoValue
|
|
277
|
+
|
|
278
|
+
// Estimate fee with current selection
|
|
279
|
+
const estimatedFee = this._calculateFee(selectedUtxos.length, outputs.length)
|
|
280
|
+
|
|
281
|
+
if (totalAmount >= targetAmount + estimatedFee) {
|
|
282
|
+
// We have enough
|
|
283
|
+
const finalFee = this._calculateFee(selectedUtxos.length, outputs.length)
|
|
284
|
+
const change = totalAmount - targetAmount - finalFee
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
selectedUtxos,
|
|
288
|
+
totalAmount,
|
|
289
|
+
estimatedFee: finalFee,
|
|
290
|
+
change
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
throw new Error('Insufficient funds')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Calculate transaction fee
|
|
300
|
+
* @param {number} inputCount - Number of inputs
|
|
301
|
+
* @param {number} outputCount - Number of outputs
|
|
302
|
+
* @param {number} feeRate - Fee rate in sats/byte
|
|
303
|
+
* @returns {number} - Fee in satoshis
|
|
304
|
+
*/
|
|
305
|
+
_calculateFee (inputCount, outputCount, feeRate = null) {
|
|
306
|
+
const rate = feeRate !== null ? feeRate : this.defaultSatsPerByte
|
|
307
|
+
|
|
308
|
+
// Handle zero fee rate explicitly
|
|
309
|
+
if (rate === 0) {
|
|
310
|
+
return 0
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const inputSize = 148 // P2PKH input size
|
|
314
|
+
const outputSize = 34 // P2PKH output size
|
|
315
|
+
const overhead = 10 // Version, locktime, etc.
|
|
316
|
+
|
|
317
|
+
const txSize = (inputCount * inputSize) + (outputCount * outputSize) + overhead
|
|
318
|
+
return Math.ceil(txSize * rate)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Build and sign transaction using ecash-lib properly
|
|
323
|
+
* @param {Array} selectedUtxos - UTXOs to spend
|
|
324
|
+
* @param {Array} outputs - Transaction outputs
|
|
325
|
+
* @param {number} changeAmount - Change amount
|
|
326
|
+
* @param {Object} walletInfo - Wallet information
|
|
327
|
+
* @returns {string} - Transaction hex
|
|
328
|
+
*/
|
|
329
|
+
async _buildTransaction (selectedUtxos, outputs, changeAmount, walletInfo) {
|
|
330
|
+
try {
|
|
331
|
+
// Get private key (prefer mnemonic)
|
|
332
|
+
let privateKeyHex
|
|
333
|
+
if (walletInfo.mnemonic) {
|
|
334
|
+
const keyData = this.keyDerivation.deriveFromMnemonic(walletInfo.mnemonic, walletInfo.hdPath)
|
|
335
|
+
privateKeyHex = keyData.privateKey
|
|
336
|
+
} else {
|
|
337
|
+
privateKeyHex = walletInfo.privateKey
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const sk = fromHex(privateKeyHex)
|
|
341
|
+
const pk = this.ecc.derivePubkey(sk)
|
|
342
|
+
|
|
343
|
+
// Build outputs
|
|
344
|
+
const txOutputs = []
|
|
345
|
+
|
|
346
|
+
// Add main outputs
|
|
347
|
+
for (const output of outputs) {
|
|
348
|
+
const decoded = decodeCashAddress(output.address)
|
|
349
|
+
txOutputs.push({
|
|
350
|
+
sats: BigInt(output.amountSat || output.amount),
|
|
351
|
+
script: Script.p2pkh(fromHex(decoded.hash))
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Add change address for automatic calculation
|
|
356
|
+
const walletDecoded = decodeCashAddress(walletInfo.xecAddress)
|
|
357
|
+
txOutputs.push(Script.p2pkh(fromHex(walletDecoded.hash)))
|
|
358
|
+
|
|
359
|
+
const inputs = selectedUtxos.map(utxo => ({
|
|
360
|
+
input: {
|
|
361
|
+
prevOut: {
|
|
362
|
+
txid: utxo.outpoint.txid,
|
|
363
|
+
outIdx: utxo.outpoint.outIdx
|
|
364
|
+
},
|
|
365
|
+
signData: {
|
|
366
|
+
sats: BigInt(this._getUtxoValue(utxo)),
|
|
367
|
+
outputScript: Script.p2pkh(fromHex(walletDecoded.hash))
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
signatory: P2PKHSignatory(sk, pk, ALL_BIP143)
|
|
371
|
+
}))
|
|
372
|
+
|
|
373
|
+
const txBuilder = new TxBuilder({ inputs, outputs: txOutputs })
|
|
374
|
+
const tx = txBuilder.sign({
|
|
375
|
+
feePerKb: BigInt(1200),
|
|
376
|
+
dustSats: BigInt(546)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
return toHex(tx.ser())
|
|
380
|
+
} catch (err) {
|
|
381
|
+
throw new Error(`Transaction failed: ${err.message}`)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
_getUtxoValue (utxo) {
|
|
386
|
+
if (utxo.sats !== undefined) {
|
|
387
|
+
return typeof utxo.sats === 'bigint' ? Number(utxo.sats) : parseInt(utxo.sats)
|
|
388
|
+
}
|
|
389
|
+
if (utxo.value !== undefined) {
|
|
390
|
+
return typeof utxo.value === 'bigint' ? Number(utxo.value) : parseInt(utxo.value)
|
|
391
|
+
}
|
|
392
|
+
return 0
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
module.exports = SendXEC
|