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,307 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Token Protocol Detection for hybrid SLP + ALP support.
|
|
3
|
+
Categorizes UTXOs by protocol and provides detection utilities.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class TokenProtocolDetector {
|
|
7
|
+
/**
|
|
8
|
+
* Detect the protocol of a token from a UTXO
|
|
9
|
+
* @param {Object} utxo - UTXO object from chronik
|
|
10
|
+
* @returns {string} - 'XEC', 'SLP', or 'ALP'
|
|
11
|
+
*/
|
|
12
|
+
static detectProtocol (utxo) {
|
|
13
|
+
if (!utxo || !utxo.token) {
|
|
14
|
+
return 'XEC'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const protocol = utxo.token.tokenType?.protocol
|
|
18
|
+
switch (protocol) {
|
|
19
|
+
case 'SLP':
|
|
20
|
+
return 'SLP'
|
|
21
|
+
case 'ALP':
|
|
22
|
+
return 'ALP'
|
|
23
|
+
default:
|
|
24
|
+
throw new Error(`Unknown token protocol: ${protocol}`)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Detect token protocol from token metadata
|
|
30
|
+
* @param {Object} tokenInfo - Token info from chronik.token()
|
|
31
|
+
* @returns {string} - 'SLP' or 'ALP'
|
|
32
|
+
*/
|
|
33
|
+
static detectProtocolFromMetadata (tokenInfo) {
|
|
34
|
+
if (!tokenInfo || !tokenInfo.tokenType) {
|
|
35
|
+
throw new Error('Invalid token metadata')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const protocol = tokenInfo.tokenType.protocol
|
|
39
|
+
switch (protocol) {
|
|
40
|
+
case 'SLP':
|
|
41
|
+
return 'SLP'
|
|
42
|
+
case 'ALP':
|
|
43
|
+
return 'ALP'
|
|
44
|
+
default:
|
|
45
|
+
throw new Error(`Unknown token protocol: ${protocol}`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Categorize UTXOs by protocol type
|
|
51
|
+
* @param {Array} utxos - Array of UTXO objects
|
|
52
|
+
* @returns {Object} - Categorized UTXOs by protocol
|
|
53
|
+
*/
|
|
54
|
+
static categorizeUtxos (utxos) {
|
|
55
|
+
const result = {
|
|
56
|
+
xecUtxos: [],
|
|
57
|
+
slpUtxos: [],
|
|
58
|
+
alpUtxos: [],
|
|
59
|
+
tokenUtxosByProtocol: new Map(), // protocol -> UTXOs[]
|
|
60
|
+
tokenUtxosById: new Map(), // tokenId -> UTXOs[]
|
|
61
|
+
protocolSummary: {
|
|
62
|
+
xec: { count: 0, totalSats: 0 },
|
|
63
|
+
slp: { count: 0, tokenTypes: new Set() },
|
|
64
|
+
alp: { count: 0, tokenTypes: new Set() }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const utxo of utxos) {
|
|
69
|
+
if (!utxo) continue
|
|
70
|
+
try {
|
|
71
|
+
const protocol = this.detectProtocol(utxo)
|
|
72
|
+
|
|
73
|
+
switch (protocol) {
|
|
74
|
+
case 'XEC':
|
|
75
|
+
result.xecUtxos.push(utxo)
|
|
76
|
+
this._addToProtocolMap(result.tokenUtxosByProtocol, 'XEC', utxo)
|
|
77
|
+
result.protocolSummary.xec.count++
|
|
78
|
+
result.protocolSummary.xec.totalSats += this._extractSats(utxo)
|
|
79
|
+
break
|
|
80
|
+
|
|
81
|
+
case 'SLP':
|
|
82
|
+
result.slpUtxos.push(utxo)
|
|
83
|
+
this._addToProtocolMap(result.tokenUtxosByProtocol, 'SLP', utxo)
|
|
84
|
+
this._addToTokenMap(result.tokenUtxosById, utxo.token.tokenId, utxo)
|
|
85
|
+
result.protocolSummary.slp.count++
|
|
86
|
+
result.protocolSummary.slp.tokenTypes.add(utxo.token.tokenId)
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
case 'ALP':
|
|
90
|
+
result.alpUtxos.push(utxo)
|
|
91
|
+
this._addToProtocolMap(result.tokenUtxosByProtocol, 'ALP', utxo)
|
|
92
|
+
this._addToTokenMap(result.tokenUtxosById, utxo.token.tokenId, utxo)
|
|
93
|
+
result.protocolSummary.alp.count++
|
|
94
|
+
result.protocolSummary.alp.tokenTypes.add(utxo.token.tokenId)
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.warn(`Skipping invalid UTXO: ${err.message}`, utxo)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Filter UTXOs for a specific token ID
|
|
107
|
+
* @param {Array} utxos - Array of UTXO objects
|
|
108
|
+
* @param {string} tokenId - Token ID to filter for
|
|
109
|
+
* @returns {Object} - XEC and token UTXOs for the specific token
|
|
110
|
+
*/
|
|
111
|
+
static filterUtxosForToken (utxos, tokenId) {
|
|
112
|
+
const categorized = this.categorizeUtxos(utxos)
|
|
113
|
+
const tokenUtxos = categorized.tokenUtxosById.get(tokenId) || []
|
|
114
|
+
|
|
115
|
+
// Get other UTXOs (not matching the requested tokenId) - includes other tokens AND XEC UTXOs
|
|
116
|
+
const otherUtxos = [...categorized.xecUtxos]
|
|
117
|
+
for (const [id, utxoList] of categorized.tokenUtxosById.entries()) {
|
|
118
|
+
if (id !== tokenId) {
|
|
119
|
+
otherUtxos.push(...utxoList)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (tokenUtxos.length === 0) {
|
|
124
|
+
return {
|
|
125
|
+
xecUtxos: categorized.xecUtxos,
|
|
126
|
+
tokenUtxos: [],
|
|
127
|
+
otherUtxos,
|
|
128
|
+
protocol: null,
|
|
129
|
+
tokenSummary: { totalAtoms: 0n, utxoCount: 0 }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Detect protocol from first token UTXO
|
|
134
|
+
const protocol = this.detectProtocol(tokenUtxos[0])
|
|
135
|
+
|
|
136
|
+
// Calculate token summary
|
|
137
|
+
const tokenSummary = this._calculateTokenSummary(tokenUtxos, protocol)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
xecUtxos: categorized.xecUtxos,
|
|
141
|
+
tokenUtxos,
|
|
142
|
+
otherUtxos,
|
|
143
|
+
protocol,
|
|
144
|
+
tokenSummary
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get all unique token IDs in wallet with their protocols
|
|
150
|
+
* @param {Array} utxos - Array of UTXO objects
|
|
151
|
+
* @returns {Array} - Array of {tokenId, protocol} objects
|
|
152
|
+
*/
|
|
153
|
+
static getTokenInventory (utxos) {
|
|
154
|
+
const tokenMap = new Map()
|
|
155
|
+
|
|
156
|
+
for (const utxo of utxos) {
|
|
157
|
+
if (utxo && utxo.token && utxo.token.tokenId) {
|
|
158
|
+
const tokenId = utxo.token.tokenId
|
|
159
|
+
if (!tokenMap.has(tokenId)) {
|
|
160
|
+
try {
|
|
161
|
+
const protocol = this.detectProtocol(utxo)
|
|
162
|
+
tokenMap.set(tokenId, {
|
|
163
|
+
tokenId,
|
|
164
|
+
protocol,
|
|
165
|
+
utxoCount: 1,
|
|
166
|
+
totalAtoms: BigInt(utxo.token.atoms || 0),
|
|
167
|
+
firstSeen: utxo.blockHeight || -1
|
|
168
|
+
})
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.warn(`Invalid token UTXO: ${err.message}`)
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
const entry = tokenMap.get(tokenId)
|
|
174
|
+
entry.utxoCount++
|
|
175
|
+
entry.totalAtoms += BigInt(utxo.token.atoms || 0)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return Array.from(tokenMap.values())
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Validate token protocol compatibility
|
|
185
|
+
* @param {string} expectedProtocol - Expected protocol (SLP or ALP)
|
|
186
|
+
* @param {Object} utxo - UTXO to validate
|
|
187
|
+
* @returns {boolean} - True if compatible
|
|
188
|
+
*/
|
|
189
|
+
static validateProtocolCompatibility (expectedProtocol, utxo) {
|
|
190
|
+
try {
|
|
191
|
+
const actualProtocol = this.detectProtocol(utxo)
|
|
192
|
+
return actualProtocol === expectedProtocol
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Private helper methods
|
|
199
|
+
|
|
200
|
+
static _addToProtocolMap (protocolMap, protocol, utxo) {
|
|
201
|
+
if (!protocolMap.has(protocol)) {
|
|
202
|
+
protocolMap.set(protocol, [])
|
|
203
|
+
}
|
|
204
|
+
protocolMap.get(protocol).push(utxo)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
static _addToTokenMap (tokenMap, tokenId, utxo) {
|
|
208
|
+
if (!tokenMap.has(tokenId)) {
|
|
209
|
+
tokenMap.set(tokenId, [])
|
|
210
|
+
}
|
|
211
|
+
tokenMap.get(tokenId).push(utxo)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
static _extractSats (utxo) {
|
|
215
|
+
if (!utxo || typeof utxo !== 'object') {
|
|
216
|
+
return 0
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (utxo.sats !== undefined) {
|
|
220
|
+
if (typeof utxo.sats === 'bigint') {
|
|
221
|
+
return Number(utxo.sats)
|
|
222
|
+
}
|
|
223
|
+
if (typeof utxo.sats === 'number') {
|
|
224
|
+
return utxo.sats
|
|
225
|
+
}
|
|
226
|
+
const parsed = parseInt(utxo.sats)
|
|
227
|
+
return isNaN(parsed) ? 0 : parsed
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return 0
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
static _calculateTokenSummary (tokenUtxos, protocol) {
|
|
234
|
+
let totalAtoms = 0n
|
|
235
|
+
const utxoCount = tokenUtxos.length
|
|
236
|
+
|
|
237
|
+
for (const utxo of tokenUtxos) {
|
|
238
|
+
if (utxo.token && utxo.token.atoms) {
|
|
239
|
+
totalAtoms += BigInt(utxo.token.atoms)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
totalAtoms,
|
|
245
|
+
utxoCount,
|
|
246
|
+
protocol
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Check if wallet contains any tokens
|
|
252
|
+
* @param {Array} utxos - Array of UTXO objects
|
|
253
|
+
* @returns {boolean} - True if any tokens found
|
|
254
|
+
*/
|
|
255
|
+
static hasTokens (utxos) {
|
|
256
|
+
return utxos.some(utxo => utxo && utxo.token && utxo.token.tokenId)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check if wallet contains specific protocol tokens
|
|
261
|
+
* @param {Array} utxos - Array of UTXO objects
|
|
262
|
+
* @param {string} protocol - Protocol to check for ('SLP' or 'ALP')
|
|
263
|
+
* @returns {boolean} - True if protocol tokens found
|
|
264
|
+
*/
|
|
265
|
+
static hasProtocolTokens (utxos, protocol) {
|
|
266
|
+
return utxos.some(utxo => {
|
|
267
|
+
if (!utxo) return false
|
|
268
|
+
try {
|
|
269
|
+
return this.detectProtocol(utxo) === protocol
|
|
270
|
+
} catch (err) {
|
|
271
|
+
return false
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get protocol statistics for wallet
|
|
278
|
+
* @param {Array} utxos - Array of UTXO objects
|
|
279
|
+
* @returns {Object} - Protocol statistics
|
|
280
|
+
*/
|
|
281
|
+
static getProtocolStats (utxos) {
|
|
282
|
+
const categorized = this.categorizeUtxos(utxos)
|
|
283
|
+
|
|
284
|
+
// Build protocols array from active protocols
|
|
285
|
+
const protocols = []
|
|
286
|
+
if (categorized.protocolSummary.slp.count > 0) protocols.push('SLP')
|
|
287
|
+
if (categorized.protocolSummary.alp.count > 0) protocols.push('ALP')
|
|
288
|
+
|
|
289
|
+
// Calculate total XEC sats from ALL UTXOs (including dust from token UTXOs)
|
|
290
|
+
const totalXecSats = utxos.reduce((sum, utxo) => sum + this._extractSats(utxo), 0)
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
totalUtxos: utxos.length,
|
|
294
|
+
xecUtxos: categorized.protocolSummary.xec.count,
|
|
295
|
+
slpTokens: categorized.protocolSummary.slp.tokenTypes.size,
|
|
296
|
+
slpUtxos: categorized.protocolSummary.slp.count,
|
|
297
|
+
alpTokens: categorized.protocolSummary.alp.tokenTypes.size,
|
|
298
|
+
alpUtxos: categorized.protocolSummary.alp.count,
|
|
299
|
+
totalXecSats,
|
|
300
|
+
hasTokens: this.hasTokens(utxos),
|
|
301
|
+
protocols,
|
|
302
|
+
hasMultipleProtocols: protocols.length > 1
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = TokenProtocolDetector
|
package/lib/utxos.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Simplified UTXO management for minimal XEC wallet
|
|
3
|
+
|
|
4
|
+
Core functionality only:
|
|
5
|
+
- Fetch UTXOs from chronik
|
|
6
|
+
- Basic validation and filtering
|
|
7
|
+
- Simple caching
|
|
8
|
+
- Essential security checks
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const SecurityValidator = require('./security')
|
|
12
|
+
|
|
13
|
+
class Utxos {
|
|
14
|
+
constructor (localConfig = {}) {
|
|
15
|
+
this.chronik = localConfig.chronik
|
|
16
|
+
this.ar = localConfig.ar
|
|
17
|
+
|
|
18
|
+
if (!this.ar) {
|
|
19
|
+
throw new Error('AdapterRouter instance required for UTXO management')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Simple UTXO store
|
|
23
|
+
this.utxoStore = {
|
|
24
|
+
xecUtxos: [],
|
|
25
|
+
lastUpdated: null,
|
|
26
|
+
cacheKey: null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Security validator
|
|
30
|
+
this.security = new SecurityValidator(localConfig.security)
|
|
31
|
+
|
|
32
|
+
// Simple configuration
|
|
33
|
+
this.maxRetries = localConfig.maxRetries || 3
|
|
34
|
+
this.retryDelay = localConfig.retryDelay || 1000
|
|
35
|
+
this.cacheTimeout = localConfig.cacheTimeout || 30000 // 30 seconds
|
|
36
|
+
|
|
37
|
+
// Performance tracking (basic)
|
|
38
|
+
this.performanceMetrics = {
|
|
39
|
+
totalRequests: 0,
|
|
40
|
+
cacheHits: 0,
|
|
41
|
+
lastRefreshTime: null,
|
|
42
|
+
totalResponseTime: 0,
|
|
43
|
+
averageResponseTime: 0
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize UTXO store for an address
|
|
49
|
+
* @param {string} addr - XEC address
|
|
50
|
+
* @param {boolean} forceRefresh - Force refresh cache
|
|
51
|
+
* @returns {boolean} - Success status
|
|
52
|
+
*/
|
|
53
|
+
async initUtxoStore (addr, forceRefresh = false) {
|
|
54
|
+
try {
|
|
55
|
+
this.performanceMetrics.totalRequests++
|
|
56
|
+
|
|
57
|
+
// Check cache validity
|
|
58
|
+
if (!forceRefresh && this._isCacheValid(addr)) {
|
|
59
|
+
this.performanceMetrics.cacheHits++
|
|
60
|
+
return true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fetch fresh UTXO data
|
|
64
|
+
const utxosResult = await this._fetchUtxosWithRetry(addr)
|
|
65
|
+
|
|
66
|
+
// Process and store UTXOs
|
|
67
|
+
this._processUtxos(utxosResult, addr)
|
|
68
|
+
|
|
69
|
+
return true
|
|
70
|
+
} catch (err) {
|
|
71
|
+
throw new Error(`UTXO initialization failed: ${err.message}`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get spendable XEC UTXOs with basic filtering
|
|
77
|
+
* @param {Object} options - Filtering options
|
|
78
|
+
* @returns {Array} - Filtered UTXOs
|
|
79
|
+
*/
|
|
80
|
+
getSpendableXecUtxos (options = {}) {
|
|
81
|
+
const {
|
|
82
|
+
includeUnconfirmed = false,
|
|
83
|
+
excludeDustAttack = true
|
|
84
|
+
} = options
|
|
85
|
+
|
|
86
|
+
// Use security validator for filtering
|
|
87
|
+
return this.security.filterSecureUtxos(this.utxoStore.xecUtxos, {
|
|
88
|
+
includeUnconfirmed,
|
|
89
|
+
excludeDustAttack
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Simple UTXO selection (largest first)
|
|
95
|
+
* @param {number} targetAmount - Target amount in satoshis
|
|
96
|
+
* @param {Object} options - Selection options
|
|
97
|
+
* @returns {Object} - Selection result
|
|
98
|
+
*/
|
|
99
|
+
selectOptimalUtxos (targetAmount, options = {}) {
|
|
100
|
+
const spendableUtxos = this.getSpendableXecUtxos(options)
|
|
101
|
+
|
|
102
|
+
if (spendableUtxos.length === 0) {
|
|
103
|
+
throw new Error('No spendable UTXOs available')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Sort by value descending (largest first)
|
|
107
|
+
const sortedUtxos = spendableUtxos.sort((a, b) => {
|
|
108
|
+
const aValue = this._getUtxoValue(a)
|
|
109
|
+
const bValue = this._getUtxoValue(b)
|
|
110
|
+
return bValue - aValue
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Simple greedy selection
|
|
114
|
+
const selectedUtxos = []
|
|
115
|
+
let totalAmount = 0
|
|
116
|
+
const inputCost = 148 // P2PKH input size in bytes
|
|
117
|
+
|
|
118
|
+
for (const utxo of sortedUtxos) {
|
|
119
|
+
const utxoValue = this._getUtxoValue(utxo)
|
|
120
|
+
selectedUtxos.push(utxo)
|
|
121
|
+
totalAmount += utxoValue
|
|
122
|
+
|
|
123
|
+
// Estimate fee
|
|
124
|
+
const estimatedFee = selectedUtxos.length * inputCost + 34 + 10 // inputs + output + overhead
|
|
125
|
+
|
|
126
|
+
if (totalAmount >= targetAmount + estimatedFee) {
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check if we have enough
|
|
132
|
+
const finalFee = selectedUtxos.length * inputCost + 34 + 10
|
|
133
|
+
if (totalAmount < targetAmount + finalFee) {
|
|
134
|
+
throw new Error('Insufficient funds')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
selectedUtxos,
|
|
139
|
+
totalAmount,
|
|
140
|
+
estimatedFee: finalFee,
|
|
141
|
+
change: totalAmount - targetAmount - finalFee
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get current balance
|
|
147
|
+
* @returns {Object} - Balance information
|
|
148
|
+
*/
|
|
149
|
+
getBalance () {
|
|
150
|
+
const utxos = this.utxoStore.xecUtxos
|
|
151
|
+
let confirmed = 0
|
|
152
|
+
let unconfirmed = 0
|
|
153
|
+
|
|
154
|
+
utxos.forEach(utxo => {
|
|
155
|
+
const value = this._getUtxoValue(utxo)
|
|
156
|
+
if (utxo.blockHeight === -1) {
|
|
157
|
+
unconfirmed += value
|
|
158
|
+
} else {
|
|
159
|
+
confirmed += value
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
confirmed,
|
|
165
|
+
unconfirmed,
|
|
166
|
+
total: confirmed + unconfirmed
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get performance metrics
|
|
172
|
+
* @returns {Object} - Performance data
|
|
173
|
+
*/
|
|
174
|
+
getPerformanceMetrics () {
|
|
175
|
+
const cacheHitRate = this.performanceMetrics.totalRequests > 0
|
|
176
|
+
? (this.performanceMetrics.cacheHits / this.performanceMetrics.totalRequests) * 100
|
|
177
|
+
: 0
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
cacheHitRate: Math.round(cacheHitRate * 100) / 100,
|
|
181
|
+
totalRequests: this.performanceMetrics.totalRequests,
|
|
182
|
+
cacheHits: this.performanceMetrics.cacheHits,
|
|
183
|
+
averageResponseTime: this.performanceMetrics.averageResponseTime,
|
|
184
|
+
lastRefreshTime: this.performanceMetrics.lastRefreshTime,
|
|
185
|
+
utxoCount: this.utxoStore.xecUtxos.length
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Clear cache
|
|
191
|
+
*/
|
|
192
|
+
clearCache () {
|
|
193
|
+
this.utxoStore = {
|
|
194
|
+
xecUtxos: [],
|
|
195
|
+
lastUpdated: null,
|
|
196
|
+
cacheKey: null
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get spendable eToken UTXOs (Phase 2 - not implemented)
|
|
202
|
+
* @returns {Array} - Empty array for Phase 1
|
|
203
|
+
*/
|
|
204
|
+
getSpendableETokenUtxos () {
|
|
205
|
+
return []
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Refresh cache for address
|
|
210
|
+
* @param {string} addr - Address to refresh
|
|
211
|
+
* @returns {boolean} - Success status
|
|
212
|
+
*/
|
|
213
|
+
async refreshCache (addr) {
|
|
214
|
+
return await this.initUtxoStore(addr, true)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Filter dust UTXOs (legacy method)
|
|
219
|
+
* @param {Array} utxos - UTXOs to filter
|
|
220
|
+
* @returns {Array} - Non-dust UTXOs
|
|
221
|
+
*/
|
|
222
|
+
_filterDustUtxos (utxos) {
|
|
223
|
+
return utxos.filter(utxo => this._getUtxoValue(utxo) >= 1000)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Sort UTXOs by value (legacy method)
|
|
228
|
+
* @param {Array} utxos - UTXOs to sort
|
|
229
|
+
* @param {string} order - 'asc' or 'desc'
|
|
230
|
+
* @returns {Array} - Sorted UTXOs
|
|
231
|
+
*/
|
|
232
|
+
_sortUtxosByValue (utxos, order = 'desc') {
|
|
233
|
+
return [...utxos].sort((a, b) => {
|
|
234
|
+
const aValue = this._getUtxoValue(a)
|
|
235
|
+
const bValue = this._getUtxoValue(b)
|
|
236
|
+
return order === 'desc' ? bValue - aValue : aValue - bValue
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Private methods
|
|
241
|
+
|
|
242
|
+
async _fetchUtxosWithRetry (addr, maxRetries = null) {
|
|
243
|
+
const retryLimit = maxRetries || this.maxRetries
|
|
244
|
+
let attempt = 1
|
|
245
|
+
|
|
246
|
+
while (attempt <= retryLimit) {
|
|
247
|
+
try {
|
|
248
|
+
const utxosResult = await this.ar.getUtxos(addr)
|
|
249
|
+
return utxosResult
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (attempt === retryLimit) {
|
|
252
|
+
throw new Error(`Failed to fetch UTXOs after ${retryLimit} attempts: ${err.message}`)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await this._delay(this.retryDelay * attempt)
|
|
256
|
+
attempt++
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
_processUtxos (utxosResult, addr) {
|
|
262
|
+
if (!utxosResult || !Array.isArray(utxosResult.utxos)) {
|
|
263
|
+
throw new Error('Invalid UTXO response format')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Filter and validate UTXOs
|
|
267
|
+
const validUtxos = utxosResult.utxos.filter(utxo => this._isValidUtxo(utxo))
|
|
268
|
+
|
|
269
|
+
// Store UTXOs
|
|
270
|
+
this.utxoStore.xecUtxos = validUtxos
|
|
271
|
+
this.utxoStore.lastUpdated = Date.now()
|
|
272
|
+
this.utxoStore.cacheKey = addr
|
|
273
|
+
this.performanceMetrics.lastRefreshTime = Date.now()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_isValidUtxo (utxo) {
|
|
277
|
+
return this.security.isValidUtxoStructure(utxo) && this._getUtxoValue(utxo) > 0
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_getUtxoValue (utxo) {
|
|
281
|
+
if (utxo.sats !== undefined) {
|
|
282
|
+
return typeof utxo.sats === 'bigint' ? Number(utxo.sats) : parseInt(utxo.sats)
|
|
283
|
+
}
|
|
284
|
+
if (utxo.value !== undefined) {
|
|
285
|
+
return typeof utxo.value === 'bigint' ? Number(utxo.value) : parseInt(utxo.value)
|
|
286
|
+
}
|
|
287
|
+
return 0
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_isCacheValid (addr) {
|
|
291
|
+
return (
|
|
292
|
+
this.utxoStore.cacheKey === addr &&
|
|
293
|
+
this.utxoStore.lastUpdated &&
|
|
294
|
+
(Date.now() - this.utxoStore.lastUpdated) < this.cacheTimeout
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
_delay (ms) {
|
|
299
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = Utxos
|