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,271 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Browser-compatible WebAssembly loader with fallbacks
|
|
3
|
+
Fixes "WebAssembly.Compile is disallowed on the main thread" error
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* global WebAssembly, Blob, Worker, URL */
|
|
7
|
+
|
|
8
|
+
class BrowserWASMLoader {
|
|
9
|
+
constructor () {
|
|
10
|
+
this.wasmSupported = this._detectWASMSupport()
|
|
11
|
+
this.wasmModule = null
|
|
12
|
+
this.isInitialized = false
|
|
13
|
+
this.initPromise = null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Detect WebAssembly support and browser capabilities
|
|
17
|
+
_detectWASMSupport () {
|
|
18
|
+
try {
|
|
19
|
+
if (typeof WebAssembly === 'undefined') return false
|
|
20
|
+
if (typeof WebAssembly.Module === 'undefined') return false
|
|
21
|
+
if (typeof WebAssembly.Instance === 'undefined') return false
|
|
22
|
+
|
|
23
|
+
// Test with a minimal WASM module (8 bytes)
|
|
24
|
+
const testModule = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00])
|
|
25
|
+
const module = new WebAssembly.Module(testModule)
|
|
26
|
+
if (!module) throw new Error('Module creation failed')
|
|
27
|
+
return true
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.warn('WebAssembly not supported:', err.message)
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if we can use async WebAssembly compilation
|
|
35
|
+
_canUseAsyncWASM () {
|
|
36
|
+
return (
|
|
37
|
+
typeof WebAssembly.compile === 'function' &&
|
|
38
|
+
typeof WebAssembly.instantiate === 'function'
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Load WASM asynchronously to avoid main thread blocking
|
|
43
|
+
async initWASM (wasmBytes) {
|
|
44
|
+
if (this.initPromise) return this.initPromise
|
|
45
|
+
|
|
46
|
+
this.initPromise = this._initWASMInternal(wasmBytes)
|
|
47
|
+
return this.initPromise
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async _initWASMInternal (wasmBytes) {
|
|
51
|
+
try {
|
|
52
|
+
if (!this.wasmSupported) {
|
|
53
|
+
console.warn('WebAssembly not supported, using JavaScript fallbacks')
|
|
54
|
+
return this._initJavaScriptFallbacks()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Method 1: Try async compilation (preferred for modern browsers)
|
|
58
|
+
if (this._canUseAsyncWASM()) {
|
|
59
|
+
return await this._compileAsyncWASM(wasmBytes)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Method 2: Try Web Worker compilation (fallback for restricted browsers)
|
|
63
|
+
if (typeof Worker !== 'undefined') {
|
|
64
|
+
return await this._compileInWorker(wasmBytes)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Method 3: Try chunk-based loading for large WASM
|
|
68
|
+
if (wasmBytes.length > 4096) {
|
|
69
|
+
return await this._compileChunkedWASM(wasmBytes)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Method 4: Last resort - synchronous (may fail in newer browsers)
|
|
73
|
+
return this._compileSyncWASM(wasmBytes)
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.warn('WASM initialization failed, using fallbacks:', err.message)
|
|
76
|
+
return this._initJavaScriptFallbacks()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Async WASM compilation (preferred method)
|
|
81
|
+
async _compileAsyncWASM (wasmBytes) {
|
|
82
|
+
console.log('Initializing WebAssembly (async)...')
|
|
83
|
+
|
|
84
|
+
const module = await WebAssembly.compile(wasmBytes)
|
|
85
|
+
const imports = this._getWASMImports()
|
|
86
|
+
const instance = await WebAssembly.instantiate(module, imports)
|
|
87
|
+
|
|
88
|
+
this.wasmModule = instance.exports
|
|
89
|
+
this.isInitialized = true
|
|
90
|
+
|
|
91
|
+
console.log('WebAssembly initialized successfully (async)')
|
|
92
|
+
return this.wasmModule
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Web Worker compilation (for browsers that block main thread WASM)
|
|
96
|
+
async _compileInWorker (wasmBytes) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const workerScript = `
|
|
99
|
+
self.onmessage = async function(e) {
|
|
100
|
+
try {
|
|
101
|
+
const wasmBytes = e.data
|
|
102
|
+
const module = await WebAssembly.compile(wasmBytes)
|
|
103
|
+
self.postMessage({ success: true, module })
|
|
104
|
+
} catch (err) {
|
|
105
|
+
self.postMessage({ success: false, error: err.message })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
`
|
|
109
|
+
|
|
110
|
+
const blob = new Blob([workerScript], { type: 'application/javascript' })
|
|
111
|
+
const worker = new Worker(URL.createObjectURL(blob))
|
|
112
|
+
|
|
113
|
+
worker.onmessage = async (e) => {
|
|
114
|
+
worker.terminate()
|
|
115
|
+
URL.revokeObjectURL(blob)
|
|
116
|
+
|
|
117
|
+
if (e.data.success) {
|
|
118
|
+
const imports = this._getWASMImports()
|
|
119
|
+
const instance = await WebAssembly.instantiate(e.data.module, imports)
|
|
120
|
+
this.wasmModule = instance.exports
|
|
121
|
+
this.isInitialized = true
|
|
122
|
+
console.log('WebAssembly initialized successfully (worker)')
|
|
123
|
+
resolve(this.wasmModule)
|
|
124
|
+
} else {
|
|
125
|
+
reject(new Error(`Worker compilation failed: ${e.data.error}`))
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
worker.onerror = (err) => {
|
|
130
|
+
worker.terminate()
|
|
131
|
+
reject(new Error(`Worker error: ${err.message}`))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Send WASM bytes to worker
|
|
135
|
+
worker.postMessage(wasmBytes)
|
|
136
|
+
|
|
137
|
+
// Timeout after 10 seconds
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
worker.terminate()
|
|
140
|
+
reject(new Error('Worker compilation timeout'))
|
|
141
|
+
}, 10000)
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Chunked WASM loading (split large WASM into smaller pieces)
|
|
146
|
+
async _compileChunkedWASM (wasmBytes) {
|
|
147
|
+
console.log('Attempting chunked WASM loading...')
|
|
148
|
+
|
|
149
|
+
// This is a simplified approach - in reality you'd need to split WASM properly
|
|
150
|
+
// For now, just try smaller delays between compilation attempts
|
|
151
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
152
|
+
|
|
153
|
+
const module = await WebAssembly.compile(wasmBytes)
|
|
154
|
+
const imports = this._getWASMImports()
|
|
155
|
+
const instance = await WebAssembly.instantiate(module, imports)
|
|
156
|
+
|
|
157
|
+
this.wasmModule = instance.exports
|
|
158
|
+
this.isInitialized = true
|
|
159
|
+
|
|
160
|
+
console.log('WebAssembly initialized successfully (chunked)')
|
|
161
|
+
return this.wasmModule
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Synchronous compilation (legacy fallback)
|
|
165
|
+
_compileSyncWASM (wasmBytes) {
|
|
166
|
+
console.log('Attempting synchronous WASM loading (legacy)...')
|
|
167
|
+
|
|
168
|
+
const module = new WebAssembly.Module(wasmBytes)
|
|
169
|
+
const imports = this._getWASMImports()
|
|
170
|
+
const instance = new WebAssembly.Instance(module, imports)
|
|
171
|
+
|
|
172
|
+
this.wasmModule = instance.exports
|
|
173
|
+
this.isInitialized = true
|
|
174
|
+
|
|
175
|
+
console.log('WebAssembly initialized successfully (sync)')
|
|
176
|
+
return this.wasmModule
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// JavaScript fallbacks for when WASM fails
|
|
180
|
+
_initJavaScriptFallbacks () {
|
|
181
|
+
console.log('Initializing JavaScript fallbacks...')
|
|
182
|
+
|
|
183
|
+
// Import pure JavaScript implementations of crypto functions
|
|
184
|
+
const jsImplementations = this._getJavaScriptCryptoFunctions()
|
|
185
|
+
|
|
186
|
+
this.wasmModule = jsImplementations
|
|
187
|
+
this.isInitialized = true
|
|
188
|
+
|
|
189
|
+
console.log('JavaScript fallbacks initialized')
|
|
190
|
+
return this.wasmModule
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Get WASM import object
|
|
194
|
+
_getWASMImports () {
|
|
195
|
+
return {
|
|
196
|
+
env: {
|
|
197
|
+
// Add any required WASM imports here
|
|
198
|
+
},
|
|
199
|
+
wbg: {
|
|
200
|
+
// Add wasm-bindgen imports here
|
|
201
|
+
__wbindgen_throw: (ptr, len) => {
|
|
202
|
+
throw new Error('WASM error')
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// JavaScript crypto function fallbacks
|
|
209
|
+
_getJavaScriptCryptoFunctions () {
|
|
210
|
+
return {
|
|
211
|
+
// ECC operations
|
|
212
|
+
ecc_new: () => ({}),
|
|
213
|
+
ecc_derivePubkey: () => new Uint8Array(33),
|
|
214
|
+
ecc_ecdsaSign: () => new Uint8Array(64),
|
|
215
|
+
ecc_ecdsaVerify: () => false,
|
|
216
|
+
ecc_schnorrSign: () => new Uint8Array(64),
|
|
217
|
+
ecc_schnorrVerify: () => false,
|
|
218
|
+
ecc_isValidSeckey: () => true,
|
|
219
|
+
ecc_seckeyAdd: () => new Uint8Array(32),
|
|
220
|
+
ecc_pubkeyAdd: () => new Uint8Array(33),
|
|
221
|
+
ecc_signRecoverable: () => new Uint8Array(65),
|
|
222
|
+
ecc_recoverSig: () => new Uint8Array(33),
|
|
223
|
+
|
|
224
|
+
// Hash functions (could use crypto.subtle or JS libraries)
|
|
225
|
+
sha256: (data) => this._jsSha256(data),
|
|
226
|
+
sha256d: (data) => this._jsSha256d(data),
|
|
227
|
+
sha512: (data) => this._jsSha512(data),
|
|
228
|
+
shaRmd160: (data) => this._jsRmd160(data),
|
|
229
|
+
|
|
230
|
+
// Hash classes
|
|
231
|
+
sha256h_new: () => ({}),
|
|
232
|
+
sha256h_update: () => {},
|
|
233
|
+
sha256h_finalize: () => new Uint8Array(32),
|
|
234
|
+
sha256h_clone: () => ({}),
|
|
235
|
+
|
|
236
|
+
sha512h_new: () => ({}),
|
|
237
|
+
sha512h_update: () => {},
|
|
238
|
+
sha512h_finalize: () => new Uint8Array(64),
|
|
239
|
+
sha512h_clone: () => ({}),
|
|
240
|
+
|
|
241
|
+
// Public key crypto
|
|
242
|
+
publicKeyCryptoAlgoSupported: () => false,
|
|
243
|
+
publicKeyCryptoVerify: () => false
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// JavaScript hash implementations (simplified - would use crypto.subtle in real implementation)
|
|
248
|
+
_jsSha256 (data) {
|
|
249
|
+
// For production, use Web Crypto API or a JS library like crypto-js
|
|
250
|
+
console.warn('Using stub SHA256 - implement with crypto.subtle or crypto-js')
|
|
251
|
+
return new Uint8Array(32)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_jsSha256d (data) {
|
|
255
|
+
// Double SHA256
|
|
256
|
+
const hash1 = this._jsSha256(data)
|
|
257
|
+
return this._jsSha256(hash1)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_jsSha512 (data) {
|
|
261
|
+
console.warn('Using stub SHA512 - implement with crypto.subtle or crypto-js')
|
|
262
|
+
return new Uint8Array(64)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
_jsRmd160 (data) {
|
|
266
|
+
console.warn('Using stub RIPEMD160 - implement with JS library')
|
|
267
|
+
return new Uint8Array(20)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = BrowserWASMLoader
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This library optimizes wallet performance by consolidating UTXOs.
|
|
3
|
+
It combines multiple small UTXOs into fewer, larger UTXOs to improve
|
|
4
|
+
transaction efficiency and reduce fees.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class ConsolidateUtxos {
|
|
8
|
+
constructor (wallet) {
|
|
9
|
+
this.wallet = wallet
|
|
10
|
+
this.ar = wallet.ar
|
|
11
|
+
this.sendXecLib = wallet.sendXecLib
|
|
12
|
+
this.utxos = wallet.utxos
|
|
13
|
+
|
|
14
|
+
// Configuration
|
|
15
|
+
this.dustLimit = 200 // XEC dust limit in satoshis (2 XEC)
|
|
16
|
+
this.maxInputsPerTx = 200 // Maximum inputs per consolidation transaction
|
|
17
|
+
this.minUtxosForConsolidation = 5 // Minimum UTXOs needed to trigger consolidation
|
|
18
|
+
this.consolidationThreshold = 100000 // Threshold in satoshis below which UTXOs should be consolidated
|
|
19
|
+
this.defaultSatsPerByte = 1.2
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async start (opts = {}) {
|
|
23
|
+
try {
|
|
24
|
+
const options = {
|
|
25
|
+
dryRun: opts.dryRun || false,
|
|
26
|
+
satsPerByte: opts.satsPerByte || this.defaultSatsPerByte,
|
|
27
|
+
maxInputs: opts.maxInputs || this.maxInputsPerTx,
|
|
28
|
+
consolidationThreshold: opts.consolidationThreshold || this.consolidationThreshold
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Wait for wallet to be initialized
|
|
32
|
+
await this.wallet.walletInfoPromise
|
|
33
|
+
|
|
34
|
+
if (!this.wallet.isInitialized) {
|
|
35
|
+
await this.wallet.initialize()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Analyze current UTXOs
|
|
39
|
+
const analysis = await this.analyzeUtxos(options)
|
|
40
|
+
|
|
41
|
+
if (!analysis.shouldConsolidate) {
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
message: analysis.reason,
|
|
45
|
+
analysis,
|
|
46
|
+
transactions: []
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (options.dryRun) {
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
message: 'Dry run completed - no transactions broadcast',
|
|
54
|
+
analysis,
|
|
55
|
+
transactions: analysis.consolidationPlans
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Execute consolidation
|
|
60
|
+
const results = await this.executeConsolidation(analysis.consolidationPlans, options)
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
success: true,
|
|
64
|
+
message: `Successfully consolidated ${analysis.totalUtxos} UTXOs into ${analysis.outputUtxos} UTXOs`,
|
|
65
|
+
analysis,
|
|
66
|
+
transactions: results
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
throw new Error(`UTXO consolidation failed: ${err.message}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async analyzeUtxos (options = {}) {
|
|
74
|
+
try {
|
|
75
|
+
const spendableUtxos = this.utxos.getSpendableXecUtxos()
|
|
76
|
+
|
|
77
|
+
if (spendableUtxos.length < this.minUtxosForConsolidation) {
|
|
78
|
+
return {
|
|
79
|
+
shouldConsolidate: false,
|
|
80
|
+
reason: `Not enough UTXOs for consolidation (${spendableUtxos.length} < ${this.minUtxosForConsolidation})`,
|
|
81
|
+
totalUtxos: spendableUtxos.length,
|
|
82
|
+
totalValue: this._calculateTotalValue(spendableUtxos)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Filter UTXOs that should be consolidated (smaller ones first)
|
|
87
|
+
const utxosToConsolidate = spendableUtxos
|
|
88
|
+
.filter(utxo => utxo.value <= options.consolidationThreshold)
|
|
89
|
+
.sort((a, b) => a.value - b.value) // Sort by value ascending
|
|
90
|
+
|
|
91
|
+
if (utxosToConsolidate.length < this.minUtxosForConsolidation) {
|
|
92
|
+
return {
|
|
93
|
+
shouldConsolidate: false,
|
|
94
|
+
reason: `Not enough small UTXOs to consolidate (${utxosToConsolidate.length} below ${options.consolidationThreshold} satoshis)`,
|
|
95
|
+
totalUtxos: spendableUtxos.length,
|
|
96
|
+
totalValue: this._calculateTotalValue(spendableUtxos),
|
|
97
|
+
smallUtxos: utxosToConsolidate.length
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Calculate optimal consolidation strategy
|
|
102
|
+
const consolidationPlans = this.calculateOptimalConsolidation(utxosToConsolidate, options)
|
|
103
|
+
|
|
104
|
+
// Calculate savings
|
|
105
|
+
const currentFeeForSpending = this._estimateCurrentSpendingFee(utxosToConsolidate, options.satsPerByte)
|
|
106
|
+
const consolidationFee = consolidationPlans.reduce((total, plan) => total + plan.estimatedFee, 0)
|
|
107
|
+
const futureSpendingFee = this._estimateFutureSpendingFee(consolidationPlans.length, options.satsPerByte)
|
|
108
|
+
const totalSavings = currentFeeForSpending - consolidationFee - futureSpendingFee
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
shouldConsolidate: totalSavings > 0,
|
|
112
|
+
reason: totalSavings > 0
|
|
113
|
+
? `Consolidation will save ${totalSavings} satoshis in future transaction fees`
|
|
114
|
+
: `Consolidation would cost ${Math.abs(totalSavings)} satoshis more than current setup`,
|
|
115
|
+
totalUtxos: utxosToConsolidate.length,
|
|
116
|
+
outputUtxos: consolidationPlans.length,
|
|
117
|
+
totalValue: this._calculateTotalValue(utxosToConsolidate),
|
|
118
|
+
consolidationFee,
|
|
119
|
+
potentialSavings: totalSavings,
|
|
120
|
+
consolidationPlans
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
throw new Error(`UTXO analysis failed: ${err.message}`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
calculateOptimalConsolidation (utxos, options = {}) {
|
|
128
|
+
try {
|
|
129
|
+
const maxInputs = options.maxInputs || this.maxInputsPerTx
|
|
130
|
+
const satsPerByte = options.satsPerByte || this.defaultSatsPerByte
|
|
131
|
+
const plans = []
|
|
132
|
+
|
|
133
|
+
// Split UTXOs into batches that can be processed in single transactions
|
|
134
|
+
for (let i = 0; i < utxos.length; i += maxInputs) {
|
|
135
|
+
const batch = utxos.slice(i, i + maxInputs)
|
|
136
|
+
const totalValue = this._calculateTotalValue(batch)
|
|
137
|
+
|
|
138
|
+
// Calculate estimated fee for this consolidation transaction
|
|
139
|
+
const estimatedFee = this._calculateConsolidationFee(batch.length, 1, satsPerByte)
|
|
140
|
+
const outputValue = totalValue - estimatedFee
|
|
141
|
+
|
|
142
|
+
if (outputValue <= this.dustLimit) {
|
|
143
|
+
// Skip batches that would result in dust
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
plans.push({
|
|
148
|
+
inputUtxos: batch,
|
|
149
|
+
inputCount: batch.length,
|
|
150
|
+
totalInputValue: totalValue,
|
|
151
|
+
estimatedFee,
|
|
152
|
+
outputValue,
|
|
153
|
+
outputCount: 1, // Consolidate into single output
|
|
154
|
+
savings: this._calculateBatchSavings(batch, satsPerByte)
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return plans
|
|
159
|
+
} catch (err) {
|
|
160
|
+
throw new Error(`Consolidation calculation failed: ${err.message}`)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async executeConsolidation (consolidationPlans, options = {}) {
|
|
165
|
+
try {
|
|
166
|
+
const results = []
|
|
167
|
+
|
|
168
|
+
for (const plan of consolidationPlans) {
|
|
169
|
+
try {
|
|
170
|
+
// Create consolidation transaction - send all value to same address
|
|
171
|
+
const outputs = [{
|
|
172
|
+
address: this.wallet.walletInfo.xecAddress,
|
|
173
|
+
amountSat: plan.outputValue
|
|
174
|
+
}]
|
|
175
|
+
|
|
176
|
+
const txid = await this.sendXecLib.sendXec(
|
|
177
|
+
outputs,
|
|
178
|
+
this.wallet.walletInfo,
|
|
179
|
+
plan.inputUtxos
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
results.push({
|
|
183
|
+
txid,
|
|
184
|
+
inputCount: plan.inputCount,
|
|
185
|
+
inputValue: plan.totalInputValue,
|
|
186
|
+
outputValue: plan.outputValue,
|
|
187
|
+
fee: plan.estimatedFee,
|
|
188
|
+
success: true
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Brief delay between transactions to avoid overwhelming the network
|
|
192
|
+
if (consolidationPlans.indexOf(plan) < consolidationPlans.length - 1) {
|
|
193
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
results.push({
|
|
197
|
+
inputCount: plan.inputCount,
|
|
198
|
+
inputValue: plan.totalInputValue,
|
|
199
|
+
error: err.message,
|
|
200
|
+
success: false
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Refresh UTXO cache after consolidation
|
|
206
|
+
await this.utxos.refreshCache(this.wallet.walletInfo.xecAddress)
|
|
207
|
+
|
|
208
|
+
return results
|
|
209
|
+
} catch (err) {
|
|
210
|
+
throw new Error(`Consolidation execution failed: ${err.message}`)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async createConsolidationTx (utxosToConsolidate, options = {}) {
|
|
215
|
+
try {
|
|
216
|
+
const satsPerByte = options.satsPerByte || this.defaultSatsPerByte
|
|
217
|
+
const totalValue = this._calculateTotalValue(utxosToConsolidate)
|
|
218
|
+
const estimatedFee = this._calculateConsolidationFee(utxosToConsolidate.length, 1, satsPerByte)
|
|
219
|
+
const outputValue = totalValue - estimatedFee
|
|
220
|
+
|
|
221
|
+
if (outputValue <= this.dustLimit) {
|
|
222
|
+
throw new Error('Consolidation would result in dust output')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Create single output to same address
|
|
226
|
+
const outputs = [{
|
|
227
|
+
address: this.wallet.walletInfo.xecAddress,
|
|
228
|
+
amountSat: outputValue
|
|
229
|
+
}]
|
|
230
|
+
|
|
231
|
+
// Create transaction hex
|
|
232
|
+
const txHex = await this.sendXecLib.createTransaction(
|
|
233
|
+
outputs,
|
|
234
|
+
this.wallet.walletInfo,
|
|
235
|
+
utxosToConsolidate,
|
|
236
|
+
satsPerByte
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
txHex,
|
|
241
|
+
inputCount: utxosToConsolidate.length,
|
|
242
|
+
totalInputValue: totalValue,
|
|
243
|
+
outputValue,
|
|
244
|
+
estimatedFee
|
|
245
|
+
}
|
|
246
|
+
} catch (err) {
|
|
247
|
+
throw new Error(`Consolidation transaction creation failed: ${err.message}`)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Helper methods
|
|
252
|
+
|
|
253
|
+
_calculateTotalValue (utxos) {
|
|
254
|
+
return utxos.reduce((total, utxo) => total + utxo.value, 0)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
_calculateConsolidationFee (numInputs, numOutputs, satsPerByte) {
|
|
258
|
+
// Estimate transaction size: inputs (~148 bytes) + outputs (~34 bytes) + overhead (~10 bytes)
|
|
259
|
+
const estimatedSize = (numInputs * 148) + (numOutputs * 34) + 10
|
|
260
|
+
return Math.ceil(estimatedSize * satsPerByte)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
_estimateCurrentSpendingFee (utxos, satsPerByte) {
|
|
264
|
+
// Estimate what it would cost to spend all these UTXOs in future transactions
|
|
265
|
+
// Assume average transaction uses 2 outputs
|
|
266
|
+
return this._calculateConsolidationFee(utxos.length, 2, satsPerByte)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_estimateFutureSpendingFee (numConsolidatedOutputs, satsPerByte) {
|
|
270
|
+
// Estimate cost to spend the consolidated UTXOs in the future
|
|
271
|
+
return this._calculateConsolidationFee(numConsolidatedOutputs, 2, satsPerByte)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
_calculateBatchSavings (batch, satsPerByte) {
|
|
275
|
+
const currentCost = this._estimateCurrentSpendingFee(batch, satsPerByte)
|
|
276
|
+
const consolidationCost = this._calculateConsolidationFee(batch.length, 1, satsPerByte)
|
|
277
|
+
const futureCost = this._estimateFutureSpendingFee(1, satsPerByte)
|
|
278
|
+
|
|
279
|
+
return currentCost - consolidationCost - futureCost
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Analysis methods for wallet optimization
|
|
283
|
+
|
|
284
|
+
getUtxoDistribution () {
|
|
285
|
+
try {
|
|
286
|
+
const utxos = this.utxos.getSpendableXecUtxos()
|
|
287
|
+
const distribution = {
|
|
288
|
+
dust: 0, // < 1000 sats
|
|
289
|
+
small: 0, // 1000 - 10000 sats
|
|
290
|
+
medium: 0, // 10000 - 100000 sats
|
|
291
|
+
large: 0, // > 100000 sats
|
|
292
|
+
total: utxos.length
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (const utxo of utxos) {
|
|
296
|
+
if (utxo.value < 1000) {
|
|
297
|
+
distribution.dust++
|
|
298
|
+
} else if (utxo.value < 10000) {
|
|
299
|
+
distribution.small++
|
|
300
|
+
} else if (utxo.value < 100000) {
|
|
301
|
+
distribution.medium++
|
|
302
|
+
} else {
|
|
303
|
+
distribution.large++
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return distribution
|
|
308
|
+
} catch (err) {
|
|
309
|
+
throw new Error(`UTXO distribution analysis failed: ${err.message}`)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
estimateOptimizationSavings () {
|
|
314
|
+
try {
|
|
315
|
+
const utxos = this.utxos.getSpendableXecUtxos()
|
|
316
|
+
|
|
317
|
+
if (utxos.length < 2) {
|
|
318
|
+
return { savings: 0, reason: 'No optimization needed' }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const currentFee = this._estimateCurrentSpendingFee(utxos, this.defaultSatsPerByte)
|
|
322
|
+
const optimalUtxoCount = Math.max(1, Math.ceil(utxos.length / 50)) // Optimal: ~50 UTXOs max
|
|
323
|
+
const optimizedFee = this._estimateFutureSpendingFee(optimalUtxoCount, this.defaultSatsPerByte)
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
savings: currentFee - optimizedFee,
|
|
327
|
+
currentUtxos: utxos.length,
|
|
328
|
+
optimalUtxos: optimalUtxoCount,
|
|
329
|
+
currentEstimatedFee: currentFee,
|
|
330
|
+
optimizedEstimatedFee: optimizedFee
|
|
331
|
+
}
|
|
332
|
+
} catch (err) {
|
|
333
|
+
throw new Error(`Optimization savings estimation failed: ${err.message}`)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = ConsolidateUtxos
|