ton-provider-system 0.2.2 → 0.2.4

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/README.md CHANGED
@@ -53,17 +53,23 @@ Or add to your `package.json`:
53
53
  ### Node.js (Scripts, Telegram Bot)
54
54
 
55
55
  ```typescript
56
- import { ProviderManager, getTonClient } from 'ton-provider-system';
56
+ import { ProviderManager, getTonClient, NodeAdapter } from 'ton-provider-system';
57
57
 
58
58
  // Initialize
59
59
  const pm = ProviderManager.getInstance();
60
60
  await pm.init('testnet');
61
61
 
62
- // Get TonClient for blockchain operations
63
- const client = await getTonClient(pm);
62
+ // Option 1: Use adapter (RECOMMENDED - handles rate limiting automatically)
63
+ const adapter = new NodeAdapter(pm);
64
+ const balance = await adapter.getAddressBalance(address);
65
+ const state = await adapter.getAddressState(address);
64
66
 
65
- // Use the client
66
- const balance = await client.getBalance(address);
67
+ // Option 2: Use TonClient with rate limiting
68
+ const client = await getTonClient(pm);
69
+ // Always use getEndpointWithRateLimit() before operations
70
+ const endpoint = await pm.getEndpointWithRateLimit();
71
+ // Note: TonClient doesn't automatically respect rate limits
72
+ // Consider using adapter methods instead
67
73
  ```
68
74
 
69
75
  ### Browser (React/Next.js)
@@ -72,12 +78,16 @@ const balance = await client.getBalance(address);
72
78
  import { ProviderManager, BrowserAdapter } from 'ton-provider-system';
73
79
 
74
80
  // Create instance (not singleton for React)
81
+ // IMPORTANT: Use 'browser' adapter to filter CORS-incompatible providers
75
82
  const pm = new ProviderManager({ adapter: 'browser' });
76
83
  await pm.init(network);
77
84
 
78
85
  // Use browser adapter for fetch-based operations
86
+ // Adapter methods automatically handle rate limiting
79
87
  const adapter = new BrowserAdapter(pm);
80
88
  const balance = await adapter.getAddressBalance(address);
89
+ const state = await adapter.getAddressState(address);
90
+ const result = await adapter.runGetMethod(address, 'method', []);
81
91
  ```
82
92
 
83
93
  ## Configuration
@@ -140,10 +150,11 @@ const pm = new ProviderManager({ adapter: 'browser' });
140
150
  await pm.init('testnet');
141
151
  await pm.init('mainnet');
142
152
 
143
- // Get endpoint URL
153
+ // Get endpoint URL (no rate limiting - use for one-off requests)
144
154
  const endpoint = await pm.getEndpoint();
145
155
 
146
- // Get endpoint with rate limiting
156
+ // Get endpoint with rate limiting (RECOMMENDED for production)
157
+ // Waits for rate limit token before returning endpoint
147
158
  const endpoint = await pm.getEndpointWithRateLimit(5000);
148
159
 
149
160
  // Test all providers
@@ -394,18 +405,104 @@ pnpm test:verbose
394
405
 
395
406
  ### No providers available
396
407
 
397
- 1. Check `.env` file has API keys configured
398
- 2. Run `pnpm check-connection` to test providers
408
+ **Symptoms**: `No providers available, using fallback` warning
409
+
410
+ **Solutions**:
411
+ 1. Check `.env` file has API keys configured for at least one provider
412
+ 2. Verify environment variables are loaded (use `dotenv` or similar)
413
+ 3. Run `pnpm test` to test all providers
414
+ 4. Check provider health: `const results = await pm.testAllProviders()`
399
415
 
400
- ### Rate limit errors
416
+ ### Rate limit errors (429)
401
417
 
402
- 1. The system automatically switches to next provider on 429 errors
403
- 2. Configure more providers in `.env` for redundancy
418
+ **Symptoms**: Frequent 429 errors, requests failing
419
+
420
+ **Solutions**:
421
+ 1. **Use `getEndpointWithRateLimit()`** instead of `getEndpoint()` - this is the recommended approach
422
+ 2. Use adapter methods (`adapter.getAddressState()`) which automatically handle rate limiting
423
+ 3. The system automatically switches to next provider on 429 errors
424
+ 4. Configure more providers in `.env` for redundancy
425
+ 5. Check RPS limits in `rpc.json` - some providers have very low limits (e.g., Tatum: 3 RPS)
426
+
427
+ **Example**:
428
+ ```typescript
429
+ // ❌ BAD - bypasses rate limiting
430
+ const endpoint = await pm.getEndpoint();
431
+ const client = new TonClient({ endpoint });
432
+ await client.getBalance(address); // May hit rate limit
433
+
434
+ // ✅ GOOD - respects rate limiting
435
+ const endpoint = await pm.getEndpointWithRateLimit();
436
+ const client = new TonClient({ endpoint });
437
+ await client.getBalance(address);
438
+
439
+ // ✅ BEST - adapter handles everything
440
+ const adapter = new NodeAdapter(pm);
441
+ const balance = await adapter.getAddressBalance(address);
442
+ ```
404
443
 
405
444
  ### Block height mismatch (stale provider)
406
445
 
407
- 1. Provider is returning old data
408
- 2. System marks it as `stale` and prefers fresh providers
446
+ **Symptoms**: Provider returns old block data
447
+
448
+ **Solutions**:
449
+ 1. System automatically marks stale providers and prefers fresh ones
450
+ 2. Stale providers are still used if no fresh providers available
451
+ 3. Check provider health: `const health = pm.getHealthChecker()?.getResult(providerId, network)`
452
+
453
+ ### Provider failures (503, 502, timeout)
454
+
455
+ **Symptoms**: Providers marked as offline, frequent failovers
456
+
457
+ **Solutions**:
458
+ 1. These are usually temporary infrastructure issues
459
+ 2. System automatically fails over to next provider
460
+ 3. Failed providers are retried after cooldown period (default: 30 seconds)
461
+ 4. Check provider status: `const results = await pm.testAllProviders()`
462
+ 5. Permanent errors (404, 401) are not retried - check API keys
463
+
464
+ ### Browser compatibility (CORS errors)
465
+
466
+ **Symptoms**: CORS errors in browser, providers not working
467
+
468
+ **Solutions**:
469
+ 1. Use `BrowserAdapter` instead of direct `TonClient` in browser
470
+ 2. Some providers are not browser-compatible (e.g., Tatum) - they're automatically filtered
471
+ 3. Check browser compatibility: `const providers = pm.getProviders()` (already filtered)
472
+ 4. Use `adapter: 'browser'` when creating `ProviderManager` in browser environment
473
+
474
+ **Example**:
475
+ ```typescript
476
+ // ✅ Correct for browser
477
+ const pm = new ProviderManager({ adapter: 'browser' });
478
+ await pm.init('testnet');
479
+ const adapter = new BrowserAdapter(pm);
480
+ const balance = await adapter.getAddressBalance(address);
481
+ ```
482
+
483
+ ### Error handling and failover
484
+
485
+ **Symptoms**: Need to handle provider failures gracefully
486
+
487
+ **Solutions**:
488
+ 1. Always wrap operations in try-catch
489
+ 2. Call `pm.reportError(error)` on failures to trigger failover
490
+ 3. Call `pm.reportSuccess()` on success to update rate limiter
491
+ 4. System automatically fails over, but manual reporting improves accuracy
492
+
493
+ **Example**:
494
+ ```typescript
495
+ try {
496
+ const endpoint = await pm.getEndpointWithRateLimit();
497
+ const client = new TonClient({ endpoint });
498
+ const result = await client.someMethod();
499
+ pm.reportSuccess(); // Update rate limiter
500
+ return result;
501
+ } catch (error) {
502
+ pm.reportError(error); // Trigger failover
503
+ throw error;
504
+ }
505
+ ```
409
506
 
410
507
  ## Publishing
411
508
 
package/dist/index.cjs CHANGED
@@ -1157,17 +1157,26 @@ var HealthChecker = class {
1157
1157
  const endTime = performance.now();
1158
1158
  const latencyMs = Math.round(endTime - startTime);
1159
1159
  const errorMsg = error.message || String(error) || "Unknown error";
1160
+ const errorMsgLower = errorMsg.toLowerCase();
1160
1161
  const isCorsError = this.isCorsError(error, errorMsg);
1161
- const is429 = errorMsg.includes("429") || errorMsg.toLowerCase().includes("rate limit");
1162
- const is404 = errorMsg.includes("404") || errorMsg.toLowerCase().includes("not found");
1163
- const is503 = errorMsg.includes("503") || errorMsg.toLowerCase().includes("service unavailable");
1164
- const is502 = errorMsg.includes("502") || errorMsg.toLowerCase().includes("bad gateway");
1165
- const isTimeout = error.name === "AbortError" || errorMsg.includes("timeout");
1166
- const isOnFinalityBackendError = provider.type === "onfinality" && (errorMsg.includes("Backend error") || errorMsg.includes("backend error"));
1162
+ const responseStatus = error?.response?.status || error?.status || error?.statusCode || null;
1163
+ const statusMatch = errorMsg.match(/\b(\d{3})\b/);
1164
+ const statusFromMsg = statusMatch ? parseInt(statusMatch[1], 10) : null;
1165
+ const httpStatus = responseStatus || statusFromMsg;
1166
+ const is429 = httpStatus === 429 || errorMsgLower.includes("429") || errorMsgLower.includes("rate limit") || errorMsgLower.includes("too many requests");
1167
+ const is404 = httpStatus === 404 || errorMsgLower.includes("404") || errorMsgLower.includes("not found");
1168
+ const is401 = httpStatus === 401 || errorMsgLower.includes("401") || errorMsgLower.includes("unauthorized") || errorMsgLower.includes("invalid api key") || errorMsgLower.includes("authentication failed");
1169
+ const is403 = httpStatus === 403 || errorMsgLower.includes("403") || errorMsgLower.includes("forbidden");
1170
+ const is503 = httpStatus === 503 || errorMsgLower.includes("503") || errorMsgLower.includes("service unavailable");
1171
+ const is502 = httpStatus === 502 || errorMsgLower.includes("502") || errorMsgLower.includes("bad gateway");
1172
+ const isTimeout = error.name === "AbortError" || errorMsgLower.includes("timeout") || errorMsgLower.includes("timed out") || errorMsgLower.includes("aborted");
1173
+ const isOnFinalityBackendError = provider.type === "onfinality" && (errorMsgLower.includes("backend error") || errorMsgLower.includes("backend error"));
1167
1174
  let status = "offline";
1168
1175
  if (is429) {
1169
1176
  status = "degraded";
1170
- } else if (is404 || is503 || is502 || isOnFinalityBackendError) {
1177
+ } else if (is404 || is401 || is403) {
1178
+ status = "offline";
1179
+ } else if (is503 || is502 || isOnFinalityBackendError) {
1171
1180
  status = "offline";
1172
1181
  } else if (isTimeout) {
1173
1182
  status = "offline";
@@ -1195,8 +1204,9 @@ var HealthChecker = class {
1195
1204
  *
1196
1205
  * @param batchSize - Number of providers to test in parallel (default: 2)
1197
1206
  * @param batchDelayMs - Delay between batches in milliseconds (default: 500 to avoid rate limits)
1207
+ * If not provided, calculates delay based on lowest RPS in batch
1198
1208
  */
1199
- async testProviders(providers, batchSize = 2, batchDelayMs = 500) {
1209
+ async testProviders(providers, batchSize = 2, batchDelayMs) {
1200
1210
  const results = [];
1201
1211
  for (let i = 0; i < providers.length; i += batchSize) {
1202
1212
  const batch = providers.slice(i, i + batchSize);
@@ -1204,8 +1214,15 @@ var HealthChecker = class {
1204
1214
  batch.map((p) => this.testProvider(p))
1205
1215
  );
1206
1216
  results.push(...batchResults);
1207
- if (i + batchSize < providers.length && batchDelayMs > 0) {
1208
- await this.sleep(batchDelayMs);
1217
+ if (i + batchSize < providers.length) {
1218
+ let delay = batchDelayMs;
1219
+ if (delay === void 0) {
1220
+ const minRps = Math.min(...batch.map((p) => p.rps || 1));
1221
+ delay = Math.max(500, Math.ceil(1e3 / minRps * 1.5));
1222
+ }
1223
+ if (delay > 0) {
1224
+ await this.sleep(delay);
1225
+ }
1209
1226
  }
1210
1227
  }
1211
1228
  return results;
@@ -1594,24 +1611,36 @@ var TokenBucketRateLimiter = class {
1594
1611
  const startTime = Date.now();
1595
1612
  if (this.processing) {
1596
1613
  const acquired = await new Promise((resolve) => {
1614
+ let timeoutInterval = null;
1615
+ let resolved = false;
1616
+ const cleanup = () => {
1617
+ if (timeoutInterval !== null) {
1618
+ clearInterval(timeoutInterval);
1619
+ timeoutInterval = null;
1620
+ }
1621
+ };
1597
1622
  const checkTimeout = () => {
1598
1623
  if (Date.now() - startTime > timeoutMs) {
1599
1624
  const idx = this.requestQueue.indexOf(resolveCallback);
1600
1625
  if (idx >= 0) {
1601
1626
  this.requestQueue.splice(idx, 1);
1602
1627
  }
1603
- resolve(false);
1628
+ if (!resolved) {
1629
+ resolved = true;
1630
+ cleanup();
1631
+ resolve(false);
1632
+ }
1604
1633
  }
1605
1634
  };
1606
- const resolveCallback = () => resolve(true);
1607
- this.requestQueue.push(resolveCallback);
1608
- const timeoutInterval = setInterval(checkTimeout, 1e3);
1609
- const cleanup = () => clearInterval(timeoutInterval);
1610
- Promise.resolve().then(() => {
1611
- if (this.requestQueue.includes(resolveCallback)) ; else {
1635
+ const resolveCallback = () => {
1636
+ if (!resolved) {
1637
+ resolved = true;
1612
1638
  cleanup();
1639
+ resolve(true);
1613
1640
  }
1614
- });
1641
+ };
1642
+ this.requestQueue.push(resolveCallback);
1643
+ timeoutInterval = setInterval(checkTimeout, 1e3);
1615
1644
  });
1616
1645
  if (!acquired) {
1617
1646
  return false;
@@ -1619,24 +1648,25 @@ var TokenBucketRateLimiter = class {
1619
1648
  }
1620
1649
  this.processing = true;
1621
1650
  try {
1622
- this.refill();
1623
1651
  if (this.currentBackoff > 0) {
1624
1652
  this.logger.debug(`Applying backoff: ${this.currentBackoff}ms`);
1625
1653
  await sleep(this.currentBackoff);
1626
1654
  this.lastRefill = Date.now();
1627
- this.currentBackoff = 0;
1628
1655
  }
1656
+ this.refill();
1629
1657
  while (this.tokens <= 0) {
1630
1658
  if (Date.now() - startTime > timeoutMs) {
1631
1659
  return false;
1632
1660
  }
1633
- await sleep(Math.min(100, this.config.minDelayMs));
1661
+ const waitTime = Math.min(100, this.config.minDelayMs);
1662
+ await sleep(waitTime);
1634
1663
  this.refill();
1635
1664
  }
1636
1665
  this.tokens--;
1637
1666
  const timeSinceLastRefill = Date.now() - this.lastRefill;
1638
1667
  if (timeSinceLastRefill < this.config.minDelayMs) {
1639
- await sleep(this.config.minDelayMs - timeSinceLastRefill);
1668
+ const remainingDelay = this.config.minDelayMs - timeSinceLastRefill;
1669
+ await sleep(remainingDelay);
1640
1670
  }
1641
1671
  this.lastRefill = Date.now();
1642
1672
  return true;
@@ -1654,10 +1684,21 @@ var TokenBucketRateLimiter = class {
1654
1684
  release() {
1655
1685
  }
1656
1686
  /**
1657
- * Report a successful request (resets backoff)
1687
+ * Report a successful request (gradually reduces backoff)
1688
+ *
1689
+ * Instead of immediately clearing backoff, we gradually reduce it to prevent
1690
+ * immediately hitting the rate limit again after a single success.
1658
1691
  */
1659
1692
  reportSuccess() {
1660
- this.currentBackoff = 0;
1693
+ if (this.currentBackoff > 0) {
1694
+ this.currentBackoff = Math.max(
1695
+ this.config.minDelayMs,
1696
+ Math.floor(this.currentBackoff / 2)
1697
+ );
1698
+ if (this.currentBackoff <= this.config.minDelayMs) {
1699
+ this.currentBackoff = 0;
1700
+ }
1701
+ }
1661
1702
  this.consecutiveErrors = 0;
1662
1703
  }
1663
1704
  /**
@@ -1848,7 +1889,9 @@ var DEFAULT_CONFIG2 = {
1848
1889
  latencyWeight: 0.4,
1849
1890
  priorityWeight: 0.3,
1850
1891
  freshnessWeight: 0.3,
1851
- minStatus: ["available", "degraded"]
1892
+ minStatus: ["available", "degraded"],
1893
+ retryCooldownMs: 3e4
1894
+ // 30 seconds
1852
1895
  };
1853
1896
  var ProviderSelector = class {
1854
1897
  constructor(registry, healthChecker, config, logger, adapter = "node") {
@@ -1942,14 +1985,19 @@ var ProviderSelector = class {
1942
1985
  return defaultProvider;
1943
1986
  }
1944
1987
  if (health.success === false && health.lastTested) {
1945
- const timeSinceFailure = Date.now() - health.lastTested.getTime();
1946
- const cooldownMs = 3e4;
1947
- if (timeSinceFailure > cooldownMs) {
1948
- this.logger.warn(
1949
- `No healthy providers for ${network}, retrying failed default after cooldown: ${defaultProvider.id}`
1988
+ if (this.isRetryableError(health.error)) {
1989
+ const timeSinceFailure = Date.now() - health.lastTested.getTime();
1990
+ if (timeSinceFailure > this.config.retryCooldownMs) {
1991
+ this.logger.warn(
1992
+ `No healthy providers for ${network}, retrying failed default after cooldown: ${defaultProvider.id}`
1993
+ );
1994
+ this.activeProviderByNetwork.set(network, defaultProvider.id);
1995
+ return defaultProvider;
1996
+ }
1997
+ } else {
1998
+ this.logger.debug(
1999
+ `Skipping provider ${defaultProvider.id} with permanent error: ${health.error}`
1950
2000
  );
1951
- this.activeProviderByNetwork.set(network, defaultProvider.id);
1952
- return defaultProvider;
1953
2001
  }
1954
2002
  }
1955
2003
  }
@@ -1963,14 +2011,19 @@ var ProviderSelector = class {
1963
2011
  return provider;
1964
2012
  }
1965
2013
  if (health.success === false && health.lastTested) {
1966
- const timeSinceFailure = Date.now() - health.lastTested.getTime();
1967
- const cooldownMs = 3e4;
1968
- if (timeSinceFailure > cooldownMs) {
1969
- this.logger.warn(
1970
- `No healthy providers for ${network}, retrying failed provider after cooldown: ${provider.id}`
2014
+ if (this.isRetryableError(health.error)) {
2015
+ const timeSinceFailure = Date.now() - health.lastTested.getTime();
2016
+ if (timeSinceFailure > this.config.retryCooldownMs) {
2017
+ this.logger.warn(
2018
+ `No healthy providers for ${network}, retrying failed provider after cooldown: ${provider.id}`
2019
+ );
2020
+ this.activeProviderByNetwork.set(network, provider.id);
2021
+ return provider;
2022
+ }
2023
+ } else {
2024
+ this.logger.debug(
2025
+ `Skipping provider ${provider.id} with permanent error: ${health.error}`
1971
2026
  );
1972
- this.activeProviderByNetwork.set(network, provider.id);
1973
- return provider;
1974
2027
  }
1975
2028
  }
1976
2029
  }
@@ -1985,6 +2038,12 @@ var ProviderSelector = class {
1985
2038
  this.logger.debug(
1986
2039
  `Best provider for ${network}: ${best.id} (score: ${scored[0].score.toFixed(2)})`
1987
2040
  );
2041
+ } else if (bestHealth && bestHealth.success === false) {
2042
+ this.bestProviderByNetwork.delete(network);
2043
+ this.activeProviderByNetwork.set(network, best.id);
2044
+ this.logger.debug(
2045
+ `Best provider for ${network}: ${best.id} (score: ${scored[0].score.toFixed(2)}, failed - not cached)`
2046
+ );
1988
2047
  } else {
1989
2048
  this.activeProviderByNetwork.set(network, best.id);
1990
2049
  this.logger.debug(
@@ -2035,11 +2094,12 @@ var ProviderSelector = class {
2035
2094
  return 0.01 * (1 / (provider.priority + 1));
2036
2095
  }
2037
2096
  if (health.success === false) {
2038
- if (health.lastTested) {
2039
- const timeSinceFailure = Date.now() - health.lastTested.getTime();
2040
- const cooldownMs = 3e4;
2041
- if (timeSinceFailure > cooldownMs) {
2042
- return 1e-3 * (1 / (provider.priority + 1));
2097
+ if (this.isRetryableError(health.error)) {
2098
+ if (health.lastTested) {
2099
+ const timeSinceFailure = Date.now() - health.lastTested.getTime();
2100
+ if (timeSinceFailure > this.config.retryCooldownMs) {
2101
+ return 1e-3 * (1 / (provider.priority + 1));
2102
+ }
2043
2103
  }
2044
2104
  }
2045
2105
  return 0;
@@ -2234,6 +2294,34 @@ var ProviderSelector = class {
2234
2294
  return true;
2235
2295
  });
2236
2296
  }
2297
+ /**
2298
+ * Check if an error is retryable (temporary) or permanent
2299
+ *
2300
+ * Permanent errors (never retry):
2301
+ * - 404 (Not Found) - endpoint doesn't exist
2302
+ * - 401 (Unauthorized) - invalid API key
2303
+ * - Invalid API key errors
2304
+ *
2305
+ * Retryable errors (can retry after cooldown):
2306
+ * - 503 (Service Unavailable) - temporary server issue
2307
+ * - 502 (Bad Gateway) - temporary gateway issue
2308
+ * - Timeout errors - network issues
2309
+ * - Network errors - connection issues
2310
+ * - 429 (Rate Limit) - temporary rate limit (handled separately)
2311
+ */
2312
+ isRetryableError(error) {
2313
+ if (!error) {
2314
+ return true;
2315
+ }
2316
+ const errorLower = error.toLowerCase();
2317
+ if (errorLower.includes("404") || errorLower.includes("not found") || errorLower.includes("401") || errorLower.includes("unauthorized") || errorLower.includes("invalid api key") || errorLower.includes("api key is invalid") || errorLower.includes("authentication failed") || errorLower.includes("forbidden")) {
2318
+ return false;
2319
+ }
2320
+ if (errorLower.includes("503") || errorLower.includes("service unavailable") || errorLower.includes("502") || errorLower.includes("bad gateway") || errorLower.includes("timeout") || errorLower.includes("network error") || errorLower.includes("connection") || errorLower.includes("econnrefused") || errorLower.includes("enotfound")) {
2321
+ return true;
2322
+ }
2323
+ return true;
2324
+ }
2237
2325
  };
2238
2326
  function createSelector(registry, healthChecker, config, logger, adapter = "node") {
2239
2327
  return new ProviderSelector(registry, healthChecker, config, logger, adapter);
@@ -2657,10 +2745,17 @@ var _ProviderManager = class _ProviderManager {
2657
2745
  }
2658
2746
  const acquired = await this.rateLimiter.acquire(provider.id, timeoutMs);
2659
2747
  if (!acquired) {
2660
- this.options.logger.warn(`Rate limit timeout for ${provider.id}`);
2748
+ const timeoutError = new Error(`Rate limit token acquisition timeout for ${provider.id} after ${timeoutMs || 6e4}ms`);
2749
+ this.options.logger.warn(timeoutError.message);
2750
+ this.reportError(timeoutError);
2661
2751
  const next = this.selector.getNextProvider(this.network, [provider.id]);
2662
2752
  if (next) {
2663
- return normalizeV2Endpoint(next.endpointV2, next);
2753
+ const nextTimeout = timeoutMs ? Math.min(timeoutMs, 1e4) : 1e4;
2754
+ const nextAcquired = await this.rateLimiter.acquire(next.id, nextTimeout);
2755
+ if (nextAcquired) {
2756
+ return normalizeV2Endpoint(next.endpointV2, next);
2757
+ }
2758
+ this.options.logger.warn(`Rate limit timeout for next provider ${next.id}, using fallback`);
2664
2759
  }
2665
2760
  return this.getFallbackEndpoint();
2666
2761
  }
@@ -2710,26 +2805,48 @@ var _ProviderManager = class _ProviderManager {
2710
2805
  if (!provider) {
2711
2806
  provider = this.selector.getBestProvider(this.network);
2712
2807
  }
2713
- if (!provider) return;
2808
+ if (!provider) {
2809
+ this.options.logger.warn(`Cannot report error: no provider available for ${this.network}`);
2810
+ return;
2811
+ }
2714
2812
  const errorMsg = error instanceof Error ? error.message : String(error);
2715
2813
  const errorMsgLower = errorMsg.toLowerCase();
2716
- const is429 = errorMsgLower.includes("429") || errorMsgLower.includes("rate limit");
2717
- const is503 = errorMsgLower.includes("503") || errorMsgLower.includes("service unavailable");
2718
- const is502 = errorMsgLower.includes("502") || errorMsgLower.includes("bad gateway");
2719
- const is404 = errorMsgLower.includes("404") || errorMsgLower.includes("not found");
2720
- const isTimeout = errorMsgLower.includes("timeout") || errorMsgLower.includes("abort");
2814
+ const enhancedErrorMsg = `Provider ${provider.id} (${provider.name}): ${errorMsg}`;
2815
+ const responseStatus = error instanceof Error && error?.response?.status || error instanceof Error && error?.status || error instanceof Error && error?.statusCode || null;
2816
+ const statusMatch = errorMsg.match(/\b(\d{3})\b/);
2817
+ const statusFromMsg = statusMatch ? parseInt(statusMatch[1], 10) : null;
2818
+ const httpStatus = responseStatus || statusFromMsg;
2819
+ const is429 = httpStatus === 429 || errorMsgLower.includes("429") || errorMsgLower.includes("rate limit") || errorMsgLower.includes("too many requests");
2820
+ const is503 = httpStatus === 503 || errorMsgLower.includes("503") || errorMsgLower.includes("service unavailable");
2821
+ const is502 = httpStatus === 502 || errorMsgLower.includes("502") || errorMsgLower.includes("bad gateway");
2822
+ const is404 = httpStatus === 404 || errorMsgLower.includes("404") || errorMsgLower.includes("not found");
2823
+ const is401 = httpStatus === 401 || errorMsgLower.includes("401") || errorMsgLower.includes("unauthorized") || errorMsgLower.includes("invalid api key") || errorMsgLower.includes("authentication failed");
2824
+ const isTimeout = error instanceof Error && error.name === "AbortError" || errorMsgLower.includes("timeout") || errorMsgLower.includes("timed out") || errorMsgLower.includes("abort");
2721
2825
  if (isRateLimitError(error) || is429) {
2722
2826
  this.rateLimiter.reportRateLimitError(provider.id);
2723
- this.healthChecker.markDegraded(provider.id, this.network, errorMsg);
2724
- } else if (is503 || is502 || is404 || isTimeout) {
2827
+ this.healthChecker.markDegraded(provider.id, this.network, enhancedErrorMsg);
2828
+ this.options.logger.warn(`${enhancedErrorMsg} - Rate limit detected, switching to next provider`);
2829
+ } else if (is404 || is401) {
2830
+ this.rateLimiter.reportError(provider.id);
2831
+ this.healthChecker.markOffline(provider.id, this.network, enhancedErrorMsg);
2832
+ this.options.logger.error(`${enhancedErrorMsg} - Permanent error (${is404 ? "404" : "401"}), provider marked offline`);
2833
+ } else if (is503 || is502 || isTimeout) {
2725
2834
  this.rateLimiter.reportError(provider.id);
2726
- this.healthChecker.markOffline(provider.id, this.network, errorMsg);
2835
+ this.healthChecker.markOffline(provider.id, this.network, enhancedErrorMsg);
2836
+ const errorType = is503 ? "503" : is502 ? "502" : "timeout";
2837
+ this.options.logger.warn(`${enhancedErrorMsg} - Server error (${errorType}), switching to next provider`);
2727
2838
  } else {
2728
2839
  this.rateLimiter.reportError(provider.id);
2729
- this.healthChecker.markDegraded(provider.id, this.network, errorMsg);
2840
+ this.healthChecker.markDegraded(provider.id, this.network, enhancedErrorMsg);
2841
+ this.options.logger.warn(`${enhancedErrorMsg} - Unknown error, switching to next provider`);
2842
+ }
2843
+ const nextProvider = this.selector.handleProviderFailure(provider.id, this.network);
2844
+ if (nextProvider) {
2845
+ this.options.logger.info(`Failover: switched from ${provider.id} to ${nextProvider.id}`);
2846
+ } else {
2847
+ this.options.logger.warn(`Failover: no alternative provider available for ${this.network}`);
2730
2848
  }
2731
2849
  this.selector.clearCache(this.network);
2732
- this.selector.handleProviderFailure(provider.id, this.network);
2733
2850
  this.notifyListeners();
2734
2851
  }
2735
2852
  // ========================================================================
@@ -3280,9 +3397,11 @@ var BrowserAdapter = class {
3280
3397
  // ========================================================================
3281
3398
  /**
3282
3399
  * Make a JSON-RPC call to the TON API
3400
+ *
3401
+ * Note: Uses rate limiting to prevent 429 errors.
3283
3402
  */
3284
3403
  async jsonRpc(method, params = {}, timeoutMs = 1e4) {
3285
- const endpoint = await this.manager.getEndpoint();
3404
+ const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
3286
3405
  const controller = new AbortController();
3287
3406
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
3288
3407
  try {
@@ -3321,9 +3440,11 @@ var BrowserAdapter = class {
3321
3440
  // ========================================================================
3322
3441
  /**
3323
3442
  * Get address state
3443
+ *
3444
+ * Note: Uses rate limiting to prevent 429 errors.
3324
3445
  */
3325
3446
  async getAddressState(address, timeoutMs = 1e4) {
3326
- const endpoint = await this.manager.getEndpoint();
3447
+ const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
3327
3448
  const baseV2 = toV2Base(endpoint);
3328
3449
  const url = `${baseV2}/getAddressState?address=${encodeURIComponent(address)}`;
3329
3450
  const controller = new AbortController();
@@ -3353,9 +3474,11 @@ var BrowserAdapter = class {
3353
3474
  }
3354
3475
  /**
3355
3476
  * Get address balance
3477
+ *
3478
+ * Note: Uses rate limiting to prevent 429 errors.
3356
3479
  */
3357
3480
  async getAddressBalance(address, timeoutMs = 1e4) {
3358
- const endpoint = await this.manager.getEndpoint();
3481
+ const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
3359
3482
  const baseV2 = toV2Base(endpoint);
3360
3483
  const url = `${baseV2}/getAddressBalance?address=${encodeURIComponent(address)}`;
3361
3484
  const controller = new AbortController();
@@ -3385,9 +3508,11 @@ var BrowserAdapter = class {
3385
3508
  }
3386
3509
  /**
3387
3510
  * Get address information
3511
+ *
3512
+ * Note: Uses rate limiting to prevent 429 errors.
3388
3513
  */
3389
3514
  async getAddressInfo(address, timeoutMs = 1e4) {
3390
- const endpoint = await this.manager.getEndpoint();
3515
+ const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
3391
3516
  const baseV2 = toV2Base(endpoint);
3392
3517
  const url = `${baseV2}/getAddressInformation?address=${encodeURIComponent(address)}`;
3393
3518
  const controller = new AbortController();
@@ -3415,9 +3540,11 @@ var BrowserAdapter = class {
3415
3540
  }
3416
3541
  /**
3417
3542
  * Run get method
3543
+ *
3544
+ * Note: Uses rate limiting to prevent 429 errors.
3418
3545
  */
3419
3546
  async runGetMethod(address, method, stack = [], timeoutMs = 15e3) {
3420
- const endpoint = await this.manager.getEndpoint();
3547
+ const endpoint = await this.manager.getEndpointWithRateLimit(timeoutMs);
3421
3548
  const baseV2 = toV2Base(endpoint);
3422
3549
  const url = `${baseV2}/runGetMethod`;
3423
3550
  const controller = new AbortController();