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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -0
  3. package/dist/minimal-xec-wallet.js +66268 -0
  4. package/dist/minimal-xec-wallet.min.js +55 -0
  5. package/examples/README.md +380 -0
  6. package/examples/advanced/browser-compatibility-test.js +263 -0
  7. package/examples/advanced/get-xec-price.js +149 -0
  8. package/examples/advanced/optimize-utxos.js +255 -0
  9. package/examples/advanced/send-op-return.js +216 -0
  10. package/examples/browser-test.html +350 -0
  11. package/examples/key-management/derive-addresses.js +191 -0
  12. package/examples/key-management/export-to-wif.js +114 -0
  13. package/examples/key-management/validate-address.js +214 -0
  14. package/examples/optimization/simple-consolidation-test.js +79 -0
  15. package/examples/optimization/test-utxo-consolidation.js +179 -0
  16. package/examples/test-examples.js +1204 -0
  17. package/examples/tokens/burn-tokens.js +293 -0
  18. package/examples/tokens/get-token-balance.js +169 -0
  19. package/examples/tokens/get-token-info.js +269 -0
  20. package/examples/tokens/list-all-tokens.js +162 -0
  21. package/examples/tokens/send-any-token.js +260 -0
  22. package/examples/tokens/test-main-wallet-integration.js +193 -0
  23. package/examples/transactions/send-all-xec.js +205 -0
  24. package/examples/transactions/send-to-multiple.js +217 -0
  25. package/examples/transactions/send-xec.js +191 -0
  26. package/examples/utils/show-qr.js +119 -0
  27. package/examples/utils/wallet-helper.js +176 -0
  28. package/examples/validation/comprehensive-infrastructure-test.js +210 -0
  29. package/examples/wallet-creation/create-new-wallet.js +67 -0
  30. package/examples/wallet-creation/import-from-wif.js +135 -0
  31. package/examples/wallet-creation/restore-from-mnemonic.js +100 -0
  32. package/examples/wallet-info/get-balance.js +99 -0
  33. package/examples/wallet-info/get-transactions.js +157 -0
  34. package/examples/wallet-info/get-utxos.js +145 -0
  35. package/examples/wallet.json +11 -0
  36. package/lib/adapters/robust-chronik-router.js +507 -0
  37. package/lib/adapters/router.js +651 -0
  38. package/lib/alp-token-handler.js +581 -0
  39. package/lib/browser-wasm-loader.js +271 -0
  40. package/lib/consolidate-utxos.js +338 -0
  41. package/lib/hybrid-token-manager.js +322 -0
  42. package/lib/key-derivation.js +466 -0
  43. package/lib/op-return.js +314 -0
  44. package/lib/security.js +270 -0
  45. package/lib/send-xec.js +396 -0
  46. package/lib/slp-token-handler.js +572 -0
  47. package/lib/token-protocol-detector.js +307 -0
  48. package/lib/utxos.js +303 -0
  49. package/package.json +125 -0
@@ -0,0 +1,145 @@
1
+ /*
2
+ Get the UTXOs (Unspent Transaction Outputs) of your wallet.
3
+ This example shows how to view all spendable outputs in your wallet.
4
+ */
5
+
6
+ const MinimalXECWallet = require('../../index')
7
+ const WalletHelper = require('../utils/wallet-helper')
8
+
9
+ async function getUtxos () {
10
+ try {
11
+ console.log('šŸ” Getting wallet UTXOs...\n')
12
+
13
+ // Load wallet from file
14
+ const walletData = WalletHelper.loadWallet()
15
+ if (!walletData) {
16
+ console.log(' Run: node examples/wallet-creation/create-new-wallet.js')
17
+ return
18
+ }
19
+
20
+ // Create wallet instance from saved data
21
+ const wallet = new MinimalXECWallet(walletData.mnemonic || walletData.privateKey)
22
+ await wallet.walletInfoPromise
23
+
24
+ console.log('šŸ”— Fetching UTXOs from blockchain...')
25
+
26
+ // Get UTXOs for the wallet
27
+ const utxoData = await wallet.getUtxos()
28
+ const utxos = utxoData.utxos || []
29
+
30
+ console.log('\nšŸ“¦ UTXO Information:')
31
+ console.log('═'.repeat(70))
32
+ console.log(`XEC Address: ${walletData.xecAddress}`)
33
+ console.log(`Total UTXOs: ${utxos.length}`)
34
+ console.log('═'.repeat(70))
35
+
36
+ if (utxos.length === 0) {
37
+ console.log('\nšŸ’ø No UTXOs found!')
38
+ console.log(' Your wallet has no spendable outputs.')
39
+ console.log(' This means either:')
40
+ console.log(' • Your wallet is empty')
41
+ console.log(' • All funds are in unconfirmed transactions')
42
+ console.log(' • The address has never received any XEC')
43
+ return
44
+ }
45
+
46
+ // Group UTXOs by confirmation status
47
+ const confirmedUtxos = utxos.filter(utxo => utxo.blockHeight !== -1)
48
+ const unconfirmedUtxos = utxos.filter(utxo => utxo.blockHeight === -1)
49
+
50
+ // Calculate totals - handle sats field properly
51
+ const getUtxoValue = (utxo) => {
52
+ if (utxo.sats !== undefined) {
53
+ return typeof utxo.sats === 'bigint' ? Number(utxo.sats) : parseInt(utxo.sats)
54
+ }
55
+ if (utxo.value !== undefined) {
56
+ return typeof utxo.value === 'bigint' ? Number(utxo.value) : parseInt(utxo.value)
57
+ }
58
+ return 0
59
+ }
60
+
61
+ const totalConfirmed = confirmedUtxos.reduce((sum, utxo) => sum + getUtxoValue(utxo), 0)
62
+ const totalUnconfirmed = unconfirmedUtxos.reduce((sum, utxo) => sum + getUtxoValue(utxo), 0)
63
+ const totalValue = totalConfirmed + totalUnconfirmed
64
+
65
+ console.log('\nšŸ’° Balance Summary:')
66
+ console.log(`Confirmed: ${(totalConfirmed / 100).toLocaleString()} XEC (${confirmedUtxos.length} UTXOs)`)
67
+ console.log(`Unconfirmed: ${(totalUnconfirmed / 100).toLocaleString()} XEC (${unconfirmedUtxos.length} UTXOs)`)
68
+ console.log(`Total: ${(totalValue / 100).toLocaleString()} XEC`)
69
+
70
+ // Show confirmed UTXOs
71
+ if (confirmedUtxos.length > 0) {
72
+ console.log('\nāœ… Confirmed UTXOs:')
73
+ console.log('─'.repeat(70))
74
+ confirmedUtxos.forEach((utxo, index) => {
75
+ const satsValue = typeof utxo.sats === 'bigint' ? Number(utxo.sats) : parseInt(utxo.sats)
76
+ const xecValue = (satsValue / 100).toLocaleString()
77
+ const txidShort = `${utxo.outpoint.txid.substring(0, 8)}...${utxo.outpoint.txid.substring(56)}`
78
+ console.log(`${index + 1}. ${xecValue} XEC`)
79
+ console.log(` TXID: ${txidShort}:${utxo.outpoint.outIdx}`)
80
+ console.log(` Block: ${utxo.blockHeight}`)
81
+ console.log(` Coinbase: ${utxo.isCoinbase ? 'Yes' : 'No'}`)
82
+ console.log()
83
+ })
84
+ }
85
+
86
+ // Show unconfirmed UTXOs
87
+ if (unconfirmedUtxos.length > 0) {
88
+ console.log('ā³ Unconfirmed UTXOs:')
89
+ console.log('─'.repeat(70))
90
+ unconfirmedUtxos.forEach((utxo, index) => {
91
+ const satsValue = typeof utxo.sats === 'bigint' ? Number(utxo.sats) : parseInt(utxo.sats)
92
+ const xecValue = (satsValue / 100).toLocaleString()
93
+ const txidShort = `${utxo.outpoint.txid.substring(0, 8)}...${utxo.outpoint.txid.substring(56)}`
94
+ console.log(`${index + 1}. ${xecValue} XEC (pending confirmation)`)
95
+ console.log(` TXID: ${txidShort}:${utxo.outpoint.outIdx}`)
96
+ console.log()
97
+ })
98
+ console.log('šŸ“ Note: Unconfirmed UTXOs cannot be spent until they are confirmed.')
99
+ }
100
+
101
+ // UTXO analysis
102
+ console.log('\nšŸ“Š UTXO Analysis:')
103
+ console.log('─'.repeat(70))
104
+
105
+ if (confirmedUtxos.length > 0) {
106
+ const values = confirmedUtxos.map(utxo => getUtxoValue(utxo) / 100)
107
+ const avgValue = values.reduce((a, b) => a + b, 0) / values.length
108
+ const maxValue = Math.max(...values)
109
+ const minValue = Math.min(...values)
110
+
111
+ console.log(`Average UTXO size: ${avgValue.toLocaleString()} XEC`)
112
+ console.log(`Largest UTXO: ${maxValue.toLocaleString()} XEC`)
113
+ console.log(`Smallest UTXO: ${minValue.toLocaleString()} XEC`)
114
+
115
+ // Dust analysis (UTXOs < 5.46 XEC are considered dust)
116
+ const dustUtxos = confirmedUtxos.filter(utxo => utxo.value < 546)
117
+ if (dustUtxos.length > 0) {
118
+ console.log(`Dust UTXOs: ${dustUtxos.length} (< 5.46 XEC)`)
119
+ console.log('šŸ’” Consider consolidating dust UTXOs to reduce transaction fees')
120
+ }
121
+ }
122
+
123
+ // Suggest optimizations
124
+ if (confirmedUtxos.length > 20) {
125
+ console.log('\n⚔ Optimization Suggestion:')
126
+ console.log(` You have ${confirmedUtxos.length} UTXOs, which might increase transaction fees.`)
127
+ console.log(' Consider running: node examples/advanced/optimize-utxos.js')
128
+ }
129
+ } catch (err) {
130
+ console.error('āŒ Failed to get UTXOs:', err.message)
131
+
132
+ // Provide helpful error context
133
+ if (err.message.includes('network') || err.message.includes('connection')) {
134
+ console.log('\n🌐 Network Error:')
135
+ console.log(' • Check your internet connection')
136
+ console.log(' • Chronik indexer might be temporarily unavailable')
137
+ console.log(' • Try again in a few moments')
138
+ }
139
+
140
+ process.exit(1)
141
+ }
142
+ }
143
+
144
+ // Run the example
145
+ getUtxos()
@@ -0,0 +1,11 @@
1
+ {
2
+ "description": "New XEC Wallet",
3
+ "created": "2025-08-15T04:08:15.166Z",
4
+ "mnemonic": "bubble leader travel summer unit fault nation error firm mad leisure urge",
5
+ "xecAddress": "ecash:qpg562clu3350dnk3z3lenvxgyexyt7j6vnz4qg606",
6
+ "privateKey": "efa59377256849360662d4af7b39851f658a188b93862889ddf37b45e88c5784",
7
+ "publicKey": "031d85d1053f528a80b3d44fbef5ba081a4f6a9ca65982565cbfa92165decf8319",
8
+ "hdPath": "m/44'/899'/0'/0/0",
9
+ "fee": 1.2,
10
+ "enableDonations": false
11
+ }
@@ -0,0 +1,507 @@
1
+ /*
2
+ Robust Chronik Router with intelligent failover, health monitoring, and connection pooling.
3
+ Based on Bitcoin-ABC chronik-client patterns with enhanced reliability features.
4
+ */
5
+
6
+ const { ChronikClient } = require('chronik-client')
7
+
8
+ // Connection error types for intelligent error handling
9
+ const ConnectionErrorType = {
10
+ NETWORK_TIMEOUT: 'network_timeout',
11
+ SERVER_UNAVAILABLE: 'server_unavailable',
12
+ SERVER_INDEXING: 'server_indexing',
13
+ PROTOCOL_ERROR: 'protocol_error',
14
+ RATE_LIMITED: 'rate_limited',
15
+ CONNECTION_REFUSED: 'connection_refused'
16
+ }
17
+
18
+ // Connection strategies available
19
+ const RobustConnectionStrategy = {
20
+ CLOSEST_FIRST: 'ClosestFirst',
21
+ ROUND_ROBIN: 'RoundRobin',
22
+ PRIORITY_BASED: 'PriorityBased',
23
+ LOAD_BALANCED: 'LoadBalanced'
24
+ }
25
+
26
+ class EndpointHealthMonitor {
27
+ constructor (config = {}) {
28
+ this.healthCheckInterval = config.healthCheckInterval || 30000 // 30 seconds
29
+ this.maxLatencyHistory = config.maxLatencyHistory || 10
30
+ this.healthCheckTimeout = config.healthCheckTimeout || 5000
31
+
32
+ // Health tracking data
33
+ this.healthScores = new Map()
34
+ this.latencyHistory = new Map()
35
+ this.failureCount = new Map()
36
+ this.lastHealthCheck = new Map()
37
+ this.isMonitoring = false
38
+ this.healthCheckTimer = null
39
+ }
40
+
41
+ async startMonitoring (endpoints) {
42
+ if (this.isMonitoring) return
43
+
44
+ this.isMonitoring = true
45
+
46
+ // Initialize health data for all endpoints
47
+ for (const endpoint of endpoints) {
48
+ this.initializeEndpointHealth(endpoint.url)
49
+ }
50
+
51
+ // Start periodic health checks
52
+ this.healthCheckTimer = setInterval(() => {
53
+ this._performHealthChecks(endpoints)
54
+ }, this.healthCheckInterval)
55
+
56
+ // Perform initial health check
57
+ await this._performHealthChecks(endpoints)
58
+ }
59
+
60
+ stopMonitoring () {
61
+ this.isMonitoring = false
62
+ if (this.healthCheckTimer) {
63
+ clearInterval(this.healthCheckTimer)
64
+ this.healthCheckTimer = null
65
+ }
66
+ }
67
+
68
+ initializeEndpointHealth (url) {
69
+ this.healthScores.set(url, 100) // Start with perfect health
70
+ this.latencyHistory.set(url, [])
71
+ this.failureCount.set(url, 0)
72
+ this.lastHealthCheck.set(url, 0)
73
+ }
74
+
75
+ async _performHealthChecks (endpoints) {
76
+ const healthCheckPromises = endpoints.map(endpoint =>
77
+ this._checkSingleEndpoint(endpoint).catch(err => {
78
+ console.warn(`Health check failed for ${endpoint.url}:`, err.message)
79
+ return false
80
+ })
81
+ )
82
+
83
+ await Promise.allSettled(healthCheckPromises)
84
+ }
85
+
86
+ async _checkSingleEndpoint (endpoint) {
87
+ const startTime = Date.now()
88
+
89
+ try {
90
+ // Use chronik's blockchain info as a lightweight health check
91
+ const chronik = new ChronikClient(endpoint.url)
92
+ await Promise.race([
93
+ chronik.blockchainInfo(),
94
+ new Promise((resolve, reject) =>
95
+ setTimeout(() => reject(new Error('Health check timeout')), this.healthCheckTimeout)
96
+ )
97
+ ])
98
+
99
+ const latency = Date.now() - startTime
100
+ this._recordSuccess(endpoint.url, latency)
101
+ return true
102
+ } catch (err) {
103
+ this._recordFailure(endpoint.url)
104
+ return false
105
+ }
106
+ }
107
+
108
+ _recordSuccess (url, latency) {
109
+ // Update latency history
110
+ const history = this.latencyHistory.get(url) || []
111
+ history.push(latency)
112
+ if (history.length > this.maxLatencyHistory) {
113
+ history.shift()
114
+ }
115
+ this.latencyHistory.set(url, history)
116
+
117
+ // Reset failure count on success
118
+ this.failureCount.set(url, 0)
119
+
120
+ // Improve health score
121
+ const currentScore = this.healthScores.get(url) || 0
122
+ const newScore = Math.min(100, currentScore + 10)
123
+ this.healthScores.set(url, newScore)
124
+
125
+ this.lastHealthCheck.set(url, Date.now())
126
+ }
127
+
128
+ _recordFailure (url) {
129
+ const failures = (this.failureCount.get(url) || 0) + 1
130
+ this.failureCount.set(url, failures)
131
+
132
+ // Reduce health score based on consecutive failures
133
+ const currentScore = this.healthScores.get(url) || 100
134
+ const penalty = Math.min(30, failures * 10)
135
+ const newScore = Math.max(0, currentScore - penalty)
136
+ this.healthScores.set(url, newScore)
137
+
138
+ this.lastHealthCheck.set(url, Date.now())
139
+ }
140
+
141
+ getHealthScore (url) {
142
+ return this.healthScores.get(url) || 0
143
+ }
144
+
145
+ getAverageLatency (url) {
146
+ const history = this.latencyHistory.get(url) || []
147
+ if (history.length === 0) return Infinity
148
+
149
+ const sum = history.reduce((a, b) => a + b, 0)
150
+ return sum / history.length
151
+ }
152
+
153
+ getHealthiestEndpoints (endpoints) {
154
+ return endpoints
155
+ .filter(endpoint => this.getHealthScore(endpoint.url) > 20) // Only consider reasonably healthy endpoints
156
+ .sort((a, b) => {
157
+ const scoreA = this.getHealthScore(a.url)
158
+ const scoreB = this.getHealthScore(b.url)
159
+ const latencyA = this.getAverageLatency(a.url)
160
+ const latencyB = this.getAverageLatency(b.url)
161
+
162
+ // Prioritize health score, then latency
163
+ if (scoreA !== scoreB) {
164
+ return scoreB - scoreA // Higher score is better
165
+ }
166
+ return latencyA - latencyB // Lower latency is better
167
+ })
168
+ }
169
+
170
+ isEndpointHealthy (url, minScore = 30) {
171
+ const score = this.getHealthScore(url)
172
+ const failures = this.failureCount.get(url) || 0
173
+ return score >= minScore && failures < 3
174
+ }
175
+ }
176
+
177
+ class ConnectionErrorHandler {
178
+ constructor () {
179
+ this.maxRetries = 3
180
+ this.baseRetryDelay = 1000 // 1 second
181
+ this.maxRetryDelay = 30000 // 30 seconds
182
+ }
183
+
184
+ classifyError (error) {
185
+ const errorMsg = error.message?.toLowerCase() || ''
186
+ const errorCode = error.code?.toLowerCase() || ''
187
+
188
+ // Network-level errors
189
+ if (errorCode === 'econnrefused' || errorMsg.includes('connection refused')) {
190
+ return ConnectionErrorType.CONNECTION_REFUSED
191
+ }
192
+
193
+ if (errorCode === 'etimedout' || errorMsg.includes('timeout')) {
194
+ return ConnectionErrorType.NETWORK_TIMEOUT
195
+ }
196
+
197
+ // Server-level errors
198
+ if (errorMsg.includes('indexing') || errorMsg.includes('error state')) {
199
+ return ConnectionErrorType.SERVER_INDEXING
200
+ }
201
+
202
+ if (errorMsg.includes('rate limit') || errorMsg.includes('too many requests')) {
203
+ return ConnectionErrorType.RATE_LIMITED
204
+ }
205
+
206
+ if (errorMsg.includes('server unavailable') || errorMsg.includes('503')) {
207
+ return ConnectionErrorType.SERVER_UNAVAILABLE
208
+ }
209
+
210
+ // Default to protocol error for chronik-specific errors
211
+ return ConnectionErrorType.PROTOCOL_ERROR
212
+ }
213
+
214
+ shouldRetry (errorType, attemptNumber) {
215
+ if (attemptNumber >= this.maxRetries) return false
216
+
217
+ switch (errorType) {
218
+ case ConnectionErrorType.NETWORK_TIMEOUT:
219
+ case ConnectionErrorType.CONNECTION_REFUSED:
220
+ case ConnectionErrorType.SERVER_UNAVAILABLE:
221
+ return true
222
+ case ConnectionErrorType.SERVER_INDEXING:
223
+ return attemptNumber < 2 // Only retry once for indexing servers
224
+ case ConnectionErrorType.RATE_LIMITED:
225
+ return true
226
+ case ConnectionErrorType.PROTOCOL_ERROR:
227
+ return false // Don't retry protocol errors
228
+ default:
229
+ return false
230
+ }
231
+ }
232
+
233
+ getRetryDelay (attemptNumber, errorType) {
234
+ const baseDelay = this.baseRetryDelay
235
+
236
+ // Adjust base delay based on error type
237
+ let multiplier = 1
238
+ switch (errorType) {
239
+ case ConnectionErrorType.RATE_LIMITED:
240
+ multiplier = 3 // Longer delay for rate limiting
241
+ break
242
+ case ConnectionErrorType.SERVER_INDEXING:
243
+ multiplier = 5 // Much longer delay for indexing servers
244
+ break
245
+ case ConnectionErrorType.NETWORK_TIMEOUT:
246
+ multiplier = 2
247
+ break
248
+ default:
249
+ multiplier = 1
250
+ }
251
+
252
+ // Exponential backoff with jitter
253
+ const exponentialDelay = baseDelay * multiplier * Math.pow(2, attemptNumber - 1)
254
+ const jitter = Math.random() * 0.1 * exponentialDelay // 10% jitter
255
+ const finalDelay = Math.min(exponentialDelay + jitter, this.maxRetryDelay)
256
+
257
+ return Math.floor(finalDelay)
258
+ }
259
+ }
260
+
261
+ class RobustChronikRouter {
262
+ constructor (config = {}) {
263
+ this.strategy = config.strategy || RobustConnectionStrategy.CLOSEST_FIRST
264
+ this.endpoints = []
265
+ this.currentEndpointIndex = 0
266
+ this.healthMonitor = new EndpointHealthMonitor(config.healthMonitor || {})
267
+ this.errorHandler = new ConnectionErrorHandler()
268
+ this.connectionPool = new Map()
269
+ this.maxConnectionsPerEndpoint = config.maxConnectionsPerEndpoint || 3
270
+ this.connectionTimeout = config.connectionTimeout || 10000
271
+ this.isInitialized = false
272
+
273
+ // Statistics
274
+ this.stats = {
275
+ requestCount: 0,
276
+ failoverCount: 0,
277
+ totalLatency: 0,
278
+ errorsByType: new Map()
279
+ }
280
+ }
281
+
282
+ async initialize (urls, strategyOptions = {}) {
283
+ try {
284
+ if (this.isInitialized) {
285
+ await this.cleanup()
286
+ }
287
+
288
+ // Validate and prepare endpoints
289
+ this.endpoints = this._prepareEndpoints(urls)
290
+
291
+ // Initialize health monitoring
292
+ await this.healthMonitor.startMonitoring(this.endpoints)
293
+
294
+ // Try to establish initial connection using chosen strategy
295
+ await this._initializeWithStrategy(strategyOptions)
296
+
297
+ this.isInitialized = true
298
+ console.log(`RobustChronikRouter initialized with ${this.endpoints.length} endpoints using ${this.strategy} strategy`)
299
+ } catch (err) {
300
+ throw new Error(`Router initialization failed: ${err.message}`)
301
+ }
302
+ }
303
+
304
+ _prepareEndpoints (urls) {
305
+ const urlArray = Array.isArray(urls) ? urls : [urls]
306
+
307
+ if (urlArray.length === 0) {
308
+ throw new Error('At least one endpoint URL is required')
309
+ }
310
+
311
+ return urlArray.map((url, index) => ({
312
+ url: this._normalizeUrl(url),
313
+ priority: index, // Lower index = higher priority
314
+ isActive: true,
315
+ createdAt: Date.now()
316
+ }))
317
+ }
318
+
319
+ _normalizeUrl (url) {
320
+ if (!url || typeof url !== 'string') {
321
+ throw new Error('URL must be a non-empty string')
322
+ }
323
+
324
+ // Remove trailing slash
325
+ const cleanUrl = url.replace(/\/$/, '')
326
+
327
+ // Validate URL format
328
+ if (!cleanUrl.startsWith('http://') && !cleanUrl.startsWith('https://')) {
329
+ throw new Error(`Invalid URL format: ${url}`)
330
+ }
331
+
332
+ return cleanUrl
333
+ }
334
+
335
+ async _initializeWithStrategy (options) {
336
+ switch (this.strategy) {
337
+ case RobustConnectionStrategy.CLOSEST_FIRST:
338
+ await this._initializeClosestFirst()
339
+ break
340
+ case RobustConnectionStrategy.PRIORITY_BASED:
341
+ this._initializePriorityBased()
342
+ break
343
+ case RobustConnectionStrategy.ROUND_ROBIN:
344
+ this._initializeRoundRobin()
345
+ break
346
+ default:
347
+ this._initializePriorityBased() // Default fallback
348
+ }
349
+ }
350
+
351
+ async _initializeClosestFirst () {
352
+ // Wait for initial health checks to complete
353
+ await new Promise(resolve => setTimeout(resolve, 1000))
354
+
355
+ // Sort endpoints by health and latency
356
+ const healthyEndpoints = this.healthMonitor.getHealthiestEndpoints(this.endpoints)
357
+
358
+ if (healthyEndpoints.length > 0) {
359
+ this.currentEndpointIndex = this.endpoints.indexOf(healthyEndpoints[0])
360
+ } else {
361
+ console.warn('No healthy endpoints found, using first endpoint')
362
+ this.currentEndpointIndex = 0
363
+ }
364
+ }
365
+
366
+ _initializePriorityBased () {
367
+ // Sort by priority (lower number = higher priority)
368
+ this.endpoints.sort((a, b) => a.priority - b.priority)
369
+ this.currentEndpointIndex = 0
370
+ }
371
+
372
+ _initializeRoundRobin () {
373
+ this.currentEndpointIndex = 0
374
+ }
375
+
376
+ async executeWithFailover (operation, maxAttempts = null) {
377
+ const attempts = maxAttempts || this.endpoints.length * this.errorHandler.maxRetries
378
+ let lastError = null
379
+
380
+ for (let attempt = 1; attempt <= attempts; attempt++) {
381
+ const endpoint = this._getCurrentEndpoint()
382
+
383
+ try {
384
+ this.stats.requestCount++
385
+ const startTime = Date.now()
386
+
387
+ const result = await this._executeWithTimeout(operation, endpoint)
388
+
389
+ // Record success metrics
390
+ this.stats.totalLatency += Date.now() - startTime
391
+
392
+ return result
393
+ } catch (err) {
394
+ lastError = err
395
+ const errorType = this.errorHandler.classifyError(err)
396
+
397
+ // Update error statistics
398
+ const errorCount = this.stats.errorsByType.get(errorType) || 0
399
+ this.stats.errorsByType.set(errorType, errorCount + 1)
400
+
401
+ // Check if we should retry
402
+ if (!this.errorHandler.shouldRetry(errorType, attempt) || attempt === attempts) {
403
+ throw err
404
+ }
405
+
406
+ // Move to next endpoint
407
+ await this._rotateToNextHealthyEndpoint()
408
+ this.stats.failoverCount++
409
+
410
+ // Wait before retry
411
+ const retryDelay = this.errorHandler.getRetryDelay(attempt, errorType)
412
+ console.warn(`Endpoint ${endpoint.url} failed (${errorType}), retrying in ${retryDelay}ms...`)
413
+ await new Promise(resolve => setTimeout(resolve, retryDelay))
414
+ }
415
+ }
416
+
417
+ throw lastError || new Error('All failover attempts exhausted')
418
+ }
419
+
420
+ async _executeWithTimeout (operation, endpoint) {
421
+ const timeoutPromise = new Promise((resolve, reject) =>
422
+ setTimeout(() => reject(new Error('Operation timeout')), this.connectionTimeout)
423
+ )
424
+
425
+ return Promise.race([
426
+ operation(endpoint),
427
+ timeoutPromise
428
+ ])
429
+ }
430
+
431
+ _getCurrentEndpoint () {
432
+ if (this.currentEndpointIndex >= this.endpoints.length) {
433
+ this.currentEndpointIndex = 0
434
+ }
435
+ return this.endpoints[this.currentEndpointIndex]
436
+ }
437
+
438
+ async _rotateToNextHealthyEndpoint () {
439
+ const startIndex = this.currentEndpointIndex
440
+ let attempts = 0
441
+
442
+ do {
443
+ this.currentEndpointIndex = (this.currentEndpointIndex + 1) % this.endpoints.length
444
+ attempts++
445
+
446
+ const endpoint = this.endpoints[this.currentEndpointIndex]
447
+
448
+ // Check if this endpoint is healthy
449
+ if (this.healthMonitor.isEndpointHealthy(endpoint.url)) {
450
+ return
451
+ }
452
+ } while (this.currentEndpointIndex !== startIndex && attempts < this.endpoints.length)
453
+
454
+ // If no healthy endpoints found, use current anyway (last resort)
455
+ console.warn('No healthy endpoints available, continuing with current endpoint')
456
+ }
457
+
458
+ async getConnection () {
459
+ if (!this.isInitialized) {
460
+ throw new Error('Router not initialized. Call initialize() first.')
461
+ }
462
+
463
+ const endpoint = this._getCurrentEndpoint()
464
+
465
+ // Try to reuse existing connection from pool
466
+ const poolKey = endpoint.url
467
+ if (this.connectionPool.has(poolKey)) {
468
+ return this.connectionPool.get(poolKey)
469
+ }
470
+
471
+ // Create new connection
472
+ const chronik = new ChronikClient(endpoint.url)
473
+ this.connectionPool.set(poolKey, chronik)
474
+
475
+ return chronik
476
+ }
477
+
478
+ getStats () {
479
+ const avgLatency = this.stats.requestCount > 0
480
+ ? this.stats.totalLatency / this.stats.requestCount
481
+ : 0
482
+
483
+ return {
484
+ ...this.stats,
485
+ averageLatency: Math.round(avgLatency),
486
+ currentEndpoint: this._getCurrentEndpoint().url,
487
+ healthyEndpoints: this.endpoints.filter(ep =>
488
+ this.healthMonitor.isEndpointHealthy(ep.url)
489
+ ).length,
490
+ totalEndpoints: this.endpoints.length
491
+ }
492
+ }
493
+
494
+ async cleanup () {
495
+ this.healthMonitor.stopMonitoring()
496
+ this.connectionPool.clear()
497
+ this.isInitialized = false
498
+ }
499
+ }
500
+
501
+ module.exports = {
502
+ RobustChronikRouter,
503
+ EndpointHealthMonitor,
504
+ ConnectionErrorHandler,
505
+ RobustConnectionStrategy,
506
+ ConnectionErrorType
507
+ }